Merge branch 'stable-2.15'

* stable-2.15:
  Fix name of commons-net dependency
  Remove drafts related code
  Fix name of commons-net dependency

Change-Id: I31b276e8dc3012fce1d0f4ac9e6da94db462efe2
diff --git a/BUILD b/BUILD
index cc18db3..27a1870 100644
--- a/BUILD
+++ b/BUILD
@@ -16,12 +16,21 @@
     ],
 )
 
+java_library(
+    name = "automerger_test_helpers",
+    srcs = glob(["src/test/java/**/helpers/*.java"]),
+)
+
 junit_tests(
     name = "automerger_tests",
-    srcs = glob(["src/test/java/**/*.java"]),
+    srcs = glob([
+        "src/test/java/**/*Test.java",
+        "src/test/java/**/*IT.java",
+    ]),
     resources = glob(["src/test/resources/**/*"]),
     tags = ["automerger"],
     deps = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":automerger_test_helpers",
         ":automerger__plugin",
         "@commons-net//jar",
     ],
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigDownstreamAction.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigDownstreamAction.java
index 339f966..e681d8a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigDownstreamAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigDownstreamAction.java
@@ -14,8 +14,8 @@
 
 package com.googlesource.gerrit.plugins.automerger;
 
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
index 2ecb88b..f99b3fe 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.automerger;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -164,6 +165,20 @@
   }
 
   /**
+   * Returns a string to append to the end of the merge conflict message for the manifest project.
+   *
+   * @return The message string, or the empty string if nothing is specified.
+   * @throws ConfigInvalidException
+   */
+  public String getManifestConflictMessage() throws ConfigInvalidException {
+    String conflictMessage = getConfig().getString("global", null, "manifestConflictMessage");
+    if (Strings.isNullOrEmpty(conflictMessage)) {
+      conflictMessage = getConflictMessage();
+    }
+    return conflictMessage;
+  }
+
+  /**
    * Get the projects that should be merged for the given pair of branches.
    *
    * @param fromBranch Branch we are merging from.
@@ -188,6 +203,42 @@
   }
 
   /**
+   * Gets the upstream branches of the given branch and project.
+   *
+   * @param toBranch The downstream branch we would merge to.
+   * @param project The project we are merging.
+   * @return The branches upstream of the given branch for the given project.
+   * @throws RestApiException
+   * @throws IOException
+   * @throws ConfigInvalidException
+   */
+  public Set<String> getUpstreamBranches(String toBranch, String project)
+      throws ConfigInvalidException, RestApiException, IOException {
+    if (toBranch == null) {
+      throw new IllegalArgumentException("toBranch cannot be null");
+    }
+    Set<String> upstreamBranches = new HashSet<String>();
+    // List all subsections of automerger, split by :
+    Set<String> subsections = getConfig().getSubsections(pluginName);
+    for (String subsection : subsections) {
+      // Subsections are of the form "fromBranch:toBranch"
+      List<String> branchPair =
+          Splitter.on(BRANCH_DELIMITER).trimResults().omitEmptyStrings().splitToList(subsection);
+      if (branchPair.size() != 2) {
+        throw new ConfigInvalidException("Automerger config branch pair malformed: " + subsection);
+      }
+      if (toBranch.equals(branchPair.get(1))) {
+        // If toBranch matches, check if project is in both their manifests
+        Set<String> projectsInScope = getProjectsInScope(branchPair.get(0), branchPair.get(1));
+        if (projectsInScope.contains(project)) {
+          upstreamBranches.add(branchPair.get(0));
+        }
+      }
+    }
+    return upstreamBranches;
+  }
+
+  /**
    * Gets the downstream branches of the given branch and project.
    *
    * @param fromBranch The branch we are merging from.
@@ -256,6 +307,20 @@
     return user.get().getAccountId();
   }
 
+  /**
+   * Returns overriden manifest config if specified, default if not
+   *
+   * @return The string name of the manifest project.
+   * @throws ConfigInvalidException
+   */
+  public String getManifestProject() throws ConfigInvalidException {
+    String manifestProject = getConfig().getString("global", null, "manifestProject");
+    if (manifestProject == null) {
+      throw new ConfigInvalidException("manifestProject not specified.");
+    }
+    return manifestProject;
+  }
+
   // Returns overriden manifest config if specified, default if not
   private String getManifestFile() throws ConfigInvalidException {
     String manifestFile = getConfig().getString("global", null, "manifestFile");
@@ -265,15 +330,6 @@
     return manifestFile;
   }
 
-  // Returns overriden manifest config if specified, default if not
-  private String getManifestProject() throws ConfigInvalidException {
-    String manifestProject = getConfig().getString("global", null, "manifestProject");
-    if (manifestProject == null) {
-      throw new ConfigInvalidException("manifestProject not specified.");
-    }
-    return manifestProject;
-  }
-
   // Returns contents of manifest file for the given branch pair
   // If manifest does not exist, return empty set.
   private Set<String> getManifestProjects(String fromBranch, String toBranch)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
index d03c744..d2b29c9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
@@ -277,6 +277,18 @@
     }
   }
 
+  public String getOrSetTopic(int sourceId, String topic)
+      throws RestApiException, OrmException, ConfigInvalidException {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      if (isNullOrEmpty(topic)) {
+        topic = "am-" + UUID.randomUUID();
+        log.debug("Setting original change {} topic to {}", sourceId, topic);
+        gApi.changes().id(sourceId).topic(topic);
+      }
+      return topic;
+    }
+  }
+
   /**
    * Creates merges downstream, and votes on the automerge label if we have a failed merge.
    *
@@ -336,7 +348,7 @@
    * @throws InvalidQueryParameterException Throws if we attempt to add an invalid value to query.
    * @throws OrmException Throws if we fail to open the request context
    */
