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}