Support regular expressions for ref access rules

This change considers the rights of the most specific ref pattern.
If the ref pattern starts with ^ it is considered to be a regular
expression, otherwise the older glob style or exact match rules
are used.

Change-Id: Ie060d3758e5184a7cedd38883253f60817a04e1b
Portions-by: carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 4427f87..63db123 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -22,6 +22,7 @@
 Apache Commons Codec        <<apache2,Apache License 2.0>>
 Apache Commons DBCP         <<apache2,Apache License 2.0>>
 Apache Commons Http Client  <<apache2,Apache License 2.0>>
+Apache Commons Lang         <<apache2,Apache License 2.0>>
 Apache Commons Logging      <<apache2,Apache License 2.0>>
 Apache Commons Net          <<apache2,Apache License 2.0>>
 Apache Commons Pool         <<apache2,Apache License 2.0>>
@@ -48,6 +49,7 @@
 juniversalchardet           <<mpl1_1,MPL 1.1>>
 AOP Alliance                Public Domain
 JSR 305                     <<jsr305,New-Style BSD>>
+Automaton                   <<automaton,New-Style BSD>>
 -----------------------------------------------------------
 
 Cryptography Notice
@@ -560,6 +562,43 @@
 POSSIBILITY OF SUCH DAMAGE.
 ----
 
+[[automaton]]
+dk.brics.automaton - The BSD License
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* link:http://www.brics.dk/automaton/index.html
+
+----
+Copyright (c) 2007-2009, dk.brics.automaton
+All rights reserved.
+
+http://www.opensource.org/licenses/bsd-license.php
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the JSR305 expert group nor the names of its
+      contributors may be used to endorse or promote products derived from
+      this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+----
+
 [[args4j]]
 args4j - MIT License
 ~~~~~~~~~~~~~~~~~~~~
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
index 06f15c22..3c2c93f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
@@ -144,17 +144,34 @@
     while (refPattern.startsWith("/")) {
       refPattern = refPattern.substring(1);
     }
-    if (!refPattern.startsWith(Constants.R_REFS)) {
-      refPattern = Constants.R_HEADS + refPattern;
-    }
-    if (refPattern.endsWith("/*")) {
-      final String prefix = refPattern.substring(0, refPattern.length() - 2);
-      if (!"refs".equals(prefix) && !Repository.isValidRefName(prefix)) {
+
+    if (refPattern.startsWith(RefRight.REGEX_PREFIX)) {
+      String example = RefControl.shortestExample(refPattern);
+
+      if (!example.startsWith(Constants.R_REFS)) {
+        refPattern = RefRight.REGEX_PREFIX + Constants.R_HEADS
+                + refPattern.substring(RefRight.REGEX_PREFIX.length());
+        example = RefControl.shortestExample(refPattern);
+      }
+
+      if (!Repository.isValidRefName(example)) {
         throw new InvalidNameException();
       }
+
     } else {
-      if (!Repository.isValidRefName(refPattern)) {
-        throw new InvalidNameException();
+      if (!refPattern.startsWith(Constants.R_REFS)) {
+        refPattern = Constants.R_HEADS + refPattern;
+      }
+
+      if (refPattern.endsWith("/*")) {
+        final String prefix = refPattern.substring(0, refPattern.length() - 2);
+        if (!"refs".equals(prefix) && !Repository.isValidRefName(prefix)) {
+          throw new InvalidNameException();
+        }
+      } else {
+        if (!Repository.isValidRefName(refPattern)) {
+          throw new InvalidNameException();
+        }
       }
     }
 
@@ -162,7 +179,7 @@
       refPattern = "-" + refPattern;
     }
 
-    if (!controlForRef(projectControl, refPattern).isOwner()) {
+    if (!projectControl.controlForRef(refPattern).isOwner()) {
       throw new NoSuchRefException(refPattern);
     }
 
@@ -187,11 +204,4 @@
     projectCache.evictAll();
     return projectDetailFactory.create(projectName).call();
   }
-
-  private RefControl controlForRef(ProjectControl p, String ref) {
-    if (ref.endsWith("/*")) {
-      ref = ref.substring(0, ref.length() - 1);
-    }
-    return p.controlForRef(ref);
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
index db179ed..92154c4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
@@ -71,7 +71,7 @@
       if (!projectName.equals(k.getProjectNameKey())) {
         throw new IllegalArgumentException("All keys must be from same project");
       }
-      if (!controlForRef(projectControl, k.getRefPattern()).isOwner()) {
+      if (!projectControl.controlForRef(k.getRefPattern()).isOwner()) {
         throw new NoSuchRefException(k.getRefPattern());
       }
     }
@@ -85,11 +85,4 @@
     projectCache.evictAll();
     return projectDetailFactory.create(projectName).call();
   }
-
-  private RefControl controlForRef(ProjectControl p, String ref) {
-    if (ref.endsWith("/*")) {
-      ref = ref.substring(0, ref.length() - 1);
-    }
-    return p.controlForRef(ref);
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index cdd535b..3ff3892f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -77,7 +76,7 @@
 
     for (final RefRight r : projectState.getInheritedRights()) {
       InheritedRefRight refRight = new InheritedRefRight(
-          r, true, controlForRef(pc, r.getRefPattern()).isOwner());
+          r, true, pc.controlForRef(r.getRefPattern()).isOwner());
       if (!refRights.contains(refRight)) {
         refRights.add(refRight);
         wantGroup(r.getAccountGroupId());
@@ -86,7 +85,7 @@
 
     for (final RefRight r : projectState.getLocalRights()) {
       refRights.add(new InheritedRefRight(
-          r, false, controlForRef(pc, r.getRefPattern()).isOwner()));
+          r, false, pc.controlForRef(r.getRefPattern()).isOwner()));
       wantGroup(r.getAccountGroupId());
     }
 
@@ -144,11 +143,4 @@
       groups.put(groupId, groupCache.get(groupId));
     }
   }