-  public void createDownstreamMerges(MultipleDownstreamMergeInput mdsMergeInput)
+  private void createDownstreamMerges(MultipleDownstreamMergeInput mdsMergeInput)
       throws RestApiException, FailedMergeException, ConfigInvalidException,
           InvalidQueryParameterException, OrmException {
     try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
@@ -365,7 +377,9 @@
                     mdsMergeInput.currentRevision,
                     mdsMergeInput.subject,
                     dsChangeNumber,
-                    mdsMergeInput.dsBranchMap.get(downstreamBranch));
+                    mdsMergeInput.dsBranchMap.get(downstreamBranch),
+                    mdsMergeInput.changeNumber,
+                    downstreamBranch);
                 createDownstreams = false;
               } catch (MergeConflictException e) {
                 failedMergeBranchMap.put(downstreamBranch, e.getMessage());
@@ -398,6 +412,10 @@
       }
 
       if (!failedMergeBranchMap.isEmpty()) {
+        String conflictMessage = config.getConflictMessage();
+        if (mdsMergeInput.project.equals(config.getManifestProject())) {
+          conflictMessage = config.getManifestConflictMessage();
+        }
         throw new FailedMergeException(
             failedMergeBranchMap,
             mdsMergeInput.currentRevision,
@@ -405,7 +423,7 @@
             mdsMergeInput.project,
             mdsMergeInput.changeNumber,
             mdsMergeInput.patchsetNumber,
-            config.getConflictMessage(),
+            conflictMessage,
             mdsMergeInput.topic);
       }
     }
@@ -423,7 +441,7 @@
    * @throws ConfigInvalidException Throws if we fail to read the config
    * @throws OrmException Throws if we fail to open the request context
    */
