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