URIish.java

  1. /*
  2.  * Copyright (C) 2009, Mykola Nikishov <mn@mn.com.ua>
  3.  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
  4.  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
  5.  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
  6.  * Copyright (C) 2013, Robin Stocker <robin@nibor.org>
  7.  * Copyright (C) 2015, Patrick Steinhardt <ps@pks.im> and others
  8.  *
  9.  * This program and the accompanying materials are made available under the
  10.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  11.  * https://www.eclipse.org/org/documents/edl-v10.php.
  12.  *
  13.  * SPDX-License-Identifier: BSD-3-Clause
  14.  */

  15. package org.eclipse.jgit.transport;

  16. import static java.nio.charset.StandardCharsets.UTF_8;

  17. import java.io.ByteArrayOutputStream;
  18. import java.io.File;
  19. import java.io.Serializable;
  20. import java.net.URISyntaxException;
  21. import java.net.URL;
  22. import java.util.BitSet;
  23. import java.util.regex.Matcher;
  24. import java.util.regex.Pattern;

  25. import org.eclipse.jgit.internal.JGitText;
  26. import org.eclipse.jgit.lib.Constants;
  27. import org.eclipse.jgit.util.RawParseUtils;
  28. import org.eclipse.jgit.util.References;
  29. import org.eclipse.jgit.util.StringUtils;

  30. /**
  31.  * This URI like construct used for referencing Git archives over the net, as
  32.  * well as locally stored archives. It is similar to RFC 2396 URI's, but also
  33.  * support SCP and the malformed file://&lt;path&gt; syntax (as opposed to the correct
  34.  * file:&lt;path&gt; syntax.
  35.  */
  36. public class URIish implements Serializable {
  37.     /**
  38.      * Part of a pattern which matches the scheme part (git, http, ...) of an
  39.      * URI. Defines one capturing group containing the scheme without the
  40.      * trailing colon and slashes
  41.      */
  42.     private static final String SCHEME_P = "([a-z][a-z0-9+-]+)://"; //$NON-NLS-1$

  43.     /**
  44.      * Part of a pattern which matches the optional user/password part (e.g.
  45.      * root:pwd@ in git://root:pwd@host.xyz/a.git) of URIs. Defines two
  46.      * capturing groups: the first containing the user and the second containing
  47.      * the password
  48.      */
  49.     private static final String OPT_USER_PWD_P = "(?:([^/:]+)(?::([^\\\\/]+))?@)?"; //$NON-NLS-1$

  50.     /**
  51.      * Part of a pattern which matches the host part of URIs. Defines one
  52.      * capturing group containing the host name.
  53.      */
  54.     private static final String HOST_P = "((?:[^\\\\/:]+)|(?:\\[[0-9a-f:]+\\]))"; //$NON-NLS-1$

  55.     /**
  56.      * Part of a pattern which matches the optional port part of URIs. Defines
  57.      * one capturing group containing the port without the preceding colon.
  58.      */
  59.     private static final String OPT_PORT_P = "(?::(\\d*))?"; //$NON-NLS-1$

  60.     /**
  61.      * Part of a pattern which matches the ~username part (e.g. /~root in
  62.      * git://host.xyz/~root/a.git) of URIs. Defines no capturing group.
  63.      */
  64.     private static final String USER_HOME_P = "(?:/~(?:[^\\\\/]+))"; //$NON-NLS-1$

  65.     /**
  66.      * Part of a pattern which matches the optional drive letter in paths (e.g.
  67.      * D: in file:///D:/a.txt). Defines no capturing group.
  68.      */
  69.     private static final String OPT_DRIVE_LETTER_P = "(?:[A-Za-z]:)?"; //$NON-NLS-1$

  70.     /**
  71.      * Part of a pattern which matches a relative path. Relative paths don't
  72.      * start with slash or drive letters. Defines no capturing group.
  73.      */
  74.     private static final String RELATIVE_PATH_P = "(?:(?:[^\\\\/]+[\\\\/]+)*[^\\\\/]+[\\\\/]*)"; //$NON-NLS-1$

  75.     /**
  76.      * Part of a pattern which matches a relative or absolute path. Defines no
  77.      * capturing group.
  78.      */
  79.     private static final String PATH_P = "(" + OPT_DRIVE_LETTER_P + "[\\\\/]?" //$NON-NLS-1$ //$NON-NLS-2$
  80.             + RELATIVE_PATH_P + ")"; //$NON-NLS-1$

  81.     private static final long serialVersionUID = 1L;

  82.     /**
  83.      * A pattern matching standard URI: </br>
  84.      * <code>scheme "://" user_password? hostname? portnumber? path</code>
  85.      */
  86.     private static final Pattern FULL_URI = Pattern.compile("^" // //$NON-NLS-1$
  87.             + SCHEME_P //
  88.             + "(?:" // start a group containing hostname and all options only //$NON-NLS-1$
  89.                     // availabe when a hostname is there
  90.             + OPT_USER_PWD_P //
  91.             + HOST_P //
  92.             + OPT_PORT_P //
  93.             + "(" // open a group capturing the user-home-dir-part //$NON-NLS-1$
  94.             + (USER_HOME_P + "?") //$NON-NLS-1$
  95.             + "(?:" // start non capturing group for host //$NON-NLS-1$
  96.                     // separator or end of line
  97.             + "[\\\\/])|$" //$NON-NLS-1$
  98.             + ")" // close non capturing group for the host//$NON-NLS-1$
  99.                     // separator or end of line
  100.             + ")?" // close the optional group containing hostname //$NON-NLS-1$
  101.             + "(.+)?" //$NON-NLS-1$
  102.             + "$"); //$NON-NLS-1$

  103.     /**
  104.      * A pattern matching the reference to a local file. This may be an absolute
  105.      * path (maybe even containing windows drive-letters) or a relative path.
  106.      */
  107.     private static final Pattern LOCAL_FILE = Pattern.compile("^" // //$NON-NLS-1$
  108.             + "([\\\\/]?" + PATH_P + ")" // //$NON-NLS-1$ //$NON-NLS-2$
  109.             + "$"); //$NON-NLS-1$

  110.     /**
  111.      * A pattern matching a URI for the scheme 'file' which has only ':/' as
  112.      * separator between scheme and path. Standard file URIs have '://' as
  113.      * separator, but java.io.File.toURI() constructs those URIs.
  114.      */
  115.     private static final Pattern SINGLE_SLASH_FILE_URI = Pattern.compile("^" // //$NON-NLS-1$
  116.             + "(file):([\\\\/](?![\\\\/])" // //$NON-NLS-1$
  117.             + PATH_P //
  118.             + ")$"); //$NON-NLS-1$

  119.     /**
  120.      * A pattern matching a SCP URI's of the form user@host:path/to/repo.git
  121.      */
  122.     private static final Pattern RELATIVE_SCP_URI = Pattern.compile("^" // //$NON-NLS-1$
  123.             + OPT_USER_PWD_P //
  124.             + HOST_P //
  125.             + ":(" // //$NON-NLS-1$
  126.             + ("(?:" + USER_HOME_P + "[\\\\/])?") // //$NON-NLS-1$ //$NON-NLS-2$
  127.             + RELATIVE_PATH_P //
  128.             + ")$"); //$NON-NLS-1$

  129.     /**
  130.      * A pattern matching a SCP URI's of the form user@host:/path/to/repo.git
  131.      */
  132.     private static final Pattern ABSOLUTE_SCP_URI = Pattern.compile("^" // //$NON-NLS-1$
  133.             + OPT_USER_PWD_P //
  134.             + "([^\\\\/:]{2,})" // //$NON-NLS-1$
  135.             + ":(" // //$NON-NLS-1$
  136.             + "[\\\\/]" + RELATIVE_PATH_P // //$NON-NLS-1$
  137.             + ")$"); //$NON-NLS-1$

  138.     private String scheme;

  139.     private String path;

  140.     private String rawPath;

  141.     private String user;

  142.     private String pass;

  143.     private int port = -1;

  144.     private String host;

  145.     /**
  146.      * Parse and construct an {@link org.eclipse.jgit.transport.URIish} from a
  147.      * string
  148.      *
  149.      * @param s
  150.      *            a {@link java.lang.String} object.
  151.      * @throws java.net.URISyntaxException
  152.      */
  153.     public URIish(String s) throws URISyntaxException {
  154.         if (StringUtils.isEmptyOrNull(s)) {
  155.             throw new URISyntaxException("The uri was empty or null", //$NON-NLS-1$
  156.                     JGitText.get().cannotParseGitURIish);
  157.         }
  158.         Matcher matcher = SINGLE_SLASH_FILE_URI.matcher(s);
  159.         if (matcher.matches()) {
  160.             scheme = matcher.group(1);
  161.             rawPath = cleanLeadingSlashes(matcher.group(2), scheme);
  162.             path = unescape(rawPath);
  163.             return;
  164.         }
  165.         matcher = FULL_URI.matcher(s);
  166.         if (matcher.matches()) {
  167.             scheme = matcher.group(1);
  168.             user = unescape(matcher.group(2));
  169.             pass = unescape(matcher.group(3));
  170.             // empty ports are in general allowed, except for URLs like
  171.             // file://D:/path for which it is more desirable to parse with
  172.             // host=null and path=D:/path
  173.             String portString = matcher.group(5);
  174.             if ("file".equals(scheme) && "".equals(portString)) { //$NON-NLS-1$ //$NON-NLS-2$
  175.                 rawPath = cleanLeadingSlashes(
  176.                         n2e(matcher.group(4)) + ":" + portString //$NON-NLS-1$
  177.                                 + n2e(matcher.group(6)) + n2e(matcher.group(7)),
  178.                         scheme);
  179.             } else {
  180.                 host = unescape(matcher.group(4));
  181.                 if (portString != null && portString.length() > 0) {
  182.                     port = Integer.parseInt(portString);
  183.                 }
  184.                 rawPath = cleanLeadingSlashes(
  185.                         n2e(matcher.group(6)) + n2e(matcher.group(7)), scheme);
  186.             }
  187.             path = unescape(rawPath);
  188.             return;
  189.         }
  190.         matcher = RELATIVE_SCP_URI.matcher(s);
  191.         if (matcher.matches()) {
  192.             user = matcher.group(1);
  193.             pass = matcher.group(2);
  194.             host = matcher.group(3);
  195.             rawPath = matcher.group(4);
  196.             path = rawPath;
  197.             return;
  198.         }
  199.         matcher = ABSOLUTE_SCP_URI.matcher(s);
  200.         if (matcher.matches()) {
  201.             user = matcher.group(1);
  202.             pass = matcher.group(2);
  203.             host = matcher.group(3);
  204.             rawPath = matcher.group(4);
  205.             path = rawPath;
  206.             return;
  207.         }
  208.         matcher = LOCAL_FILE.matcher(s);
  209.         if (matcher.matches()) {
  210.             rawPath = matcher.group(1);
  211.             path = rawPath;
  212.             return;
  213.         }
  214.         throw new URISyntaxException(s, JGitText.get().cannotParseGitURIish);
  215.     }

  216.     private static int parseHexByte(byte c1, byte c2) {
  217.             return ((RawParseUtils.parseHexInt4(c1) << 4)
  218.                     | RawParseUtils.parseHexInt4(c2));
  219.     }

  220.     private static String unescape(String s) throws URISyntaxException {
  221.         if (s == null)
  222.             return null;
  223.         if (s.indexOf('%') < 0)
  224.             return s;

  225.         byte[] bytes = s.getBytes(UTF_8);

  226.         byte[] os = new byte[bytes.length];
  227.         int j = 0;
  228.         for (int i = 0; i < bytes.length; ++i) {
  229.             byte c = bytes[i];
  230.             if (c == '%') {
  231.                 if (i + 2 >= bytes.length)
  232.                     throw new URISyntaxException(s, JGitText.get().cannotParseGitURIish);
  233.                 byte c1 = bytes[i + 1];
  234.                 byte c2 = bytes[i + 2];
  235.                 int val;
  236.                 try {
  237.                     val = parseHexByte(c1, c2);
  238.                 } catch (ArrayIndexOutOfBoundsException e) {
  239.                     URISyntaxException use = new URISyntaxException(s,
  240.                             JGitText.get().cannotParseGitURIish);
  241.                     use.initCause(e);
  242.                     throw use;
  243.                 }
  244.                 os[j++] = (byte) val;
  245.                 i += 2;
  246.             } else
  247.                 os[j++] = c;
  248.         }
  249.         return RawParseUtils.decode(os, 0, j);
  250.     }

  251.     private static final BitSet reservedChars = new BitSet(127);

  252.     static {
  253.         for (byte b : Constants.encodeASCII("!*'();:@&=+$,/?#[]")) //$NON-NLS-1$
  254.             reservedChars.set(b);
  255.     }

  256.     /**
  257.      * Escape unprintable characters optionally URI-reserved characters
  258.      *
  259.      * @param s
  260.      *            The Java String to encode (may contain any character)
  261.      * @param escapeReservedChars
  262.      *            true to escape URI reserved characters
  263.      * @param encodeNonAscii
  264.      *            encode any non-ASCII characters
  265.      * @return a URI-encoded string
  266.      */
  267.     private static String escape(String s, boolean escapeReservedChars,
  268.             boolean encodeNonAscii) {
  269.         if (s == null)
  270.             return null;
  271.         ByteArrayOutputStream os = new ByteArrayOutputStream(s.length());
  272.         byte[] bytes = s.getBytes(UTF_8);
  273.         for (byte c : bytes) {
  274.             int b = c & 0xFF;
  275.             if (b <= 32 || (encodeNonAscii && b > 127) || b == '%'
  276.                     || (escapeReservedChars && reservedChars.get(b))) {
  277.                 os.write('%');
  278.                 byte[] tmp = Constants.encodeASCII(String.format("%02x", //$NON-NLS-1$
  279.                         Integer.valueOf(b)));
  280.                 os.write(tmp[0]);
  281.                 os.write(tmp[1]);
  282.             } else {
  283.                 os.write(b);
  284.             }
  285.         }
  286.         byte[] buf = os.toByteArray();
  287.         return RawParseUtils.decode(buf, 0, buf.length);
  288.     }

  289.     private String n2e(String s) {
  290.         return s == null ? "" : s; //$NON-NLS-1$
  291.     }

  292.     // takes care to cut of a leading slash if a windows drive letter or a
  293.     // user-home-dir specifications are
  294.     private String cleanLeadingSlashes(String p, String s) {
  295.         if (p.length() >= 3
  296.                 && p.charAt(0) == '/'
  297.                 && p.charAt(2) == ':'
  298.                 && ((p.charAt(1) >= 'A' && p.charAt(1) <= 'Z')
  299.                         || (p.charAt(1) >= 'a' && p.charAt(1) <= 'z')))
  300.             return p.substring(1);
  301.         else if (s != null && p.length() >= 2 && p.charAt(0) == '/'
  302.                 && p.charAt(1) == '~')
  303.             return p.substring(1);
  304.         else
  305.             return p;
  306.     }

  307.     /**
  308.      * Construct a URIish from a standard URL.
  309.      *
  310.      * @param u
  311.      *            the source URL to convert from.
  312.      */
  313.     public URIish(URL u) {
  314.         scheme = u.getProtocol();
  315.         path = u.getPath();
  316.         path = cleanLeadingSlashes(path, scheme);
  317.         try {
  318.             rawPath = u.toURI().getRawPath();
  319.             rawPath = cleanLeadingSlashes(rawPath, scheme);
  320.         } catch (URISyntaxException e) {
  321.             throw new RuntimeException(e); // Impossible
  322.         }

  323.         final String ui = u.getUserInfo();
  324.         if (ui != null) {
  325.             final int d = ui.indexOf(':');
  326.             user = d < 0 ? ui : ui.substring(0, d);
  327.             pass = d < 0 ? null : ui.substring(d + 1);
  328.         }

  329.         port = u.getPort();
  330.         host = u.getHost();
  331.     }

  332.     /**
  333.      * Create an empty, non-configured URI.
  334.      */
  335.     public URIish() {
  336.         // Configure nothing.
  337.     }

  338.     private URIish(URIish u) {
  339.         this.scheme = u.scheme;
  340.         this.rawPath = u.rawPath;
  341.         this.path = u.path;
  342.         this.user = u.user;
  343.         this.pass = u.pass;
  344.         this.port = u.port;
  345.         this.host = u.host;
  346.     }

  347.     /**
  348.      * Whether this URI references a repository on another system.
  349.      *
  350.      * @return true if this URI references a repository on another system.
  351.      */
  352.     public boolean isRemote() {
  353.         return getHost() != null;
  354.     }

  355.     /**
  356.      * Get host name part.
  357.      *
  358.      * @return host name part or null
  359.      */
  360.     public String getHost() {
  361.         return host;
  362.     }

  363.     /**
  364.      * Return a new URI matching this one, but with a different host.
  365.      *
  366.      * @param n
  367.      *            the new value for host.
  368.      * @return a new URI with the updated value.
  369.      */
  370.     public URIish setHost(String n) {
  371.         final URIish r = new URIish(this);
  372.         r.host = n;
  373.         return r;
  374.     }

  375.     /**
  376.      * Get protocol name
  377.      *
  378.      * @return protocol name or null for local references
  379.      */
  380.     public String getScheme() {
  381.         return scheme;
  382.     }

  383.     /**
  384.      * Return a new URI matching this one, but with a different scheme.
  385.      *
  386.      * @param n
  387.      *            the new value for scheme.
  388.      * @return a new URI with the updated value.
  389.      */
  390.     public URIish setScheme(String n) {
  391.         final URIish r = new URIish(this);
  392.         r.scheme = n;
  393.         return r;
  394.     }

  395.     /**
  396.      * Get path name component
  397.      *
  398.      * @return path name component
  399.      */
  400.     public String getPath() {
  401.         return path;
  402.     }

  403.     /**
  404.      * Get path name component
  405.      *
  406.      * @return path name component
  407.      */
  408.     public String getRawPath() {
  409.         return rawPath;
  410.     }

  411.     /**
  412.      * Return a new URI matching this one, but with a different path.
  413.      *
  414.      * @param n
  415.      *            the new value for path.
  416.      * @return a new URI with the updated value.
  417.      */
  418.     public URIish setPath(String n) {
  419.         final URIish r = new URIish(this);
  420.         r.path = n;
  421.         r.rawPath = n;
  422.         return r;
  423.     }

  424.     /**
  425.      * Return a new URI matching this one, but with a different (raw) path.
  426.      *
  427.      * @param n
  428.      *            the new value for path.
  429.      * @return a new URI with the updated value.
  430.      * @throws java.net.URISyntaxException
  431.      */
  432.     public URIish setRawPath(String n) throws URISyntaxException {
  433.         final URIish r = new URIish(this);
  434.         r.path = unescape(n);
  435.         r.rawPath = n;
  436.         return r;
  437.     }

  438.     /**
  439.      * Get user name requested for transfer
  440.      *
  441.      * @return user name requested for transfer or null
  442.      */
  443.     public String getUser() {
  444.         return user;
  445.     }

  446.     /**
  447.      * Return a new URI matching this one, but with a different user.
  448.      *
  449.      * @param n
  450.      *            the new value for user.
  451.      * @return a new URI with the updated value.
  452.      */
  453.     public URIish setUser(String n) {
  454.         final URIish r = new URIish(this);
  455.         r.user = n;
  456.         return r;
  457.     }

  458.     /**
  459.      * Get password requested for transfer
  460.      *
  461.      * @return password requested for transfer or null
  462.      */
  463.     public String getPass() {
  464.         return pass;
  465.     }

  466.     /**
  467.      * Return a new URI matching this one, but with a different password.
  468.      *
  469.      * @param n
  470.      *            the new value for password.
  471.      * @return a new URI with the updated value.
  472.      */
  473.     public URIish setPass(String n) {
  474.         final URIish r = new URIish(this);
  475.         r.pass = n;
  476.         return r;
  477.     }

  478.     /**
  479.      * Get port number requested for transfer or -1 if not explicit
  480.      *
  481.      * @return port number requested for transfer or -1 if not explicit
  482.      */
  483.     public int getPort() {
  484.         return port;
  485.     }

  486.     /**
  487.      * Return a new URI matching this one, but with a different port.
  488.      *
  489.      * @param n
  490.      *            the new value for port.
  491.      * @return a new URI with the updated value.
  492.      */
  493.     public URIish setPort(int n) {
  494.         final URIish r = new URIish(this);
  495.         r.port = n > 0 ? n : -1;
  496.         return r;
  497.     }

  498.     /** {@inheritDoc} */
  499.     @Override
  500.     public int hashCode() {
  501.         int hc = 0;
  502.         if (getScheme() != null)
  503.             hc = hc * 31 + getScheme().hashCode();
  504.         if (getUser() != null)
  505.             hc = hc * 31 + getUser().hashCode();
  506.         if (getPass() != null)
  507.             hc = hc * 31 + getPass().hashCode();
  508.         if (getHost() != null)
  509.             hc = hc * 31 + getHost().hashCode();
  510.         if (getPort() > 0)
  511.             hc = hc * 31 + getPort();
  512.         if (getPath() != null)
  513.             hc = hc * 31 + getPath().hashCode();
  514.         return hc;
  515.     }

  516.     /** {@inheritDoc} */
  517.     @Override
  518.     public boolean equals(Object obj) {
  519.         if (!(obj instanceof URIish))
  520.             return false;
  521.         final URIish b = (URIish) obj;
  522.         if (!eq(getScheme(), b.getScheme()))
  523.             return false;
  524.         if (!eq(getUser(), b.getUser()))
  525.             return false;
  526.         if (!eq(getPass(), b.getPass()))
  527.             return false;
  528.         if (!eq(getHost(), b.getHost()))
  529.             return false;
  530.         if (getPort() != b.getPort())
  531.             return false;
  532.         if (!eq(getPath(), b.getPath()))
  533.             return false;
  534.         return true;
  535.     }

  536.     private static boolean eq(String a, String b) {
  537.         if (References.isSameObject(a, b)) {
  538.             return true;
  539.         }
  540.         if (StringUtils.isEmptyOrNull(a) && StringUtils.isEmptyOrNull(b))
  541.             return true;
  542.         if (a == null || b == null)
  543.             return false;
  544.         return a.equals(b);
  545.     }

  546.     /**
  547.      * Obtain the string form of the URI, with the password included.
  548.      *
  549.      * @return the URI, including its password field, if any.
  550.      */
  551.     public String toPrivateString() {
  552.         return format(true, false);
  553.     }

  554.     /** {@inheritDoc} */
  555.     @Override
  556.     public String toString() {
  557.         return format(false, false);
  558.     }

  559.     private String format(boolean includePassword, boolean escapeNonAscii) {
  560.         final StringBuilder r = new StringBuilder();
  561.         if (getScheme() != null) {
  562.             r.append(getScheme());
  563.             r.append("://"); //$NON-NLS-1$
  564.         }

  565.         if (getUser() != null) {
  566.             r.append(escape(getUser(), true, escapeNonAscii));
  567.             if (includePassword && getPass() != null) {
  568.                 r.append(':');
  569.                 r.append(escape(getPass(), true, escapeNonAscii));
  570.             }
  571.         }

  572.         if (getHost() != null) {
  573.             if (getUser() != null && getUser().length() > 0)
  574.                 r.append('@');
  575.             r.append(escape(getHost(), false, escapeNonAscii));
  576.             if (getScheme() != null && getPort() > 0) {
  577.                 r.append(':');
  578.                 r.append(getPort());
  579.             }
  580.         }

  581.         if (getPath() != null) {
  582.             if (getScheme() != null) {
  583.                 if (!getPath().startsWith("/") && !getPath().isEmpty()) //$NON-NLS-1$
  584.                     r.append('/');
  585.             } else if (getHost() != null)
  586.                 r.append(':');
  587.             if (getScheme() != null)
  588.                 if (escapeNonAscii)
  589.                     r.append(escape(getPath(), false, escapeNonAscii));
  590.                 else
  591.                     r.append(getRawPath());
  592.             else
  593.                 r.append(getPath());
  594.         }

  595.         return r.toString();
  596.     }

  597.     /**
  598.      * Get the URI as an ASCII string.
  599.      *
  600.      * @return the URI as an ASCII string. Password is not included.
  601.      */
  602.     public String toASCIIString() {
  603.         return format(false, true);
  604.     }

  605.     /**
  606.      * Convert the URI including password, formatted with only ASCII characters
  607.      * such that it will be valid for use over the network.
  608.      *
  609.      * @return the URI including password, formatted with only ASCII characters
  610.      *         such that it will be valid for use over the network.
  611.      */
  612.     public String toPrivateASCIIString() {
  613.         return format(true, true);
  614.     }

  615.     /**
  616.      * Get the "humanish" part of the path. Some examples of a 'humanish' part
  617.      * for a full path:
  618.      * <table summary="path vs humanish path" border="1">
  619.      * <tr>
  620.      * <th>Path</th>
  621.      * <th>Humanish part</th>
  622.      * </tr>
  623.      * <tr>
  624.      * <td><code>/path/to/repo.git</code></td>
  625.      * <td rowspan="4"><code>repo</code></td>
  626.      * </tr>
  627.      * <tr>
  628.      * <td><code>/path/to/repo.git/</code></td>
  629.      * </tr>
  630.      * <tr>
  631.      * <td><code>/path/to/repo/.git</code></td>
  632.      * </tr>
  633.      * <tr>
  634.      * <td><code>/path/to/repo/</code></td>
  635.      * </tr>
  636.      * <tr>
  637.      * <td><code>localhost</code></td>
  638.      * <td><code>ssh://localhost/</code></td>
  639.      * </tr>
  640.      * <tr>
  641.      * <td><code>/path//to</code></td>
  642.      * <td>an empty string</td>
  643.      * </tr>
  644.      * </table>
  645.      *
  646.      * @return the "humanish" part of the path. May be an empty string. Never
  647.      *         {@code null}.
  648.      * @throws java.lang.IllegalArgumentException
  649.      *             if it's impossible to determine a humanish part, or path is
  650.      *             {@code null} or empty
  651.      * @see #getPath
  652.      */
  653.     public String getHumanishName() throws IllegalArgumentException {
  654.         String s = getPath();
  655.         if ("/".equals(s) || "".equals(s)) //$NON-NLS-1$ //$NON-NLS-2$
  656.             s = getHost();
  657.         if (s == null) // $NON-NLS-1$
  658.             throw new IllegalArgumentException();

  659.         String[] elements;
  660.         if ("file".equals(scheme) || LOCAL_FILE.matcher(s).matches()) //$NON-NLS-1$
  661.             elements = s.split("[\\" + File.separatorChar + "/]"); //$NON-NLS-1$ //$NON-NLS-2$
  662.         else
  663.             elements = s.split("/+"); //$NON-NLS-1$
  664.         if (elements.length == 0)
  665.             throw new IllegalArgumentException();
  666.         String result = elements[elements.length - 1];
  667.         if (Constants.DOT_GIT.equals(result))
  668.             result = elements[elements.length - 2];
  669.         else if (result.endsWith(Constants.DOT_GIT_EXT))
  670.             result = result.substring(0, result.length()
  671.                     - Constants.DOT_GIT_EXT.length());
  672.         if (("file".equals(scheme) || LOCAL_FILE.matcher(s) //$NON-NLS-1$
  673.                 .matches())
  674.                 && result.endsWith(Constants.DOT_BUNDLE_EXT)) {
  675.             result = result.substring(0,
  676.                     result.length() - Constants.DOT_BUNDLE_EXT.length());
  677.         }
  678.         return result;
  679.     }

  680. }