-  public List<Integer> getExistingMergesOnBranch(
+  private List<Integer> getExistingMergesOnBranch(
       String upstreamRevision, String topic, String downstreamBranch)
       throws RestApiException, InvalidQueryParameterException, OrmException,
           ConfigInvalidException {
@@ -455,7 +473,7 @@
    * @throws InvalidQueryParameterException
    * @throws OrmException
    */
-  public void createSingleDownstreamMerge(SingleDownstreamMergeInput sdsMergeInput)
+  private void createSingleDownstreamMerge(SingleDownstreamMergeInput sdsMergeInput)
       throws RestApiException, ConfigInvalidException, InvalidQueryParameterException,
           OrmException {
     try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
@@ -504,18 +522,6 @@
     }
   }
 
-  public String getOrSetTopic(int sourceId, String topic)
-      throws RestApiException, OrmException, ConfigInvalidException {
-    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
-      if (isNullOrEmpty(topic)) {
-        topic = "am-" + UUID.randomUUID();
-        log.debug("Setting original change {} topic to {}", sourceId, topic);
-        gApi.changes().id(sourceId).topic(topic);
-      }
-      return topic;
-    }
-  }
-
   /**
    * Get the base change ID that the downstream change should be based off of, given the parents.
    *
@@ -641,8 +647,13 @@
   }
 
   private void updateDownstreamMerge(
-      String newParentRevision, String upstreamSubject, Integer sourceNum, boolean doMerge)
-      throws RestApiException {
+      String newParentRevision,
+      String upstreamSubject,
+      Integer sourceNum,
+      boolean doMerge,
+      Integer upstreamChangeNumber,
+      String downstreamBranch)
+      throws RestApiException, InvalidQueryParameterException {
     MergeInput mergeInput = new MergeInput();
     mergeInput.source = newParentRevision;
 
@@ -658,6 +669,10 @@
     }
     mergePatchSetInput.merge = mergeInput;
 
+    mergePatchSetInput.baseChange =
+        getBaseChangeId(
+            getChangeParents(upstreamChangeNumber, newParentRevision), downstreamBranch);
+
     ChangeApi originalChange = gApi.changes().id(sourceNum);
 
     if (originalChange.info().status == ChangeStatus.ABANDONED) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ManifestReader.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ManifestReader.java
index 004c1ca..80d26c0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/ManifestReader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ManifestReader.java
@@ -14,6 +14,13 @@
 
 package com.googlesource.gerrit.plugins.automerger;
 
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.HashSet;
+import java.util.Set;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
@@ -23,14 +30,6 @@
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
 
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import java.io.IOException;
-import java.io.StringReader;
-import java.util.HashSet;
-import java.util.Set;
-
 /** Class to read a repo manifest. */
 public class ManifestReader {
   private static final Logger log = LoggerFactory.getLogger(ManifestReader.class);
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index a1752b1..6e629b3 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -10,6 +10,7 @@
     hostName = https://hostname.example.com
     conflictMessage = Merge conflict found on ${branch}\n\
 " # Example of multiline conflict message"
+    manifestConflictMessage = Conflict found on platform/manifest
     manifestProject = platform/manifest
     manifestFile = default.xml
     alwaysBlankMerge = .*SKIP ME ALWAYS.*
@@ -71,6 +72,12 @@
     conflictMessage = Conflict message ${conflict} found on branch ${branch}
   ```
 
+global.manifestConflictMessage
+: Like conflictMessage, but only applies to the manifestProject.
+
+  Some messages on normal projects don't apply to manifest projects. This
+  provides users the option to have a different message for manifest projects.
+
 global.manifestProject
 : Project to look for a [repo manifest][1] in.
 
diff --git a/src/main/resources/Documentation/rest-api-all-config-downstream.md b/src/main/resources/Documentation/rest-api-all-config-downstream.md
deleted file mode 100644
index 3eee7eb..0000000
--- a/src/main/resources/Documentation/rest-api-all-config-downstream.md
+++ /dev/null
@@ -1,33 +0,0 @@
-automerger config-downstream
-=============================
-
-NAME
-----
-all-config-downstream - Get set of all downstream branches
-
-SYNOPSIS
---------
->     GET /projects/{project-name}/branches/{branch-id}/automerger~all-config-downstream
-
-DESCRIPTION
------------
-Returns a list of branch names that are downstream, including ones more than one
-hop away.
-
-REQUEST
------------
-```
-  GET /projects/{project-name}/branches/{branch-id}/automerger~all-config-downstream HTTP/1.0
-```
-
-RESPONSE
------------
-```
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
-  )]}'
-  [
-    "master", "branch_two"
-  ]
-```
diff --git a/src/main/resources/Documentation/rest-api-automerge-change.md b/src/main/resources/Documentation/rest-api-automerge-change.md
deleted file mode 100644
index fba7f10..0000000
--- a/src/main/resources/Documentation/rest-api-automerge-change.md
+++ /dev/null
@@ -1,37 +0,0 @@
-automerger automerge-change
-=============================
-
-NAME
-----
-automerge-change - Automerge a change downstream
-
-SYNOPSIS
---------
->     POST /projects/{project-name}/automerger~automerge-change
-
-DESCRIPTION
------------
-Returns an HTTP 204 if successful.
-
-OPTIONS
--------
---branch_map
-> A map of downstream branches to their merge value (false means it is skipped)
-
-REQUEST
------------
-```
-  POST /projects/{project-name}/automerger~automerge-change HTTP/1.0
-  Content-Type application/json;charset=UTF-8
-
-  {
-    "master": true,
-    "branch_two": false
-  }
-```
-
-RESPONSE
------------
-```
-  HTTP/1.1 204 No Content
-```
diff --git a/src/main/resources/Documentation/rest-api-config-downstream.md b/src/main/resources/Documentation/rest-api-config-downstream.md
deleted file mode 100644
index a7aecf5..0000000
--- a/src/main/resources/Documentation/rest-api-config-downstream.md
+++ /dev/null
@@ -1,45 +0,0 @@
-automerger config-downstream
-=============================
-
-NAME
-----
-config-downstream - Get the downstream config map
-
-SYNOPSIS
---------
->     POST /projects/{project-name}/automerger~config-downstream
-
-DESCRIPTION
------------
-Returns a map of branches that are one hop downstream to whether or not it
-should be skipped by default.
-
-OPTIONS
--------
-
---subject
-> The subject of the current change
-
-REQUEST
------------
-```
-  POST /projects/{project-name}/automerger~config-downstream HTTP/1.0
-  Content-Type application/json;charset=UTF-8
-
-  {
-    "subject": "DO NOT MERGE i am a test subject"
-  }
-```
-
-RESPONSE
------------
-```
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
-  )]}'
-  {
-    "master": true,
-    "branch_two": false
-  }
-```
diff --git a/src/main/resources/Documentation/rest-api.md b/src/main/resources/Documentation/rest-api.md
new file mode 100644
index 0000000..3b0b5b6
--- /dev/null
+++ b/src/main/resources/Documentation/rest-api.md
@@ -0,0 +1,88 @@
+Automerger - REST API
+============================
+
+This page describes the REST endpoints that are added by the automerger plugin.
+
+Please also take note of the general information on the
+[REST API](https://gerrit-review.googlesource.com/Documentation/rest-api.html).
+
+<a id="automerger-endpoints"> Automerger Endpoints
+-------------------------------------------------
+
+### <a id="config-downstream"> Config Downstream
+POST /projects/[\{project-name\}](https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-name)/automerger~config-downstream
+
+Returns a map of branches that are one hop downstream to whether or not it
+should be skipped by default.
+
+#### Request
+
+```
+  POST /projects/platform/test_data/automerger~config-downstream HTTP/1.0
+  Content-Type application/json;charset=UTF-8
+
+  {
+    "subject": "DO NOT MERGE i am a test subject"
+  }
+```
+
+#### Response
+
+```
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+  )]}'
+  {
+    "master": true,
+    "branch_two": false
+  }
+```
+
+### <a id="all-config-downstream"> All Config Downstream
+GET /projects/[\{project-name\}](https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-name)/branches/[\{branch-id\}](https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#branch-id)/automerger~all-config-downstream
+
+Returns a list of branch names that are downstream, including ones more than one
+hop away.
+
+#### Request
+
+```
+  GET /projects/platform/test_data/branches/test_branch_name/automerger~all-config-downstream HTTP/1.0
+```
+
+#### Response
+
+```
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+  )]}'
+  [
+    "master", "branch_two"
+  ]
+```
+
+### <a id="automerge-change"> Automerge Change
+POST /changes/[\{change-id\}](https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id)/revisions/[\{revision-id\}](https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id)/automerger~automerge-change
+
+Automerges changes based on the given map, with merges being done with the
+strategy `-s ours` if the value in the map is false.
+
+#### Request
+
+```
+  POST /changes/Id3adb33f/revisions/1/automerger~automerge-change HTTP/1.0
+  Content-Type application/json;charset=UTF-8
+
+  {
+    "master": true,
+    "branch_two": false
+  }
+```
+
+#### Response
+
+```
+  HTTP/1.1 204 No Content
+```
\ No newline at end of file
diff --git a/src/main/resources/static/automerger.js b/src/main/resources/static/automerger.js
index d8995d5..1c83a8d 100644
--- a/src/main/resources/static/automerger.js
+++ b/src/main/resources/static/automerger.js
@@ -31,7 +31,7 @@
             branchToCheckbox[branch] = c.label(checkbox, branch);
         });
 
-        //Add checkboxes to box for each downstream branch
+        // Add checkboxes to box for each downstream branch
         var checkboxes = [];
         Object.keys(branchToCheckbox).forEach(function(branch) {
             checkboxes.push(branchToCheckbox[branch])
@@ -45,14 +45,15 @@
     }
 
     function createMergeButton(c, branchToCheckbox) {
-        return c.button('Merge', {onclick: function(){
+        return c.button('Merge', {onclick: function(e){
             var branchMap = {};
             Object.keys(branchToCheckbox).forEach(function(key){
                 branchMap[key] = branchToCheckbox[key].firstChild.checked;
             });
             // gerrit converts to camelcase on the java end
             c.call({'branch_map': branchMap},
-                function(r){ Gerrit.refresh(); });
+                function(r){ c.refresh(); });
+            e.currentTarget.setAttribute("disabled", true);
         }});
     }
 
@@ -88,6 +89,10 @@
         getDownstreamConfigMap();
     }
 
+    if (window.Polymer) {
+        self.deprecated.install();
+    }
+
     self.onAction('revision', 'automerge-change', onAutomergeChange);
-    Gerrit.on('showchange', onShowChange);
-});
\ No newline at end of file
+    self.on('showchange', onShowChange);
+});
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java
index d692e95..0235cd8 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -48,8 +49,8 @@
   private ConfigLoader configLoader;
   @Inject private AllProjectsName allProjectsName;
   @Inject private PluginConfigFactory cfgFactory;
-  @Inject private String canonicalWebUrl;
-  @Inject private Provider<CurrentUser> user;
+  @Inject @CanonicalWebUrl String canonicalGerritWebUrl;
+  @Inject private Provider<CurrentUser> currentUser;
   private Project.NameKey manifestNameKey;
 
   @Test
@@ -146,6 +147,23 @@
   }
 
   @Test
+  public void upstreamBranchesTest() throws Exception {
+    defaultSetup("automerger.config");
+    Set<String> expectedBranches = new HashSet<String>();
+    expectedBranches.add("master");
+    assertThat(configLoader.getUpstreamBranches("ds_two", "platform/some/project"))
+        .isEqualTo(expectedBranches);
+  }
+
+  @Test
+  public void upstreamBranchesTest_nonexistentBranch() throws Exception {
+    defaultSetup("automerger.config");
+    Set<String> expectedBranches = new HashSet<String>();
+    assertThat(configLoader.getUpstreamBranches("master", "platform/some/project"))
+        .isEqualTo(expectedBranches);
+  }
+
+  @Test
   public void downstreamBranchesTest() throws Exception {
     defaultSetup("automerger.config");
     Set<String> expectedBranches = new HashSet<String>();
@@ -204,6 +222,20 @@
   }
 
   @Test
+  public void getDefaultManifestConflictMessageTest() throws Exception {
+    defaultSetup("automerger.config");
+    assertThat(configLoader.getManifestConflictMessage())
+        .isEqualTo("Merge conflict found on ${branch}");
+  }
+
+  @Test
+  public void getMultilineManifestConflictMessageTest() throws Exception {
+    defaultSetup("alternate.config");
+    assertThat(configLoader.getManifestConflictMessage())
+        .isEqualTo("mline1\n" + "mline2\n" + "mline3 ${branch}\n" + "mline4");
+  }
+
+  @Test
   public void getMinAutomergeVoteTest() throws Exception {
     defaultSetup("alternate.config");
     assertThat(configLoader.getMinAutomergeVote()).isEqualTo(-3);
@@ -224,7 +256,7 @@
   @Test
   public void getContextUserIdTest_noContextUser() throws Exception {
     defaultSetup("automerger.config");
-    assertThat(configLoader.getContextUserId()).isEqualTo(user.get().getAccountId());
+    assertThat(configLoader.getContextUserId()).isEqualTo(currentUser.get().getAccountId());
   }
 
   private void setupTestRepo(
@@ -262,6 +294,7 @@
   private void loadConfig(String configFilename) throws Exception {
     pushConfig(configFilename);
     configLoader =
-        new ConfigLoader(gApi, allProjectsName, "automerger", canonicalWebUrl, cfgFactory, user);
+        new ConfigLoader(
+            gApi, allProjectsName, "automerger", canonicalGerritWebUrl, cfgFactory, currentUser);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java
index d04fd1d..57a25e5 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -38,9 +39,10 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.testutil.TestTimeUtil;
+import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.automerger.helpers.ConfigOption;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.sql.Timestamp;
@@ -67,12 +69,13 @@
   public void testExpectedFlow() throws Exception {
     Project.NameKey manifestNameKey = defaultSetup();
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().project().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
     createBranch(new Branch.NameKey(projectName, "ds_two"));
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+    pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
     // After we upload our config, we upload a new patchset to create the downstreams
     amendChange(result.getChangeId());
     result.assertOkStatus();
@@ -148,7 +151,8 @@
     createBranch(new Branch.NameKey(projectName, "bottom"));
     pushDiamondConfig(manifestNameKey.get(), projectName);
     // After we upload our config, we upload a new patchset to create the downstreams
-    PushOneCommit.Result result = createChange("subject", "filename2", "echo Hello", "sometopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic");
     result.assertOkStatus();
     // Check that there are the correct number of changes in the topic
     List<ChangeInfo> changesInTopic =
@@ -205,14 +209,7 @@
 
     // Either bottomChangeInfoA came from left and bottomChangeInfoB came from right, or vice versa
     // We don't know which, so we use the if condition to check
-    String bottomChangeInfoASecondParent =
-        bottomChangeInfoA
-            .revisions
-            .get(bottomChangeInfoA.currentRevision)
-            .commit
-            .parents
-            .get(1)
-            .commit;
+    String bottomChangeInfoASecondParent = getParent(bottomChangeInfoA, 1);
     if (bottomChangeInfoASecondParent.equals(leftChangeInfo.currentRevision)) {
       assertThat(bottomChangeInfoA.subject)
           .isEqualTo(
@@ -251,9 +248,10 @@
 
     // Freeze time so that the merge commit from left->bottom and right->bottom have same SHA
     TestTimeUtil.resetWithClockStep(0, TimeUnit.MILLISECONDS);
-    TestTimeUtil.setClock(new Timestamp(TestTimeUtil.START.getMillis()));
+    TestTimeUtil.setClock(new Timestamp(TestTimeUtil.START.toEpochMilli()));
     // After we upload our config, we upload a new patchset to create the downstreams.
-    PushOneCommit.Result result = createChange("subject", "filename2", "echo Hello", "sometopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic");
     TestTimeUtil.useSystemTime();
     result.assertOkStatus();
 
@@ -310,16 +308,17 @@
   public void testChangeStack() throws Exception {
     Project.NameKey manifestNameKey = defaultSetup();
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().project().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
-    createBranch(new Branch.NameKey(projectName, "ds_two"));
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+    pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one");
     // After we upload our config, we upload a new patchset to create the downstreams
     amendChange(result.getChangeId());
     result.assertOkStatus();
-    PushOneCommit.Result result2 = createChange("subject2", "filename2", "content2", "testtopic");
+    PushOneCommit.Result result2 =
+        createChange(testRepo, "master", "subject2", "filename2", "content2", "testtopic");
     result2.assertOkStatus();
     // Check that there are the correct number of changes in the topic
     List<ChangeInfo> changesInTopic =
@@ -327,64 +326,98 @@
             .query("topic: " + gApi.changes().id(result.getChangeId()).topic())
             .withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT)
             .get();
-    assertThat(changesInTopic).hasSize(6);
+    assertThat(changesInTopic).hasSize(4);
     List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
 
-    // Change A
-    ChangeInfo dsOneChangeInfo = sortedChanges.get(0);
-    assertThat(dsOneChangeInfo.branch).isEqualTo("ds_one");
-    // Change B
-    ChangeInfo dsOneChangeInfo2 = sortedChanges.get(1);
-    assertThat(dsOneChangeInfo2.branch).isEqualTo("ds_one");
-    String dsOneChangeInfo2FirstParentSha =
-        dsOneChangeInfo2
-            .revisions
-            .get(dsOneChangeInfo2.currentRevision)
-            .commit
-            .parents
-            .get(0)
-            .commit;
-    assertThat(dsOneChangeInfo.currentRevision).isEqualTo(dsOneChangeInfo2FirstParentSha);
-
     // Change A'
-    ChangeInfo dsTwoChangeInfo = sortedChanges.get(2);
-    assertThat(dsTwoChangeInfo.branch).isEqualTo("ds_two");
+    ChangeInfo aPrime = sortedChanges.get(0);
+    assertThat(aPrime.branch).isEqualTo("ds_one");
     // Change B'
-    ChangeInfo dsTwoChangeInfo2 = sortedChanges.get(3);
-    assertThat(dsTwoChangeInfo2.branch).isEqualTo("ds_two");
-    String dsTwoChangeInfo2FirstParentSha =
-        dsTwoChangeInfo2
-            .revisions
-            .get(dsTwoChangeInfo2.currentRevision)
-            .commit
-            .parents
-            .get(0)
-            .commit;
-    // Check that first parent of B' is A'
-    assertThat(dsTwoChangeInfo.currentRevision).isEqualTo(dsTwoChangeInfo2FirstParentSha);
+    ChangeInfo bPrime = sortedChanges.get(1);
+    assertThat(bPrime.branch).isEqualTo("ds_one");
+    String bPrimeFirstParent = getParent(bPrime, 0);
+    assertThat(aPrime.currentRevision).isEqualTo(bPrimeFirstParent);
 
-    // Change A''
-    ChangeInfo masterChangeInfo = sortedChanges.get(4);
-    assertThat(masterChangeInfo.branch).isEqualTo("master");
-    // Change B''
-    ChangeInfo masterChangeInfo2 = sortedChanges.get(5);
-    assertThat(masterChangeInfo2.branch).isEqualTo("master");
-    String masterChangeInfo2FirstParentSha =
-        masterChangeInfo2
-            .revisions
-            .get(masterChangeInfo2.currentRevision)
-            .commit
-            .parents
-            .get(0)
-            .commit;
-    // Check that first parent of B'' is A''
-    assertThat(masterChangeInfo.currentRevision).isEqualTo(masterChangeInfo2FirstParentSha);
+    // Change A
+    ChangeInfo a = sortedChanges.get(2);
+    assertThat(a.branch).isEqualTo("master");
+    // Change B
+    ChangeInfo b = sortedChanges.get(3);
+    assertThat(b.branch).isEqualTo("master");
+    String bFirstParent = getParent(b, 0);
+    // Check that first parent of B is A
+    assertThat(bFirstParent).isEqualTo(a.currentRevision);
 
     // Ensure that commit subjects are correct
-    String shortMasterSha = masterChangeInfo.currentRevision.substring(0, 10);
-    assertThat(masterChangeInfo.subject).doesNotContainMatch("automerger");
-    assertThat(dsOneChangeInfo.subject).isEqualTo("[automerger] test commit am: " + shortMasterSha);
-    assertThat(dsTwoChangeInfo.subject).isEqualTo("[automerger] test commit am: " + shortMasterSha);
+    String shortASha = a.currentRevision.substring(0, 10);
+    assertThat(a.subject).doesNotContainMatch("automerger");
+    assertThat(aPrime.subject).isEqualTo("[automerger] test commit am: " + shortASha);
+  }
+
+  @Test
+  public void testChangeStack_rebaseAfterUpload() throws Exception {
+    Project.NameKey manifestNameKey = defaultSetup();
+    // Save initial ref at HEAD
+    ObjectId initial = repo().exactRef("HEAD").getLeaf().getObjectId();
+    // Create initial change
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
+    // Project name is scoped by test, so we need to get it from our initial change
+    String projectName = result.getChange().project().get();
+    createBranch(new Branch.NameKey(projectName, "ds_one"));
+    pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one");
+    // After we upload our config, we upload a new patchset to create the downstreams
+    amendChange(result.getChangeId());
+    result.assertOkStatus();
+
+    // Reset to initial ref to create a sibling
+    testRepo.reset(initial);
+
+    PushOneCommit.Result result2 =
+        createChange(testRepo, "master", "subject2", "filename2", "content2", "testtopic");
+    result2.assertOkStatus();
+    // Check that there are the correct number of changes in the topic
+    List<ChangeInfo> changesInTopic =
+        gApi.changes()
+            .query("topic: " + gApi.changes().id(result.getChangeId()).topic())
+            .withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT)
+            .get();
+    assertThat(changesInTopic).hasSize(4);
+    List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
+
+    // Check the first downstream change A'
+    ChangeInfo aPrime = sortedChanges.get(0);
+    assertThat(aPrime.branch).isEqualTo("ds_one");
+    // Check the second downstream change B'
+    ChangeInfo bPrime = sortedChanges.get(1);
+    assertThat(bPrime.branch).isEqualTo("ds_one");
+    // Check that B' does not have a first parent of A' yet
+    String bPrimeFirstParent = getParent(bPrime, 0);
+    assertThat(aPrime.currentRevision).isNotEqualTo(bPrimeFirstParent);
+
+    // Change A
+    ChangeInfo a = sortedChanges.get(2);
+    assertThat(a.branch).isEqualTo("master");
+    // Change B
+    ChangeInfo b = sortedChanges.get(3);
+    assertThat(b.branch).isEqualTo("master");
+    String masterChangeInfo2FirstParentSha = getParent(b, 0);
+    // Check that first parent of B is not A
+    assertThat(a.currentRevision).isNotEqualTo(masterChangeInfo2FirstParentSha);
+
+    // Rebase B on A
+    RebaseInput rebaseInput = new RebaseInput();
+    rebaseInput.base = a.currentRevision;
+    gApi.changes().id(b.changeId).rebase(rebaseInput);
+
+    // Check that B is now based on A, and B' is now based on A'
+    ChangeInfo bAfterRebase = gApi.changes().id(b.changeId).get();
+    String bAfterRebaseFirstParent = getParent(bAfterRebase, 0);
+    assertThat(bAfterRebaseFirstParent).isEqualTo(a.currentRevision);
+
+    ChangeInfo bPrimeAfterRebase = gApi.changes().id(bPrime.changeId).get();
+    String bPrimeAfterRebaseFirstParent = getParent(bPrimeAfterRebase, 0);
+    assertThat(bPrimeAfterRebaseFirstParent).isEqualTo(aPrime.currentRevision);
   }
 
   @Test
@@ -392,12 +425,13 @@
     Project.NameKey manifestNameKey = defaultSetup();
     // Create initial change
     PushOneCommit.Result result =
-        createChange("DO NOT MERGE subject", "filename", "content", "testtopic");
+        createChange(
+            testRepo, "master", "DO NOT MERGE subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().project().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
     createBranch(new Branch.NameKey(projectName, "ds_two"));
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+    pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
     // After we upload our config, we upload a new patchset to create the downstreams
     amendChange(result.getChangeId(), "DO NOT MERGE subject", "filename", "content");
     result.assertOkStatus();
@@ -455,12 +489,18 @@
     Project.NameKey manifestNameKey = defaultSetup();
     // Create initial change
     PushOneCommit.Result result =
-        createChange("DO NOT MERGE ANYWHERE subject", "filename", "content", "testtopic");
+        createChange(
+            testRepo,
+            "master",
+            "DO NOT MERGE ANYWHERE subject",
+            "filename",
+            "content",
+            "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().project().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
     createBranch(new Branch.NameKey(projectName, "ds_two"));
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+    pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
     // After we upload our config, we upload a new patchset to create the downstreams
     amendChange(result.getChangeId(), "DO NOT MERGE ANYWHERE subject", "filename", "content");
     result.assertOkStatus();
@@ -533,7 +573,7 @@
     merge(ds1Result);
     // Reset to allow our merge conflict to come
     testRepo.reset(initial);
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+    pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
     // After we upload our config, we upload a new change to create the downstreams
     PushOneCommit.Result masterResult =
         pushFactory
@@ -600,7 +640,7 @@
     merge(ds1Result);
     // Reset to allow our merge conflict to come
     testRepo.reset(initial);
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+    pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
 
     // Block Code Review label to test restrictions
     blockLabel("Code-Review", -2, 2, SystemGroupBackend.CHANGE_OWNER, "refs/heads/*", project);
@@ -653,12 +693,13 @@
   public void testTopicEditedListener() throws Exception {
     Project.NameKey manifestNameKey = defaultSetup();
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().project().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
     createBranch(new Branch.NameKey(projectName, "ds_two"));
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+    pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
     // After we upload our config, we upload a new patchset to create the downstreams
     amendChange(result.getChangeId());
     result.assertOkStatus();
@@ -678,12 +719,13 @@
   public void testTopicEditedListener_withQuotes() throws Exception {
     Project.NameKey manifestNameKey = defaultSetup();
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().project().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
     createBranch(new Branch.NameKey(projectName, "ds_two"));
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+    pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
     // After we upload our config, we upload a new patchset to create the downstreams
     amendChange(result.getChangeId());
     result.assertOkStatus();
@@ -705,12 +747,13 @@
   public void testTopicEditedListener_withBraces() throws Exception {
     Project.NameKey manifestNameKey = defaultSetup();
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().project().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
     createBranch(new Branch.NameKey(projectName, "ds_two"));
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
+    pushDefaultConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
     // After we upload our config, we upload a new patchset to create the downstreams
     amendChange(result.getChangeId());
     result.assertOkStatus();
@@ -730,12 +773,13 @@
   public void testTopicEditedListener_branchWithBracesAndQuotes() throws Exception {
     Project.NameKey manifestNameKey = defaultSetup();
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().project().get();
     createBranch(new Branch.NameKey(projectName, "branch{}braces"));
     createBranch(new Branch.NameKey(projectName, "branch\"quotes"));
-    pushConfig(
+    pushDefaultConfig(
         "automerger.config",
         manifestNameKey.get(),
         projectName,
@@ -758,11 +802,12 @@
   public void testTopicEditedListener_emptyTopic() throws Exception {
     Project.NameKey manifestNameKey = defaultSetup();
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().project().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
-    pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", null);
+    pushSimpleConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one");
     // After we upload our config, we upload a new patchset to create the downstreams
     amendChange(result.getChangeId());
     result.assertOkStatus();
@@ -805,10 +850,12 @@
         false,
         AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id),
         true);
-    pushContextUserConfig(manifestNameKey.get(), projectName, contextUserApi.get()._accountId);
+    pushContextUserConfig(
+        manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString());
 
     // After we upload our config, we upload a new patchset to create the downstreams
-    PushOneCommit.Result result = createChange("subject", "filename2", "echo Hello", "sometopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic");
     result.assertOkStatus();
     // Check that there are the correct number of changes in the topic
     List<ChangeInfo> changesInTopic =
@@ -872,10 +919,12 @@
         false,
         AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id),
         false);
-    pushContextUserConfig(manifestNameKey.get(), projectName, contextUserApi.get()._accountId);
+    pushContextUserConfig(
+        manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString());
 
     // After we upload our config, we upload a new patchset to create the downstreams
-    PushOneCommit.Result result = createChange("subject", "filename2", "echo Hello", "sometopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic");
     result.assertOkStatus();
     // Check that there are the correct number of changes in the topic
     List<ChangeInfo> changesInTopic =
@@ -965,10 +1014,12 @@
         false,
         AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id),
         false);
-    pushContextUserConfig(manifestNameKey.get(), projectName, contextUserApi.get()._accountId);
+    pushContextUserConfig(
+        manifestNameKey.get(), projectName, contextUserApi.get()._accountId.toString());
 
     // After we upload our config, we upload a new patchset to create the downstreams
-    PushOneCommit.Result result = createChange("subject", "filename2", "echo Hello", "sometopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename2", "echo Hello", "sometopic");
     result.assertOkStatus();
     // Check that there are the correct number of changes in the topic
     List<ChangeInfo> changesInTopic =
@@ -1009,9 +1060,7 @@
     }
   }
 
-  private void pushConfig(
-      String resourceName, String manifestName, String project, String branch1, String branch2)
-      throws Exception {
+  private void pushConfig(List<ConfigOption> cfgOptions, String resourceName) throws Exception {
     TestRepository<InMemoryRepository> allProjectRepo = cloneProject(allProjects, admin);
     GitUtil.fetch(allProjectRepo, RefNames.REFS_CONFIG + ":config");
     allProjectRepo.reset("config");
@@ -1020,15 +1069,11 @@
 
       Config cfg = new Config();
       cfg.fromText(resourceString);
-      // Update manifest project path to the result of createProject(resourceName), since it is
-      // scoped to the test method
-      cfg.setString("global", null, "manifestProject", manifestName);
-      if (branch1 != null) {
-        cfg.setString("automerger", "master:" + branch1, "setProjects", project);
+
+      for (ConfigOption cfgOption : cfgOptions) {
+        cfg.setString(cfgOption.section, cfgOption.subsection, cfgOption.key, cfgOption.value);
       }
-      if (branch2 != null) {
-        cfg.setString("automerger", "master:" + branch2, "setProjects", project);
-      }
+
       PushOneCommit push =
           pushFactory.create(
               db, admin.getIdent(), allProjectRepo, "Subject", "automerger.config", cfg.toText());
@@ -1036,49 +1081,42 @@
     }
   }
 
+  private void pushSimpleConfig(
+      String resourceName, String manifestName, String project, String branch1) throws Exception {
+    List<ConfigOption> options = new ArrayList<>();
+    options.add(new ConfigOption("global", null, "manifestProject", manifestName));
+    options.add(new ConfigOption("automerger", "master:" + branch1, "setProjects", project));
+    pushConfig(options, resourceName);
+  }
+
+  private void pushDefaultConfig(
+      String resourceName, String manifestName, String project, String branch1, String branch2)
+      throws Exception {
+    List<ConfigOption> options = new ArrayList<>();
+    options.add(new ConfigOption("global", null, "manifestProject", manifestName));
+    options.add(new ConfigOption("automerger", "master:" + branch1, "setProjects", project));
+    options.add(new ConfigOption("automerger", "master:" + branch2, "setProjects", project));
+    pushConfig(options, resourceName);
+  }
+
   private void pushDiamondConfig(String manifestName, String project) throws Exception {
-    TestRepository<InMemoryRepository> allProjectRepo = cloneProject(allProjects, admin);
-    GitUtil.fetch(allProjectRepo, RefNames.REFS_CONFIG + ":config");
-    allProjectRepo.reset("config");
-    try (InputStream in = getClass().getResourceAsStream("diamond.config")) {
-      String resourceString = CharStreams.toString(new InputStreamReader(in, Charsets.UTF_8));
-      Config cfg = new Config();
-      cfg.fromText(resourceString);
-      // Update manifest project path to the result of createProject(resourceName), since it is
-      // scoped to the test method
-      cfg.setString("global", null, "manifestProject", manifestName);
-      cfg.setString("automerger", "master:left", "setProjects", project);
-      cfg.setString("automerger", "master:right", "setProjects", project);
-      cfg.setString("automerger", "left:bottom", "setProjects", project);
-      cfg.setString("automerger", "right:bottom", "setProjects", project);
-      PushOneCommit push =
-          pushFactory.create(
-              db, admin.getIdent(), allProjectRepo, "Subject", "automerger.config", cfg.toText());
-      push.to(RefNames.REFS_CONFIG).assertOkStatus();
-    }
+    List<ConfigOption> options = new ArrayList<>();
+    options.add(new ConfigOption("global", null, "manifestProject", manifestName));
+    options.add(new ConfigOption("automerger", "master:left", "setProjects", project));
+    options.add(new ConfigOption("automerger", "master:right", "setProjects", project));
+    options.add(new ConfigOption("automerger", "left:bottom", "setProjects", project));
+    options.add(new ConfigOption("automerger", "right:bottom", "setProjects", project));
+    pushConfig(options, "diamond.config");
   }
 
-  private void pushContextUserConfig(String manifestName, String project, int contextUserId)
+  private void pushContextUserConfig(String manifestName, String project, String contextUserId)
       throws Exception {
-    TestRepository<InMemoryRepository> allProjectRepo = cloneProject(allProjects, admin);
-    GitUtil.fetch(allProjectRepo, RefNames.REFS_CONFIG + ":config");
-    allProjectRepo.reset("config");
-    try (InputStream in = getClass().getResourceAsStream("context_user.config")) {
-      String resourceString = CharStreams.toString(new InputStreamReader(in, Charsets.UTF_8));
-
-      Config cfg = new Config();
-      cfg.fromText(resourceString);
-      // Update manifest project path to the result of createProject(resourceName), since it is
-      // scoped to the test method
-      cfg.setString("global", null, "manifestProject", manifestName);
-      cfg.setInt("global", null, "contextUserId", contextUserId);
-      cfg.setString("automerger", "master:ds_one", "setProjects", project);
-      cfg.setString("automerger", "ds_one:ds_two", "setProjects", project);
-      PushOneCommit push =
-          pushFactory.create(
-              db, admin.getIdent(), allProjectRepo, "Subject", "automerger.config", cfg.toText());
-      push.to(RefNames.REFS_CONFIG).assertOkStatus();
-    }
+    List<ConfigOption> options = new ArrayList<>();
+    options.add(new ConfigOption("global", null, "manifestProject", manifestName));
+    options.add(new ConfigOption("global", null, "contextUserId", contextUserId));
+    options.add(new ConfigOption("automerger", "master:ds_one", "setProjects", project));
+    options.add(new ConfigOption("automerger", "ds_one:ds_two", "setProjects", project));
+    pushConfig(options, "context_user.config");
   }
 
   private ApprovalInfo getVote(ChangeApi change, String label) throws RestApiException {
@@ -1111,4 +1149,8 @@
         });
     return listCopy;
   }
+
+  public String getParent(ChangeInfo info, int number) {
+    return info.revisions.get(info.currentRevision).commit.parents.get(number).commit;
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/ManifestReaderTest.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/ManifestReaderTest.java
index bd7b79e..faeffa8 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/ManifestReaderTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/ManifestReaderTest.java
@@ -14,17 +14,16 @@
 
 package com.googlesource.gerrit.plugins.automerger;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import com.google.common.base.Charsets;
 import com.google.common.io.CharStreams;
-import org.junit.Before;
-import org.junit.Test;
-
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.util.HashSet;
 import java.util.Set;
-
-import static com.google.common.truth.Truth.assertThat;
+import org.junit.Before;
+import org.junit.Test;
 
 public class ManifestReaderTest {
   private ManifestReader manifestReader;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java
index f89b2cd..368b5cf 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/MergeValidatorIT.java
@@ -65,7 +65,8 @@
   @Test
   public void testNoMissingDownstreamMerges() throws Exception {
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().change().getProject().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
@@ -111,7 +112,8 @@
   @Test
   public void testNoMissingDownstreamMerges_branchWithQuotes() throws Exception {
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().change().getProject().get();
     createBranch(new Branch.NameKey(projectName, "branch\"quotes"));
@@ -125,7 +127,8 @@
   @Test
   public void testNoMissingDownstreamMerges_branchWithBraces() throws Exception {
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().change().getProject().get();
     createBranch(new Branch.NameKey(projectName, "branch{}braces"));
@@ -139,7 +142,8 @@
   @Test
   public void testMultiWordTopic() throws Exception {
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     // Project name is scoped by test, so we need to get it from our initial change
     String projectName = result.getChange().change().getProject().get();
     createBranch(new Branch.NameKey(projectName, "ds_one"));
@@ -155,7 +159,8 @@
   @Test
   public void testMissingDownstreamMerges() throws Exception {
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     pushConfig("automerger.config", result.getChange().project().get(), "ds_one");
     result.assertOkStatus();
     int changeNumber = result.getChange().getId().id;
@@ -171,7 +176,8 @@
   @Test
   public void testMissingDownstreamMerges_custom() throws Exception {
     // Create initial change
-    PushOneCommit.Result result = createChange("subject", "filename", "content", "testtopic");
+    PushOneCommit.Result result =
+        createChange(testRepo, "master", "subject", "filename", "content", "testtopic");
     pushConfig("alternate.config", result.getChange().project().get(), "ds_one");
     result.assertOkStatus();
     int changeNumber = result.getChange().getId().id;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/helpers/ConfigOption.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/helpers/ConfigOption.java
new file mode 100644
index 0000000..838d3ea
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/helpers/ConfigOption.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.automerger.helpers;
+
+public class ConfigOption {
+  public String section;
+  public String subsection;
+  public String key;
+  public String value;
+
+  /**
+   * @param section
+   * @param subsection
+   * @param key
+   * @param value
+   */
+  public ConfigOption(String section, String subsection, String key, String value) {
+    this.section = section;
+    this.subsection = subsection;
+    this.key = key;
+    this.value = value;
+  }
+}
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config b/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config
index 19d69c1..80b7bb6 100644
--- a/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config
+++ b/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config
@@ -18,4 +18,8 @@
 line2\n\
 line3 ${branch}\n\
 line4
+  manifestConflictMessage = mline1\n\
+mline2\n\
+mline3 ${branch}\n\
+mline4
   missingDownstreamsMessage = there is no ${missingDownstreams}
\ No newline at end of file