-
-  private RefControl controlForRef(ProjectControl p, String ref) {
-    if (ref.endsWith("/*")) {
-      ref = ref.substring(0, ref.length() - 1);
-    }
-    return p.controlForRef(ref);
-  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
index 0db09e4..ec70051 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
@@ -25,6 +25,9 @@
   /** Pattern that matches all references in a project. */
   public static final String ALL = "refs/*";
 
+  /** Prefix that triggers a regular expression pattern. */
+  public static final String REGEX_PREFIX = "^";
+
   public static class RefPattern extends
       StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index e3ce9a0..db8093c 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -54,6 +54,11 @@
     </dependency>
 
     <dependency>
+      <groupId>commons-lang</groupId>
+      <artifactId>commons-lang</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>commons-net</groupId>
       <artifactId>commons-net</artifactId>
     </dependency>
@@ -133,6 +138,11 @@
       <groupId>com.google.gerrit</groupId>
       <artifactId>juniversalchardet</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>dk.brics.automaton</groupId>
+      <artifactId>automaton</artifactId>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 32fa01c..23e2df8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -34,6 +34,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 
+import dk.brics.automaton.RegExp;
+
+import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -49,6 +52,7 @@
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import java.util.regex.Pattern;
 
 
 /** Manages access control for Git references (aka branches, tags). */
@@ -59,9 +63,17 @@
   private Boolean canForgeAuthor;
   private Boolean canForgeCommitter;
 
-  RefControl(final ProjectControl projectControl, final String refName) {
+  RefControl(final ProjectControl projectControl, String ref) {
+    if (ref.startsWith(RefRight.REGEX_PREFIX)) {
+      ref = shortestExample(ref);
+
+    } else if (ref.endsWith("/*")) {
+      ref = ref.substring(0, ref.length() - 1);
+
+    }
+
     this.projectControl = projectControl;
-    this.refName = refName;
+    this.refName = ref;
   }
 
   public String getRefName() {
@@ -94,7 +106,9 @@
     // calls us to find out if there is ownership of all references in
     // order to determine project level ownership.
     //
-    if (!RefRight.ALL.equals(getRefName()) && getProjectControl().isOwner()) {
+    if (getRefName().equals(
+        RefRight.ALL.substring(0, RefRight.ALL.length() - 1))
+        && getProjectControl().isOwner()) {
       return true;
     }
 
@@ -302,19 +316,95 @@
     return val >= level;
   }
 
-  public static final Comparator<String> DESCENDING_SORT =
+  /**
+   * Order the Ref Pattern by the most specific. This sort is done by:
+   * <ul>
+   * <li>1 - The minor value of Levenshtein string distance between the branch
+   * name and the regex string shortest example. A shorter distance is a more
+   * specific match.
+   * <li>2 - Finites first, infinities after.
+   * <li>3 - Number of transitions.
+   * <li>4 - Length of the expression text.
+   * </ul>
+   *
+   * Levenshtein distance is a measure of the similarity between two strings.
+   * The distance is the number of deletions, insertions, or substitutions
+   * required to transform one string into another.
+   *
+   * For example, if given refs/heads/m* and refs/heads/*, the distances are 5
+   * and 6. It means that refs/heads/m* is more specific because it's closer to
+   * refs/heads/master than refs/heads/*.
+   *
+   * Another example could be refs/heads/* and refs/heads/[a-zA-Z]*, the
+   * distances are both 6. Both are infinite, but refs/heads/[a-zA-Z]* has more
+   * transitions, which after all turns it more specific.
+   */
+  private final Comparator<String> BY_MOST_SPECIFIC_SORT =
       new Comparator<String>() {
+        public int compare(final String pattern1, final String pattern2) {
+          int cmp = distance(pattern2) - distance(pattern1);
+          if (cmp == 0) {
+            boolean p1_finite = finite(pattern1);
+            boolean p2_finite = finite(pattern2);
 
-    @Override
-    public int compare(String a, String b) {
-      int aLength = a.length();
-      int bLength = b.length();
-      if (bLength == aLength) {
-        return a.compareTo(b);
-      }
-      return bLength - aLength;
-    }
-  };
+            if (p1_finite && !p2_finite) {
+              cmp = -1;
+            } else if (!p1_finite && p2_finite) {
+              cmp = 1;
+            } else /* if (f1 == f2) */{
+              cmp = 0;
+            }
+          }
+          if (cmp == 0) {
+            cmp = transitions(pattern1) - transitions(pattern2);
+          }
+          if (cmp == 0) {
+            cmp = pattern2.length() - pattern1.length();
+          }
+          return cmp;
+        }
+
+        private int distance(String pattern) {
+          String example;
+          if (pattern.startsWith(RefRight.REGEX_PREFIX)) {
+            example = shortestExample(pattern);
+
+          } else if (pattern.endsWith("/*")) {
+            example = pattern.substring(0, pattern.length() - 1) + '1';
+
+          } else if (pattern.equals(getRefName())) {
+            return 0;
+
+          } else {
+            return Math.max(pattern.length(), getRefName().length());
+          }
+          return StringUtils.getLevenshteinDistance(example, getRefName());
+        }
+
+        private boolean finite(String pattern) {
+          if (pattern.startsWith(RefRight.REGEX_PREFIX)) {
+            return toRegExp(pattern).toAutomaton().isFinite();
+
+          } else if (pattern.endsWith("/*")) {
+            return false;
+
+          } else {
+            return true;
+          }
+        }
+
+        private int transitions(String pattern) {
+          if (pattern.startsWith(RefRight.REGEX_PREFIX)) {
+            return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
+
+          } else if (pattern.endsWith("/*")) {
+            return pattern.length();
+
+          } else {
+            return pattern.length();
+          }
+        }
+      };
 
   /**
    * Sorts all given rights into a map, ordered by descending length of
@@ -338,10 +428,10 @@
    * @param actionRights
    * @return A sorted map keyed off the ref pattern of all rights.
    */
-  private static SortedMap<String, RefRightsForPattern> sortedRightsByPattern(
+  private SortedMap<String, RefRightsForPattern> sortedRightsByPattern(
       List<RefRight> actionRights) {
     SortedMap<String, RefRightsForPattern> rights =
-      new TreeMap<String, RefRightsForPattern>(DESCENDING_SORT);
+      new TreeMap<String, RefRightsForPattern>(BY_MOST_SPECIFIC_SORT);
     for (RefRight actionRight : actionRights) {
       RefRightsForPattern patternRights =
         rights.get(actionRight.getRefPattern());
@@ -403,6 +493,10 @@
   }
 
   public static boolean matches(String refName, String refPattern) {
+    if (refPattern.startsWith(RefRight.REGEX_PREFIX)) {
+      return Pattern.matches(refPattern, refName);
+    }
+
     if (refPattern.endsWith("/*")) {
       String prefix = refPattern.substring(0, refPattern.length() - 1);
       return refName.startsWith(prefix);
@@ -411,4 +505,21 @@
       return refName.equals(refPattern);
     }
   }
+
+  public static String shortestExample(String pattern) {
+    if (pattern.startsWith(RefRight.REGEX_PREFIX)) {
+      return toRegExp(pattern).toAutomaton().getShortestExample(true);
+    } else if (pattern.endsWith("/*")) {
+      return pattern.substring(0, pattern.length() - 1) + '1';
+    } else {
+      return pattern;
+    }
+  }
+
+  private static RegExp toRegExp(String refPattern) {
+    if (refPattern.startsWith(RefRight.REGEX_PREFIX)) {
+      refPattern = refPattern.substring(1);
+    }
+    return new RegExp(refPattern);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
index 2f06f70..fa94146 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
@@ -19,19 +19,33 @@
 import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.RefRight.RefPattern;
-import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.project.RefControl.RefRightsForPattern;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 
 public class Schema_34 extends SchemaVersion {
+  private static final Comparator<String> DESCENDING_SORT =
+      new Comparator<String>() {
+
+        @Override
+        public int compare(String a, String b) {
+          int aLength = a.length();
+          int bLength = b.length();
+          if (bLength == aLength) {
+            return a.compareTo(b);
+          }
+          return bLength - aLength;
+        }
+      };
+
   @Inject
   Schema_34(Provider<Schema_33> prior) {
     super(prior);
@@ -54,7 +68,7 @@
         ApprovalCategory.Id cat = right.getApprovalCategoryId();
         if (r.get(cat) == null) {
           Map<String, RefRightsForPattern> m =
-            new TreeMap<String, RefRightsForPattern>(RefControl.DESCENDING_SORT);
+            new TreeMap<String, RefRightsForPattern>(DESCENDING_SORT);
           r.put(cat, m);
         }
         if (r.get(cat).get(right.getRefPattern()) == null) {
diff --git a/pom.xml b/pom.xml
index 4e89770..ea62331 100644
--- a/pom.xml
+++ b/pom.xml
@@ -549,6 +549,12 @@
       </dependency>
 
       <dependency>
+        <groupId>commons-lang</groupId>
+        <artifactId>commons-lang</artifactId>
+        <version>2.5</version>
+      </dependency>
+
+      <dependency>
         <groupId>eu.medsea.mimeutil</groupId>
         <artifactId>mime-util</artifactId>
         <version>2.1.3</version>
@@ -725,6 +731,12 @@
         <artifactId>juniversalchardet</artifactId>
         <version>1.0.3</version>
       </dependency>
+
+      <dependency>
+        <groupId>dk.brics.automaton</groupId>
+        <artifactId>automaton</artifactId>
+        <version>1.11.2</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
@@ -758,5 +770,10 @@
       <id>objectweb-repository</id>
       <url>http://maven.objectweb.org/maven2/</url>
     </repository>
+
+    <repository>
+      <id>clojars-repo</id>
+      <url>http://clojars.org/repo</url>
+    </repository>
   </repositories>
 </project>