View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.scm.provider.git.repository;
20  
21  import java.net.URI;
22  import java.net.URISyntaxException;
23  import java.util.regex.Matcher;
24  import java.util.regex.Pattern;
25  
26  import org.apache.maven.scm.ScmException;
27  import org.apache.maven.scm.provider.ScmProviderRepository;
28  import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost;
29  import org.apache.maven.scm.provider.git.util.GitUtil;
30  
31  /**
32   * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
33   * @author <a href="mailto:struberg@apache.org">Mark Struberg</a>
34   */
35  public class GitScmProviderRepository extends ScmProviderRepositoryWithHost {
36  
37      /**
38       * Sequence used to delimit the fetch URL.
39       */
40      public static final String URL_DELIMITER_FETCH = "[fetch=]";
41  
42      /**
43       * Sequence used to delimit the push URL.
44       */
45      public static final String URL_DELIMITER_PUSH = "[push=]";
46  
47      /**
48       * This trails every protocol.
49       */
50      public static final String PROTOCOL_SEPARATOR = "://";
51  
52      /**
53       * Use local file as transport.
54       */
55      public static final String PROTOCOL_FILE = "file";
56  
57      /**
58       * Use gits internal protocol.
59       */
60      public static final String PROTOCOL_GIT = "git";
61  
62      /**
63       * Use secure shell protocol.
64       */
65      public static final String PROTOCOL_SSH = "ssh";
66  
67      /**
68       * Use the standard port 80 http protocol.
69       */
70      public static final String PROTOCOL_HTTP = "http";
71  
72      /**
73       * Use the standard port 443 https protocol.
74       */
75      public static final String PROTOCOL_HTTPS = "https";
76  
77      /**
78       * Use rsync for retrieving the data
79       * TODO implement!
80       */
81      public static final String PROTOCOL_RSYNC = "rsync";
82  
83      private static final Pattern HOST_AND_PORT_EXTRACTOR =
84              Pattern.compile("([^:/\\\\~]*)(?::(\\d*))?(?:([:/\\\\~])(.*))?");
85  
86      /**
87       * No special protocol specified. Git will either use git://
88       * or ssh:// depending on whether we work locally or over the network.
89       */
90      public static final String PROTOCOL_NONE = "";
91  
92      /**
93       * This may either 'git' or 'jgit' depending on the underlying implementation being used.
94       */
95      private String provider;
96  
97      /**
98       * The URL used to fetch from the upstream repository.
99       */
100     private RepositoryUrl fetchInfo;
101 
102     /**
103      * The URL used to push to the upstream repository.
104      */
105     private RepositoryUrl pushInfo;
106 
107     public GitScmProviderRepository(String url) throws ScmException {
108         if (url == null) {
109             throw new ScmException("url must not be null");
110         }
111 
112         if (url.startsWith(URL_DELIMITER_FETCH)) {
113             String fetch = url.substring(URL_DELIMITER_FETCH.length());
114 
115             int indexPushDelimiter = fetch.indexOf(URL_DELIMITER_PUSH);
116             if (indexPushDelimiter >= 0) {
117                 String push = fetch.substring(indexPushDelimiter + URL_DELIMITER_PUSH.length());
118                 pushInfo = parseUrl(push);
119 
120                 fetch = fetch.substring(0, indexPushDelimiter);
121             }
122 
123             fetchInfo = parseUrl(fetch);
124 
125             if (pushInfo == null) {
126                 pushInfo = fetchInfo;
127             }
128         } else if (url.startsWith(URL_DELIMITER_PUSH)) {
129             String push = url.substring(URL_DELIMITER_PUSH.length());
130 
131             int indexFetchDelimiter = push.indexOf(URL_DELIMITER_FETCH);
132             if (indexFetchDelimiter >= 0) {
133                 String fetch = push.substring(indexFetchDelimiter + URL_DELIMITER_FETCH.length());
134                 fetchInfo = parseUrl(fetch);
135 
136                 push = push.substring(0, indexFetchDelimiter);
137             }
138 
139             pushInfo = parseUrl(push);
140 
141             if (fetchInfo == null) {
142                 fetchInfo = pushInfo;
143             }
144         } else {
145             fetchInfo = pushInfo = parseUrl(url);
146         }
147 
148         // set the default values for backward compatibility from the push url
149         // because it's more likely that the push URL contains 'better' credentials
150         setUser(pushInfo.getUserName());
151         setPassword(pushInfo.getPassword());
152         setHost(pushInfo.getHost());
153         if (pushInfo.getPort() != null && pushInfo.getPort().length() > 0) {
154             setPort(Integer.parseInt(pushInfo.getPort()));
155         }
156     }
157 
158     public GitScmProviderRepository(String url, String user, String password) throws ScmException {
159         this(url);
160 
161         setUser(user);
162 
163         setPassword(password);
164     }
165 
166     /**
167      * @return either 'git' or 'jgit' depending on the underlying implementation being used
168      */
169     public String getProvider() {
170         return provider;
171     }
172 
173     public RepositoryUrl getFetchInfo() {
174         return fetchInfo;
175     }
176 
177     public RepositoryUrl getPushInfo() {
178         return pushInfo;
179     }
180 
181     /**
182      * @return the URL used to fetch from the upstream repository
183      */
184     public String getFetchUrl() {
185         return getUrl(fetchInfo);
186     }
187 
188     /**
189      * @return the URL to fetch from with masked password used for logging purposes only
190      */
191     public String getFetchUrlWithMaskedPassword() {
192         return GitUtil.maskPasswordInUrl(getFetchUrl());
193     }
194 
195     /**
196      * @return the URL used to push to the upstream repository
197      */
198     public String getPushUrl() {
199         return getUrl(pushInfo);
200     }
201 
202     /**
203      * @return the URL to push to with masked password used for logging purposes only
204      */
205     public String getPushUrlWithMaskedPassword() {
206         return GitUtil.maskPasswordInUrl(getPushUrl());
207     }
208 
209     /**
210      * Parse the given url string and store all the extracted
211      * information in a {@code RepositoryUrl}
212      *
213      * @param url to parse
214      * @return filled with the information from the given URL
215      * @throws ScmException
216      */
217     private RepositoryUrl parseUrl(String url) throws ScmException {
218         RepositoryUrl repoUrl = new RepositoryUrl();
219 
220         url = parseProtocol(repoUrl, url);
221         url = parseUserInfo(repoUrl, url);
222         url = parseHostAndPort(repoUrl, url);
223         // the rest of the url must be the path to the repository on the server
224         repoUrl.setPath(url);
225         return repoUrl;
226     }
227 
228     /**
229      * @param repoUrl
230      * @return TODO
231      */
232     private String getUrl(RepositoryUrl repoUrl) {
233         StringBuilder urlSb = new StringBuilder(repoUrl.getProtocol());
234         boolean urlSupportsUserInformation = false;
235 
236         if (PROTOCOL_SSH.equals(repoUrl.getProtocol())
237                 || PROTOCOL_RSYNC.equals(repoUrl.getProtocol())
238                 || PROTOCOL_GIT.equals(repoUrl.getProtocol())
239                 || PROTOCOL_HTTP.equals(repoUrl.getProtocol())
240                 || PROTOCOL_HTTPS.equals(repoUrl.getProtocol())
241                 || PROTOCOL_NONE.equals(repoUrl.getProtocol())) {
242             urlSupportsUserInformation = true;
243         }
244 
245         if (repoUrl.getProtocol() != null && repoUrl.getProtocol().length() > 0) {
246             urlSb.append("://");
247         }
248 
249         // add user information if given and allowed for the protocol
250         if (urlSupportsUserInformation) {
251             String userName = repoUrl.getUserName();
252             // if specified on the commandline or other configuration, we take this.
253             if (getUser() != null && getUser().length() > 0) {
254                 userName = getUser();
255             }
256 
257             String password = repoUrl.getPassword();
258             if (getPassword() != null && getPassword().length() > 0) {
259                 password = getPassword();
260             }
261 
262             if (userName != null && userName.length() > 0) {
263                 String userInfo = userName;
264                 if (password != null && password.length() > 0) {
265                     userInfo += ":" + password;
266                 }
267 
268                 try {
269                     URI uri = new URI(null, userInfo, "localhost", -1, null, null, null);
270                     urlSb.append(uri.getRawUserInfo());
271                 } catch (URISyntaxException e) {
272                     // Quite impossible...
273                     // Otherwise throw a RTE since this method is also used by toString()
274                     e.printStackTrace();
275                 }
276 
277                 urlSb.append('@');
278             }
279         }
280 
281         // add host and port information
282         urlSb.append(repoUrl.getHost());
283         if (repoUrl.getPort() != null && repoUrl.getPort().length() > 0) {
284             urlSb.append(':').append(repoUrl.getPort());
285         }
286 
287         // finaly we add the path to the repo on the host
288         urlSb.append(repoUrl.getPath());
289 
290         return urlSb.toString();
291     }
292 
293     /**
294      * Parse the protocol from the given url and fill it into the given RepositoryUrl.
295      *
296      * @param repoUrl
297      * @param url
298      * @return the given url with the protocol parts removed
299      */
300     private String parseProtocol(RepositoryUrl repoUrl, String url) throws ScmException {
301         // extract the protocol
302         if (url.startsWith(PROTOCOL_FILE + PROTOCOL_SEPARATOR)) {
303             repoUrl.setProtocol(PROTOCOL_FILE);
304         } else if (url.startsWith(PROTOCOL_HTTPS + PROTOCOL_SEPARATOR)) {
305             repoUrl.setProtocol(PROTOCOL_HTTPS);
306         } else if (url.startsWith(PROTOCOL_HTTP + PROTOCOL_SEPARATOR)) {
307             repoUrl.setProtocol(PROTOCOL_HTTP);
308         } else if (url.startsWith(PROTOCOL_SSH + PROTOCOL_SEPARATOR)) {
309             repoUrl.setProtocol(PROTOCOL_SSH);
310         } else if (url.startsWith(PROTOCOL_GIT + PROTOCOL_SEPARATOR)) {
311             repoUrl.setProtocol(PROTOCOL_GIT);
312         } else if (url.startsWith(PROTOCOL_RSYNC + PROTOCOL_SEPARATOR)) {
313             repoUrl.setProtocol(PROTOCOL_RSYNC);
314         } else {
315             // when no protocol is specified git will pick either ssh:// or git://
316             // depending on whether we work locally or over the network
317             repoUrl.setProtocol(PROTOCOL_NONE);
318             return url;
319         }
320 
321         url = url.substring(repoUrl.getProtocol().length() + 3);
322 
323         return url;
324     }
325 
326     /**
327      * Parse the user information from the given url and fill
328      * user name and password into the given RepositoryUrl.
329      *
330      * @param repoUrl
331      * @param url
332      * @return the given url with the user parts removed
333      */
334     private String parseUserInfo(RepositoryUrl repoUrl, String url) throws ScmException {
335         if (PROTOCOL_FILE.equals(repoUrl.getProtocol())) {
336             // a file:// URL may contain userinfo according to RFC 8089, but our implementation is broken
337             return url;
338         }
339         // extract user information, broken see SCM-907
340         int indexAt = url.lastIndexOf('@');
341         if (indexAt >= 0) {
342             String userInfo = url.substring(0, indexAt);
343             int indexPwdSep = userInfo.indexOf(':');
344             if (indexPwdSep < 0) {
345                 repoUrl.setUserName(userInfo);
346             } else {
347                 repoUrl.setUserName(userInfo.substring(0, indexPwdSep));
348                 repoUrl.setPassword(userInfo.substring(indexPwdSep + 1));
349             }
350 
351             url = url.substring(indexAt + 1);
352         }
353         return url;
354     }
355 
356     /**
357      * Parse server and port from the given url and fill it into the
358      * given RepositoryUrl.
359      *
360      * @param repoUrl
361      * @param url
362      * @return the given url with the server parts removed
363      * @throws ScmException
364      */
365     private String parseHostAndPort(RepositoryUrl repoUrl, String url) throws ScmException {
366 
367         repoUrl.setPort("");
368         repoUrl.setHost("");
369 
370         if (PROTOCOL_FILE.equals(repoUrl.getProtocol())) {
371             // a file:// URL doesn't need any further parsing as it cannot contain a port, etc
372             return url;
373         } else {
374 
375             Matcher hostAndPortMatcher = HOST_AND_PORT_EXTRACTOR.matcher(url);
376             if (hostAndPortMatcher.matches()) {
377                 if (hostAndPortMatcher.groupCount() > 1 && hostAndPortMatcher.group(1) != null) {
378                     repoUrl.setHost(hostAndPortMatcher.group(1));
379                 }
380                 if (hostAndPortMatcher.groupCount() > 2 && hostAndPortMatcher.group(2) != null) {
381                     repoUrl.setPort(hostAndPortMatcher.group(2));
382                 }
383 
384                 StringBuilder computedUrl = new StringBuilder();
385                 if (hostAndPortMatcher.group(hostAndPortMatcher.groupCount() - 1) != null) {
386                     computedUrl.append(hostAndPortMatcher.group(hostAndPortMatcher.groupCount() - 1));
387                 }
388                 if (hostAndPortMatcher.group(hostAndPortMatcher.groupCount()) != null) {
389                     computedUrl.append(hostAndPortMatcher.group(hostAndPortMatcher.groupCount()));
390                 }
391                 return computedUrl.toString();
392             } else {
393                 // Pattern doesn't match, let's return the original url
394                 return url;
395             }
396         }
397     }
398 
399     /**
400      * {@inheritDoc}
401      */
402     @Override
403     public String getRelativePath(ScmProviderRepository ancestor) {
404         if (ancestor instanceof GitScmProviderRepository) {
405             GitScmProviderRepository gitAncestor = (GitScmProviderRepository) ancestor;
406 
407             // X TODO review!
408             String url = getFetchUrl();
409             String path = url.replaceFirst(gitAncestor.getFetchUrl() + "/", "");
410 
411             if (!path.equals(url)) {
412                 return path;
413             }
414         }
415         return null;
416     }
417 
418     /**
419      * {@inheritDoc}
420      */
421     @Override
422     public String toString() {
423         // yes we really like to check if those are the exact same instance!
424         if (fetchInfo == pushInfo) {
425             return getFetchUrlWithMaskedPassword();
426         }
427         return URL_DELIMITER_FETCH
428                 + getFetchUrlWithMaskedPassword()
429                 + URL_DELIMITER_PUSH
430                 + getPushUrlWithMaskedPassword();
431     }
432 }