001package org.apache.maven.scm.provider.git.repository;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.maven.scm.ScmException;
023import org.apache.maven.scm.provider.ScmProviderRepository;
024import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost;
025
026import java.io.UnsupportedEncodingException;
027import java.net.URLDecoder;
028import java.net.URLEncoder;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031
032/**
033 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
034 * @author <a href="mailto:struberg@apache.org">Mark Struberg</a>
035 *
036 */
037public class GitScmProviderRepository
038    extends ScmProviderRepositoryWithHost
039{
040
041    /**
042     * sequence used to delimit the fetch URL
043     */
044    public static final String URL_DELIMITER_FETCH = "[fetch=]";
045
046    /**
047     * sequence used to delimit the push URL
048     */
049    public static final String URL_DELIMITER_PUSH = "[push=]";
050
051    /**
052     * this trails every protocol
053     */
054    public static final String PROTOCOL_SEPARATOR = "://";
055
056    /**
057     * use local file as transport
058     */
059    public static final String PROTOCOL_FILE = "file";
060
061    /**
062     * use gits internal protocol
063     */
064    public static final String PROTOCOL_GIT = "git";
065
066    /**
067     * use secure shell protocol
068     */
069    public static final String PROTOCOL_SSH = "ssh";
070
071    /**
072     * use the standard port 80 http protocol
073     */
074    public static final String PROTOCOL_HTTP = "http";
075
076    /**
077     * use the standard port 443 https protocol
078     */
079    public static final String PROTOCOL_HTTPS = "https";
080
081    /**
082     * use rsync for retrieving the data
083     * TODO implement!
084     */
085    public static final String PROTOCOL_RSYNC = "rsync";
086
087    private static final Pattern HOST_AND_PORT_EXTRACTOR =
088        Pattern.compile( "([^:/\\\\~]*)(?::(\\d*))?(?:([:/\\\\~])(.*))?" );
089
090    /**
091     * No special protocol specified. Git will either use git://
092     * or ssh:// depending on whether we work locally or over the network
093     */
094    public static final String PROTOCOL_NONE = "";
095
096    /**
097     * this may either 'git' or 'jgit' depending on the underlying implementation being used
098     */
099    private String provider;
100
101    /**
102     * the URL used to fetch from the upstream repository
103     */
104    private RepositoryUrl fetchInfo;
105
106    /**
107     * the URL used to push to the upstream repository
108     */
109    private RepositoryUrl pushInfo;
110
111    public GitScmProviderRepository( String url )
112        throws ScmException
113    {
114        if ( url == null )
115        {
116            throw new ScmException( "url must not be null" );
117        }
118
119        if ( url.startsWith( URL_DELIMITER_FETCH ) )
120        {
121            String fetch = url.substring( URL_DELIMITER_FETCH.length() );
122
123            int indexPushDelimiter = fetch.indexOf( URL_DELIMITER_PUSH );
124            if ( indexPushDelimiter >= 0 )
125            {
126                String push = fetch.substring( indexPushDelimiter + URL_DELIMITER_PUSH.length() );
127                pushInfo = parseUrl( push );
128
129                fetch = fetch.substring( 0, indexPushDelimiter );
130            }
131
132            fetchInfo = parseUrl( fetch );
133
134            if ( pushInfo == null )
135            {
136                pushInfo = fetchInfo;
137            }
138        }
139        else if ( url.startsWith( URL_DELIMITER_PUSH ) )
140        {
141            String push = url.substring( URL_DELIMITER_PUSH.length() );
142
143            int indexFetchDelimiter = push.indexOf( URL_DELIMITER_FETCH );
144            if ( indexFetchDelimiter >= 0 )
145            {
146                String fetch = push.substring( indexFetchDelimiter + URL_DELIMITER_FETCH.length() );
147                fetchInfo = parseUrl( fetch );
148
149                push = push.substring( 0, indexFetchDelimiter );
150            }
151
152            pushInfo = parseUrl( push );
153
154            if ( fetchInfo == null )
155            {
156                fetchInfo = pushInfo;
157            }
158        }
159        else
160        {
161            fetchInfo = pushInfo = parseUrl( url );
162        }
163
164        // set the default values for backward compatibility from the push url
165        // because it's more likely that the push URL contains 'better' credentials
166        setUser( pushInfo.getUserName() );
167        setPassword( pushInfo.getPassword() );
168        setHost( pushInfo.getHost() );
169        if ( pushInfo.getPort() != null && pushInfo.getPort().length() > 0 )
170        {
171            setPort( Integer.parseInt( pushInfo.getPort() ) );
172        }
173    }
174
175    public GitScmProviderRepository( String url, String user, String password )
176        throws ScmException
177    {
178        this( url );
179
180        setUser( user );
181
182        setPassword( password );
183    }
184
185    /**
186     * @return either 'git' or 'jgit' depending on the underlying implementation being used
187     */
188    public String getProvider()
189    {
190        return provider;
191    }
192
193    public RepositoryUrl getFetchInfo()
194    {
195        return fetchInfo;
196    }
197
198    public RepositoryUrl getPushInfo()
199    {
200        return pushInfo;
201    }
202
203
204    /**
205     * @return the URL used to fetch from the upstream repository
206     */
207    public String getFetchUrl()
208    {
209        return getUrl( fetchInfo );
210    }
211
212    /**
213     * @return the URL used to push to the upstream repository
214     */
215    public String getPushUrl()
216    {
217        return getUrl( pushInfo );
218    }
219
220
221    /**
222     * Parse the given url string and store all the extracted
223     * information in a {@code RepositoryUrl}
224     *
225     * @param url to parse
226     * @return filled with the information from the given URL
227     * @throws ScmException
228     */
229    private RepositoryUrl parseUrl( String url )
230        throws ScmException
231    {
232        RepositoryUrl repoUrl = new RepositoryUrl();
233
234        url = parseProtocol( repoUrl, url );
235        url = parseUserInfo( repoUrl, url );
236        url = parseHostAndPort( repoUrl, url );
237        // the rest of the url must be the path to the repository on the server
238        repoUrl.setPath( url );
239        return repoUrl;
240    }
241
242
243    /**
244     * @param repoUrl
245     * @return
246     */
247    private String getUrl( RepositoryUrl repoUrl )
248    {
249        StringBuilder urlSb = new StringBuilder( repoUrl.getProtocol() );
250        boolean urlSupportsUserInformation = false;
251
252        if ( PROTOCOL_SSH.equals( repoUrl.getProtocol() ) || PROTOCOL_RSYNC.equals( repoUrl.getProtocol() )
253            || PROTOCOL_GIT.equals( repoUrl.getProtocol() ) || PROTOCOL_HTTP.equals( repoUrl.getProtocol() )
254            || PROTOCOL_HTTPS.equals( repoUrl.getProtocol() ) || PROTOCOL_NONE.equals( repoUrl.getProtocol() ) )
255        {
256            urlSupportsUserInformation = true;
257        }
258
259        if ( repoUrl.getProtocol() != null && repoUrl.getProtocol().length() > 0 )
260        {
261            urlSb.append( "://" );
262        }
263
264        // add user information if given and allowed for the protocol
265        if ( urlSupportsUserInformation )
266        {
267            String userName = repoUrl.getUserName();
268            // if specified on the commandline or other configuration, we take this.
269            if ( getUser() != null && getUser().length() > 0 )
270            {
271                userName = getUser();
272            }
273
274            String password = repoUrl.getPassword();
275            if ( getPassword() != null && getPassword().length() > 0 )
276            {
277                password = getPassword();
278            }
279
280            if ( userName != null && userName.length() > 0 )
281            {
282                try
283                {
284                    urlSb.append( URLEncoder.encode( userName, "UTF-8" ) );
285                }
286                catch ( UnsupportedEncodingException e )
287                {
288                    // Quite impossible...
289                    // Otherwise throw a RTE, since this method is also used by toString()
290                    e.printStackTrace();
291                }
292
293                if ( password != null && password.length() > 0 )
294                {
295                    urlSb.append( ':' );
296                    try
297                    {
298                        urlSb.append( URLEncoder.encode( password, "UTF-8" ) );
299                    }
300                    catch ( UnsupportedEncodingException e )
301                    {
302                        // Quite impossible...
303                        // Otherwise throw a RTE, since this method is also used by toString()
304                        e.printStackTrace();
305                    }
306                }
307
308                urlSb.append( '@' );
309            }
310        }
311
312        // add host and port information
313        urlSb.append( repoUrl.getHost() );
314        if ( repoUrl.getPort() != null && repoUrl.getPort().length() > 0 )
315        {
316            urlSb.append( ':' ).append( repoUrl.getPort() );
317        }
318
319        // finaly we add the path to the repo on the host
320        urlSb.append( repoUrl.getPath() );
321
322        return urlSb.toString();
323    }
324
325    /**
326     * Parse the protocol from the given url and fill it into the given RepositoryUrl.
327     *
328     * @param repoUrl
329     * @param url
330     * @return the given url with the protocol parts removed
331     */
332    private String parseProtocol( RepositoryUrl repoUrl, String url )
333        throws ScmException
334    {
335        // extract the protocol
336        if ( url.startsWith( PROTOCOL_FILE + PROTOCOL_SEPARATOR ) )
337        {
338            repoUrl.setProtocol( PROTOCOL_FILE );
339        }
340        else if ( url.startsWith( PROTOCOL_HTTPS + PROTOCOL_SEPARATOR ) )
341        {
342            repoUrl.setProtocol( PROTOCOL_HTTPS );
343        }
344        else if ( url.startsWith( PROTOCOL_HTTP + PROTOCOL_SEPARATOR ) )
345        {
346            repoUrl.setProtocol( PROTOCOL_HTTP );
347        }
348        else if ( url.startsWith( PROTOCOL_SSH + PROTOCOL_SEPARATOR ) )
349        {
350            repoUrl.setProtocol( PROTOCOL_SSH );
351        }
352        else if ( url.startsWith( PROTOCOL_GIT + PROTOCOL_SEPARATOR ) )
353        {
354            repoUrl.setProtocol( PROTOCOL_GIT );
355        }
356        else if ( url.startsWith( PROTOCOL_RSYNC + PROTOCOL_SEPARATOR ) )
357        {
358            repoUrl.setProtocol( PROTOCOL_RSYNC );
359        }
360        else
361        {
362            // when no protocol is specified git will pick either ssh:// or git://
363            // depending on whether we work locally or over the network
364            repoUrl.setProtocol( PROTOCOL_NONE );
365            return url;
366        }
367
368        url = url.substring( repoUrl.getProtocol().length() + 3 );
369
370        return url;
371    }
372
373    /**
374     * Parse the user information from the given url and fill
375     * user name and password into the given RepositoryUrl.
376     *
377     * @param repoUrl
378     * @param url
379     * @return the given url with the user parts removed
380     */
381    private String parseUserInfo( RepositoryUrl repoUrl, String url )
382        throws ScmException
383    {
384        // extract user information
385        int indexAt = url.lastIndexOf( '@' );
386        if ( indexAt >= 0 )
387        {
388            String userInfo = url.substring( 0, indexAt );
389            int indexPwdSep = userInfo.indexOf( ':' );
390            if ( indexPwdSep < 0 )
391            {
392                repoUrl.setUserName( userInfo );
393            }
394            else
395            {
396                repoUrl.setUserName( userInfo.substring( 0, indexPwdSep ) );
397                repoUrl.setPassword( userInfo.substring( indexPwdSep + 1 ) );
398            }
399
400            url = url.substring( indexAt + 1 );
401        }
402        return url;
403    }
404
405    /**
406     * Parse server and port from the given url and fill it into the
407     * given RepositoryUrl.
408     *
409     * @param repoUrl
410     * @param url
411     * @return the given url with the server parts removed
412     * @throws ScmException
413     */
414    private String parseHostAndPort( RepositoryUrl repoUrl, String url )
415        throws ScmException
416    {
417
418        repoUrl.setPort( "" );
419        repoUrl.setHost( "" );
420
421        if ( PROTOCOL_FILE.equals( repoUrl.getProtocol() ) )
422        {
423            // a file:// URL doesn't need any further parsing as it cannot contain a port, etc
424            return url;
425        }
426        else
427        {
428
429            Matcher hostAndPortMatcher = HOST_AND_PORT_EXTRACTOR.matcher( url );
430            if ( hostAndPortMatcher.matches() )
431            {
432                if ( hostAndPortMatcher.groupCount() > 1 && hostAndPortMatcher.group( 1 ) != null )
433                {
434                    repoUrl.setHost( hostAndPortMatcher.group( 1 ) );
435                }
436                if ( hostAndPortMatcher.groupCount() > 2 && hostAndPortMatcher.group( 2 ) != null )
437                {
438                    repoUrl.setPort( hostAndPortMatcher.group( 2 ) );
439                }
440
441                StringBuilder computedUrl = new StringBuilder();
442                if ( hostAndPortMatcher.group( hostAndPortMatcher.groupCount() - 1 ) != null )
443                {
444                    computedUrl.append( hostAndPortMatcher.group( hostAndPortMatcher.groupCount() - 1 ) );
445                }
446                if ( hostAndPortMatcher.group( hostAndPortMatcher.groupCount() ) != null )
447                {
448                    computedUrl.append( hostAndPortMatcher.group( hostAndPortMatcher.groupCount() ) );
449                }
450                return computedUrl.toString();
451            }
452            else
453            {
454                // Pattern doesn't match, let's return the original url
455                return url;
456            }
457        }
458    }
459
460
461    /**
462     * {@inheritDoc}
463     */
464    public String getRelativePath( ScmProviderRepository ancestor )
465    {
466        if ( ancestor instanceof GitScmProviderRepository )
467        {
468            GitScmProviderRepository gitAncestor = (GitScmProviderRepository) ancestor;
469
470            //X TODO review!
471            String url = getFetchUrl();
472            String path = url.replaceFirst( gitAncestor.getFetchUrl() + "/", "" );
473
474            if ( !path.equals( url ) )
475            {
476                return path;
477            }
478        }
479        return null;
480    }
481
482    /**
483     * {@inheritDoc}
484     */
485    public String toString()
486    {
487        // yes we really like to check if those are the exact same instance!
488        if ( fetchInfo == pushInfo )
489        {
490            return getUrl( fetchInfo );
491        }
492        return URL_DELIMITER_FETCH + getUrl( fetchInfo ) + URL_DELIMITER_PUSH + getUrl( pushInfo );
493    }
494
495}