001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.scm.provider.git.repository;
020
021import java.net.URI;
022import java.net.URISyntaxException;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import org.apache.maven.scm.ScmException;
027import org.apache.maven.scm.provider.ScmProviderRepository;
028import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost;
029
030/**
031 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
032 * @author <a href="mailto:struberg@apache.org">Mark Struberg</a>
033 *
034 */
035public class GitScmProviderRepository extends ScmProviderRepositoryWithHost {
036
037    /**
038     * sequence used to delimit the fetch URL
039     */
040    public static final String URL_DELIMITER_FETCH = "[fetch=]";
041
042    /**
043     * sequence used to delimit the push URL
044     */
045    public static final String URL_DELIMITER_PUSH = "[push=]";
046
047    /**
048     * this trails every protocol
049     */
050    public static final String PROTOCOL_SEPARATOR = "://";
051
052    /**
053     * use local file as transport
054     */
055    public static final String PROTOCOL_FILE = "file";
056
057    /**
058     * use gits internal protocol
059     */
060    public static final String PROTOCOL_GIT = "git";
061
062    /**
063     * use secure shell protocol
064     */
065    public static final String PROTOCOL_SSH = "ssh";
066
067    /**
068     * use the standard port 80 http protocol
069     */
070    public static final String PROTOCOL_HTTP = "http";
071
072    /**
073     * use the standard port 443 https protocol
074     */
075    public static final String PROTOCOL_HTTPS = "https";
076
077    /**
078     * use rsync for retrieving the data
079     * TODO implement!
080     */
081    public static final String PROTOCOL_RSYNC = "rsync";
082
083    private static final Pattern HOST_AND_PORT_EXTRACTOR =
084            Pattern.compile("([^:/\\\\~]*)(?::(\\d*))?(?:([:/\\\\~])(.*))?");
085
086    /**
087     * No special protocol specified. Git will either use git://
088     * or ssh:// depending on whether we work locally or over the network
089     */
090    public static final String PROTOCOL_NONE = "";
091
092    /**
093     * this may either 'git' or 'jgit' depending on the underlying implementation being used
094     */
095    private String provider;
096
097    /**
098     * the URL used to fetch from the upstream repository
099     */
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 used to push to the upstream repository
190     */
191    public String getPushUrl() {
192        return getUrl(pushInfo);
193    }
194
195    /**
196     * Parse the given url string and store all the extracted
197     * information in a {@code RepositoryUrl}
198     *
199     * @param url to parse
200     * @return filled with the information from the given URL
201     * @throws ScmException
202     */
203    private RepositoryUrl parseUrl(String url) throws ScmException {
204        RepositoryUrl repoUrl = new RepositoryUrl();
205
206        url = parseProtocol(repoUrl, url);
207        url = parseUserInfo(repoUrl, url);
208        url = parseHostAndPort(repoUrl, url);
209        // the rest of the url must be the path to the repository on the server
210        repoUrl.setPath(url);
211        return repoUrl;
212    }
213
214    /**
215     * @param repoUrl
216     * @return TODO
217     */
218    private String getUrl(RepositoryUrl repoUrl) {
219        StringBuilder urlSb = new StringBuilder(repoUrl.getProtocol());
220        boolean urlSupportsUserInformation = false;
221
222        if (PROTOCOL_SSH.equals(repoUrl.getProtocol())
223                || PROTOCOL_RSYNC.equals(repoUrl.getProtocol())
224                || PROTOCOL_GIT.equals(repoUrl.getProtocol())
225                || PROTOCOL_HTTP.equals(repoUrl.getProtocol())
226                || PROTOCOL_HTTPS.equals(repoUrl.getProtocol())
227                || PROTOCOL_NONE.equals(repoUrl.getProtocol())) {
228            urlSupportsUserInformation = true;
229        }
230
231        if (repoUrl.getProtocol() != null && repoUrl.getProtocol().length() > 0) {
232            urlSb.append("://");
233        }
234
235        // add user information if given and allowed for the protocol
236        if (urlSupportsUserInformation) {
237            String userName = repoUrl.getUserName();
238            // if specified on the commandline or other configuration, we take this.
239            if (getUser() != null && getUser().length() > 0) {
240                userName = getUser();
241            }
242
243            String password = repoUrl.getPassword();
244            if (getPassword() != null && getPassword().length() > 0) {
245                password = getPassword();
246            }
247
248            if (userName != null && userName.length() > 0) {
249                String userInfo = userName;
250                if (password != null && password.length() > 0) {
251                    userInfo += ":" + password;
252                }
253
254                try {
255                    URI uri = new URI(null, userInfo, "localhost", -1, null, null, null);
256                    urlSb.append(uri.getRawUserInfo());
257                } catch (URISyntaxException e) {
258                    // Quite impossible...
259                    // Otherwise throw a RTE since this method is also used by toString()
260                    e.printStackTrace();
261                }
262
263                urlSb.append('@');
264            }
265        }
266
267        // add host and port information
268        urlSb.append(repoUrl.getHost());
269        if (repoUrl.getPort() != null && repoUrl.getPort().length() > 0) {
270            urlSb.append(':').append(repoUrl.getPort());
271        }
272
273        // finaly we add the path to the repo on the host
274        urlSb.append(repoUrl.getPath());
275
276        return urlSb.toString();
277    }
278
279    /**
280     * Parse the protocol from the given url and fill it into the given RepositoryUrl.
281     *
282     * @param repoUrl
283     * @param url
284     * @return the given url with the protocol parts removed
285     */
286    private String parseProtocol(RepositoryUrl repoUrl, String url) throws ScmException {
287        // extract the protocol
288        if (url.startsWith(PROTOCOL_FILE + PROTOCOL_SEPARATOR)) {
289            repoUrl.setProtocol(PROTOCOL_FILE);
290        } else if (url.startsWith(PROTOCOL_HTTPS + PROTOCOL_SEPARATOR)) {
291            repoUrl.setProtocol(PROTOCOL_HTTPS);
292        } else if (url.startsWith(PROTOCOL_HTTP + PROTOCOL_SEPARATOR)) {
293            repoUrl.setProtocol(PROTOCOL_HTTP);
294        } else if (url.startsWith(PROTOCOL_SSH + PROTOCOL_SEPARATOR)) {
295            repoUrl.setProtocol(PROTOCOL_SSH);
296        } else if (url.startsWith(PROTOCOL_GIT + PROTOCOL_SEPARATOR)) {
297            repoUrl.setProtocol(PROTOCOL_GIT);
298        } else if (url.startsWith(PROTOCOL_RSYNC + PROTOCOL_SEPARATOR)) {
299            repoUrl.setProtocol(PROTOCOL_RSYNC);
300        } else {
301            // when no protocol is specified git will pick either ssh:// or git://
302            // depending on whether we work locally or over the network
303            repoUrl.setProtocol(PROTOCOL_NONE);
304            return url;
305        }
306
307        url = url.substring(repoUrl.getProtocol().length() + 3);
308
309        return url;
310    }
311
312    /**
313     * Parse the user information from the given url and fill
314     * user name and password into the given RepositoryUrl.
315     *
316     * @param repoUrl
317     * @param url
318     * @return the given url with the user parts removed
319     */
320    private String parseUserInfo(RepositoryUrl repoUrl, String url) throws ScmException {
321        if (PROTOCOL_FILE.equals(repoUrl.getProtocol())) {
322            // a file:// URL may contain userinfo according to RFC 8089, but our implementation is broken
323            return url;
324        }
325        // extract user information, broken see SCM-907
326        int indexAt = url.lastIndexOf('@');
327        if (indexAt >= 0) {
328            String userInfo = url.substring(0, indexAt);
329            int indexPwdSep = userInfo.indexOf(':');
330            if (indexPwdSep < 0) {
331                repoUrl.setUserName(userInfo);
332            } else {
333                repoUrl.setUserName(userInfo.substring(0, indexPwdSep));
334                repoUrl.setPassword(userInfo.substring(indexPwdSep + 1));
335            }
336
337            url = url.substring(indexAt + 1);
338        }
339        return url;
340    }
341
342    /**
343     * Parse server and port from the given url and fill it into the
344     * given RepositoryUrl.
345     *
346     * @param repoUrl
347     * @param url
348     * @return the given url with the server parts removed
349     * @throws ScmException
350     */
351    private String parseHostAndPort(RepositoryUrl repoUrl, String url) throws ScmException {
352
353        repoUrl.setPort("");
354        repoUrl.setHost("");
355
356        if (PROTOCOL_FILE.equals(repoUrl.getProtocol())) {
357            // a file:// URL doesn't need any further parsing as it cannot contain a port, etc
358            return url;
359        } else {
360
361            Matcher hostAndPortMatcher = HOST_AND_PORT_EXTRACTOR.matcher(url);
362            if (hostAndPortMatcher.matches()) {
363                if (hostAndPortMatcher.groupCount() > 1 && hostAndPortMatcher.group(1) != null) {
364                    repoUrl.setHost(hostAndPortMatcher.group(1));
365                }
366                if (hostAndPortMatcher.groupCount() > 2 && hostAndPortMatcher.group(2) != null) {
367                    repoUrl.setPort(hostAndPortMatcher.group(2));
368                }
369
370                StringBuilder computedUrl = new StringBuilder();
371                if (hostAndPortMatcher.group(hostAndPortMatcher.groupCount() - 1) != null) {
372                    computedUrl.append(hostAndPortMatcher.group(hostAndPortMatcher.groupCount() - 1));
373                }
374                if (hostAndPortMatcher.group(hostAndPortMatcher.groupCount()) != null) {
375                    computedUrl.append(hostAndPortMatcher.group(hostAndPortMatcher.groupCount()));
376                }
377                return computedUrl.toString();
378            } else {
379                // Pattern doesn't match, let's return the original url
380                return url;
381            }
382        }
383    }
384
385    /**
386     * {@inheritDoc}
387     */
388    public String getRelativePath(ScmProviderRepository ancestor) {
389        if (ancestor instanceof GitScmProviderRepository) {
390            GitScmProviderRepository gitAncestor = (GitScmProviderRepository) ancestor;
391
392            // X TODO review!
393            String url = getFetchUrl();
394            String path = url.replaceFirst(gitAncestor.getFetchUrl() + "/", "");
395
396            if (!path.equals(url)) {
397                return path;
398            }
399        }
400        return null;
401    }
402
403    /**
404     * {@inheritDoc}
405     */
406    public String toString() {
407        // yes we really like to check if those are the exact same instance!
408        if (fetchInfo == pushInfo) {
409            return getUrl(fetchInfo);
410        }
411        return URL_DELIMITER_FETCH + getUrl(fetchInfo) + URL_DELIMITER_PUSH + getUrl(pushInfo);
412    }
413}