Allow opening new changes on existing commits

The %base argument can be used with refs/for/ to identify a specific
revision the server should start to look for new commits at. Any
commits in the range $base..$tip will be opened as a new change,
even if the commit already has another change on a different branch.

Change-Id: Ic2ea9b7c53df3b99d29043cd6a6fd68214f3398f
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 83ce6ce..cbb152c3 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -353,6 +353,20 @@
 set the merge can be reattempted by using `%submit` again.
 
 
+[[base]]
+Selecting Merge Base
+~~~~~~~~~~~~~~~~~~~~
+
+By default new changes are opened only for new unique commits
+that have never before been seen by the Gerrit server. Clients
+may override that behavior and force new changes to be created
+by setting the merge base SHA-1 using the '%base' argument:
+
+====
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%base=$(git rev-parse origin/master)
+====
+
+
 repo upload
 -----------
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 45c6e5a..05ee2d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -1003,6 +1003,10 @@
     RefControl ctl;
     Set<Account.Id> reviewer = Sets.newLinkedHashSet();
     Set<Account.Id> cc = Sets.newLinkedHashSet();
+    RevCommit baseCommit;
+
+    @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
+    ObjectId base;
 
     @Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes")
     String topic;
@@ -1147,13 +1151,31 @@
       reject(cmd, "submit not allowed");
     }
 
+    RevWalk walk = rp.getRevWalk();
+    if (magicBranch.base != null) {
+      try {
+        magicBranch.baseCommit = walk.parseCommit(magicBranch.base);
+      } catch (IncorrectObjectTypeException notCommit) {
+        reject(cmd, "base must be a commit");
+        return;
+      } catch (MissingObjectException e) {
+        reject(cmd, "base not found");
+        return;
+      } catch (IOException e) {
+        log.warn(String.format(
+            "Project %s cannot read %s",
+            project.getName(), magicBranch.base.name()), e);
+        reject(cmd, "internal server error");
+        return;
+      }
+    }
+
     // Validate that the new commits are connected with the target
     // branch.  If they aren't, we want to abort. We do this check by
     // looking to see if we can compute a merge base between the new
     // commits and the target branch head.
     //
     try {
-      final RevWalk walk = rp.getRevWalk();
       final RevCommit tip = walk.parseCommit(magicBranch.cmd.getNewId());
       Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.ctl.getRefName());
       if (targetRef == null || targetRef.getObjectId() == null) {
@@ -1283,10 +1305,14 @@
     try {
       Set<ObjectId> existing = Sets.newHashSet();
       walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId()));
-      markHeadsAsUninteresting(
-          walk,
-          existing,
-          magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+      if (magicBranch.baseCommit != null) {
+        walk.markUninteresting(magicBranch.baseCommit);
+      } else {
+        markHeadsAsUninteresting(
+            walk,
+            existing,
+            magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+      }
 
       List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<Change.Key>();