Move plugin config from YAML to git config

Instead of a YAML file stored in a project,
we use the standard plugin config mechanism
and store it in All-Projects refs/meta/config.

Change-Id: I14afffb8f80774fc871f153e321abc4e828974db
diff --git a/.gitignore b/.gitignore
index 21cb77a..fbdceef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@
 .buckversion
 .watchmanconfig
 /eclipse-out
+bin/
diff --git a/BUILD b/BUILD
index a8d88ae..887d6fa 100644
--- a/BUILD
+++ b/BUILD
@@ -13,7 +13,6 @@
     resources = glob(["src/main/resources/**/*"]),
     deps = [
         "@re2j//jar",
-        "@yaml//jar",
     ],
 )
 
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 0396eea..ebb3c56 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -1,16 +1,11 @@
-load("//tools/bzl:maven_jar.bzl", "maven_jar")
+"""External plugin dependencies for the Automerger plugin."""
+load('//tools/bzl:maven_jar.bzl', 'maven_jar')
 
 def external_plugin_deps():
   maven_jar(
-    name = 'yaml',
-    artifact = 'org.yaml:snakeyaml:1.17',
-    sha1 = '7a27ea250c5130b2922b86dea63cbb1cc10a660c',
-  )
-
-  maven_jar(
     name = 'mockito',
-    artifact = 'org.mockito:mockito-all:1.10.19',
-    sha1 = '539df70269cc254a58cccc5d8e43286b4a73bf30',
+    artifact = 'org.mockito:mockito-all:1.9.5',
+    sha1 = '79a8984096fc6591c1e3690e07d41be506356fa5',
   )
 
   maven_jar(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
index f7b2e29..0990ec5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.automerger;
 
+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;
@@ -26,6 +27,7 @@
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -53,12 +55,11 @@
    * @param rev RevisionResource of the change whose page we are clicking the button.
    * @param input A map of branch to whether or not the merge should be "-s ours".
    * @return HTTP 200 on success.
+   * @throws IOException
    * @throws RestApiException
-   * @throws FailedMergeException
    */
   @Override
-  public Object apply(RevisionResource rev, Input input)
-      throws RestApiException, FailedMergeException {
+  public Object apply(RevisionResource rev, Input input) throws IOException, RestApiException {
     Map<String, Boolean> branchMap = input.branchMap;
 
     Change change = rev.getChange();
@@ -79,7 +80,12 @@
 
     log.debug("Multiple downstream merge input: {}", mdsMergeInput.dsBranchMap);
 
-    dsCreator.createMergesAndHandleConflicts(mdsMergeInput);
+    try {
+      dsCreator.createMergesAndHandleConflicts(mdsMergeInput);
+    } catch (ConfigInvalidException e) {
+      throw new ResourceConflictException(
+          "Automerger configuration file is invalid: " + e.getMessage());
+    }
     return Response.none();
   }
 
@@ -101,7 +107,7 @@
       } else {
         desc = desc.setVisible(user.get() instanceof IdentifiedUser);
       }
-    } catch (RestApiException | IOException e) {
+    } catch (RestApiException | IOException | ConfigInvalidException e) {
       log.error("Failed to recreate automerges for {} on {}", project, branch);
       desc = desc.setVisible(false);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java
index 9b21b2c..9d61595 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
 
 import com.google.gerrit.extensions.events.ChangeAbandonedListener;
-import com.google.gerrit.extensions.events.ChangeMergedListener;
 import com.google.gerrit.extensions.events.ChangeRestoredListener;
 import com.google.gerrit.extensions.events.CommentAddedListener;
 import com.google.gerrit.extensions.events.DraftPublishedListener;
@@ -36,7 +35,6 @@
   protected void configure() {
     DynamicSet.bind(binder(), CommentAddedListener.class).to(DownstreamCreator.class);
     DynamicSet.bind(binder(), ChangeAbandonedListener.class).to(DownstreamCreator.class);
-    DynamicSet.bind(binder(), ChangeMergedListener.class).to(DownstreamCreator.class);
     DynamicSet.bind(binder(), ChangeRestoredListener.class).to(DownstreamCreator.class);
     DynamicSet.bind(binder(), DraftPublishedListener.class).to(DownstreamCreator.class);
     DynamicSet.bind(binder(), RevisionCreatedListener.class).to(DownstreamCreator.class);
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 5c9f060..339f966 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigDownstreamAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigDownstreamAction.java
@@ -14,6 +14,7 @@
 
 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.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -24,6 +25,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /** The logic behind auto-filling the branch map, aka the input to AutomergeChangeAction. */
 class ConfigDownstreamAction
@@ -57,13 +59,18 @@
     String branchName = rev.getChange().getDest().getShortName();
     String projectName = rev.getProject().get();
 
-    Set<String> downstreamBranches = config.getDownstreamBranches(branchName, projectName);
-    Map<String, Boolean> downstreamMap = new HashMap<>();
-    for (String downstreamBranch : downstreamBranches) {
-      boolean isSkipMerge = config.isSkipMerge(branchName, downstreamBranch, input.subject);
-      downstreamMap.put(downstreamBranch, !isSkipMerge);
+    try {
+      Set<String> downstreamBranches = config.getDownstreamBranches(branchName, projectName);
+      Map<String, Boolean> downstreamMap = new HashMap<>();
+      for (String downstreamBranch : downstreamBranches) {
+        boolean isSkipMerge = config.isSkipMerge(branchName, downstreamBranch, input.subject);
+        downstreamMap.put(downstreamBranch, !isSkipMerge);
+      }
+      return Response.created(downstreamMap);
+    } catch (ConfigInvalidException e) {
+      throw new ResourceConflictException(
+          "Automerger configuration file is invalid: " + e.getMessage());
     }
-    return Response.created(downstreamMap);
   }
 
   static class Input {
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 54310ee..5d439b6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
@@ -14,109 +14,113 @@
 
 package com.googlesource.gerrit.plugins.automerger;
 
-import com.google.common.base.Charsets;
-import com.google.common.io.CharStreams;
+import com.google.common.base.Joiner;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.re2j.Pattern;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.yaml.snakeyaml.Yaml;
 
-/** Class to read the config and swap it out of memory if the config has changed. */
+/** Class to read the config. */
 @Singleton
 public class ConfigLoader {
   private static final Logger log = LoggerFactory.getLogger(ConfigLoader.class);
-  public final String configProject;
-  public final String configProjectBranch;
-  public final String configFilename;
-  public final List<String> configOptionKeys;
+  private static final String BRANCH_DELIMITER = ":";
 
-  protected GerritApi gApi;
-  private volatile LoadedConfig config;
+  private final GerritApi gApi;
+  private final String pluginName;
+  private final AllProjectsName allProjectsName;
+  private final PluginConfigFactory cfgFactory;
 
   /**
-   * Read static configuration from config_keys.yaml and try to load initial dynamic configuration.
-   *
-   * <p>If loading dynamic configuration fails, logs and treats configuration as empty. Callers can
-   * call {@link loadConfig} to retry.
+   * Class to handle getting information from the config.
    *
    * @param gApi API to access gerrit information.
-   * @throws IOException if reading config_keys.yaml failed
+   * @param allProjectsName The name of the top-level project.
+   * @param pluginName The name of the plugin we are running.
+   * @param cfgFactory Factory to generate the plugin config.
    */
   @Inject
-  public ConfigLoader(GerritApi gApi) throws IOException, RestApiException {
+  public ConfigLoader(
+      GerritApi gApi,
+      AllProjectsName allProjectsName,
+      @PluginName String pluginName,
+      PluginConfigFactory cfgFactory) {
     this.gApi = gApi;
+    this.pluginName = pluginName;
+    this.cfgFactory = cfgFactory;
+    this.allProjectsName = allProjectsName;
+  }
 
-    String configKeysPath = "/config/config_keys.yaml";
-    try (InputStreamReader streamReader =
-        new InputStreamReader(getClass().getResourceAsStream(configKeysPath), Charsets.UTF_8)) {
-
-      String automergerConfigYamlString = CharStreams.toString(streamReader);
-      Map<String, Object> automergerConfig =
-          (Map<String, Object>) (new Yaml().load(automergerConfigYamlString));
-      configProject = (String) automergerConfig.get("config_project");
-      configProjectBranch = (String) automergerConfig.get("config_project_branch");
-      configFilename = (String) automergerConfig.get("config_filename");
-      configOptionKeys = (List<String>) automergerConfig.get("config_option_keys");
-
-      loadConfig();
+  private Config getConfig() throws ConfigInvalidException {
+    try {
+      return cfgFactory.getProjectPluginConfig(allProjectsName, pluginName);
+    } catch (NoSuchProjectException e) {
+      throw new ConfigInvalidException(
+          "Config invalid because " + allProjectsName.get() + " does not exist!");
     }
   }
 
   /**
-   * Swap out the current config for a new, up to date config.
-   *
-   * @throws IOException
-   * @throws RestApiException
-   */
-  public void loadConfig() throws IOException, RestApiException {
-    config =
-        new LoadedConfig(
-            gApi, configProject, configProjectBranch, configFilename, configOptionKeys);
-  }
-
-  /**
-   * Detects whether to skip a change based on the configuration. ( )
+   * Detects whether to skip a change based on the configuration.
    *
    * @param fromBranch Branch we are merging from.
    * @param toBranch Branch we are merging to.
    * @param commitMessage Commit message of the change.
    * @return True if we match blank_merge_regex and merge_all is false, or we match
    *     always_blank_merge_regex
+   * @throws ConfigInvalidException
    */
-  public boolean isSkipMerge(String fromBranch, String toBranch, String commitMessage) {
-    return config.isSkipMerge(fromBranch, toBranch, commitMessage);
+  public boolean isSkipMerge(String fromBranch, String toBranch, String commitMessage)
+      throws ConfigInvalidException {
+    Pattern alwaysBlankMergePattern = getConfigPattern("alwaysBlankMerge");
+    if (alwaysBlankMergePattern.matches(commitMessage)) {
+      return true;
+    }
+
+    Pattern blankMergePattern = getConfigPattern("blankMerge");
+    // If regex matches blank_merge (DO NOT MERGE), skip iff merge_all is false
+    if (blankMergePattern.matches(commitMessage)) {
+      return !getMergeAll(fromBranch, toBranch);
+    }
+    return false;
   }
 
-  /**
-   * Get the merge configuration for a pair of branches.
-   *
-   * @param fromBranch Branch we are merging from.
-   * @param toBranch Branch we are merging to.
-   * @return The configuration for the given input.
-   */
-  public Map<String, Object> getConfig(String fromBranch, String toBranch) {
-    return config.getMergeConfig(fromBranch, toBranch);
+  private Pattern getConfigPattern(String key) throws ConfigInvalidException {
+    String[] patternList = getConfig().getStringList("global", null, key);
+    Set<String> mergeStrings = new HashSet<>(Arrays.asList(patternList));
+    return Pattern.compile(Joiner.on("|").join(mergeStrings), Pattern.DOTALL);
+  }
+
+  private boolean getMergeAll(String fromBranch, String toBranch) throws ConfigInvalidException {
+    return getConfig()
+        .getBoolean("automerger", fromBranch + BRANCH_DELIMITER + toBranch, "mergeAll", false);
   }
 
   /**
    * Returns the name of the automerge label (i.e. the label to vote -1 if we have a merge conflict)
    *
    * @return Returns the name of the automerge label.
+   * @throws ConfigInvalidException
    */
-  public String getAutomergeLabel() {
-    return config.getAutomergeLabel();
+  public String getAutomergeLabel() throws ConfigInvalidException {
+    String automergeLabel = getConfig().getString("global", null, "automergeLabel");
+    return automergeLabel != null ? automergeLabel : "Verified";
   }
 
   /**
@@ -127,29 +131,13 @@
    * @return The projects that are in scope of the given projects.
    * @throws RestApiException
    * @throws IOException
+   * @throws ConfigInvalidException
    */
   public Set<String> getProjectsInScope(String fromBranch, String toBranch)
-      throws RestApiException, IOException {
+      throws RestApiException, IOException, ConfigInvalidException {
     try {
-      Set<String> projectSet = new HashSet<String>();
-
-      Set<String> fromProjectSet = getManifestProjects(fromBranch);
-      projectSet.addAll(fromProjectSet);
-
-      Set<String> toProjectSet = getManifestProjects(fromBranch, toBranch);
-      // Take intersection of project sets, unless one is empty.
-      if (projectSet.isEmpty()) {
-        projectSet = toProjectSet;
-      } else if (!toProjectSet.isEmpty()) {
-        projectSet.retainAll(toProjectSet);
-      }
-
-      // The lower the level a config is applied, the higher priority it has
-      // For example, a project ignored in the global config but added in the branch config will
-      // be added to the final project set, not ignored
-      applyConfig(projectSet, config.getGlobal());
-      applyConfig(projectSet, config.getMergeConfig(fromBranch));
-      applyConfig(projectSet, config.getMergeConfig(fromBranch, toBranch));
+      Set<String> projectSet = getManifestProjects(fromBranch, toBranch);
+      projectSet = applyConfig(fromBranch, toBranch, projectSet);
 
       log.debug("Project set for {} to {} is {}", fromBranch, toBranch, projectSet);
       return projectSet;
@@ -167,20 +155,24 @@
    * @return The branches downstream of the given branch for the given project.
    * @throws RestApiException
    * @throws IOException
+   * @throws ConfigInvalidException
    */
   public Set<String> getDownstreamBranches(String fromBranch, String project)
-      throws RestApiException, IOException {
+      throws RestApiException, IOException, ConfigInvalidException {
     Set<String> downstreamBranches = new HashSet<String>();
-    Map<String, Object> fromBranchConfig = config.getMergeConfig(fromBranch);
-
-    if (fromBranchConfig != null) {
-      for (String key : fromBranchConfig.keySet()) {
-        if (!configOptionKeys.contains(key)) {
-          // If it's not a config option, then the key is the toBranch
-          Set<String> projectsInScope = getProjectsInScope(fromBranch, key);
-          if (projectsInScope.contains(project)) {
-            downstreamBranches.add(key);
-          }
+    // List all subsections of automerger, split by :
+    Set<String> subsections = getConfig().getSubsections(pluginName);
+    for (String subsection : subsections) {
+      // Subsections are of the form "fromBranch:toBranch"
+      String[] branchPair = subsection.split(Pattern.quote(BRANCH_DELIMITER));
+      if (branchPair.length != 2) {
+        throw new ConfigInvalidException("Automerger config branch pair malformed: " + subsection);
+      }
+      if (fromBranch.equals(branchPair[0])) {
+        // If fromBranches match, check if project is in both their manifests
+        Set<String> projectsInScope = getProjectsInScope(branchPair[0], branchPair[1]);
+        if (projectsInScope.contains(project)) {
+          downstreamBranches.add(branchPair[1]);
         }
       }
     }
@@ -188,43 +180,52 @@
   }
 
   // Returns overriden manifest config if specified, default if not
-  private Map<String, String> getManifestInfoFromConfig(Map<String, Object> configMap) {
-    if (configMap.containsKey("manifest")) {
-      return (Map<String, String>) configMap.get("manifest");
+  private String getManifestFile() throws ConfigInvalidException {
+    String manifestFile = getConfig().getString("global", null, "manifestFile");
+    if (manifestFile == null) {
+      throw new ConfigInvalidException("manifestFile not specified.");
     }
-    return config.getDefaultManifestInfo();
+    return manifestFile;
   }
 
-  // Returns contents of manifest file for the given branch.
-  // If manifest does not exist, return empty set.
-  private Set<String> getManifestProjects(String fromBranch) throws RestApiException, IOException {
-    Map<String, Object> fromBranchConfig = config.getMergeConfig(fromBranch);
-    if (fromBranchConfig == null) {
-      return new HashSet<>();
+  // 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.");
     }
-    Map<String, String> manifestProjectInfo = getManifestInfoFromConfig(fromBranchConfig);
-    return getManifestProjectsForBranch(manifestProjectInfo, fromBranch);
+    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)
-      throws RestApiException, IOException {
-    Map<String, Object> toBranchConfig = config.getMergeConfig(fromBranch, toBranch);
-    if (toBranchConfig == null) {
-      return new HashSet<>();
+      throws RestApiException, IOException, ConfigInvalidException {
+    boolean ignoreSourceManifest =
+        getConfig()
+            .getBoolean(
+                "automerger",
+                fromBranch + BRANCH_DELIMITER + toBranch,
+                "ignoreSourceManifest",
+                false);
+
+    Set<String> toProjects =
+        getProjectsInManifest(getManifestProject(), getManifestFile(), toBranch);
+    if (ignoreSourceManifest) {
+      return toProjects;
     }
-    Map<String, String> manifestProjectInfo = getManifestInfoFromConfig(toBranchConfig);
-    return getManifestProjectsForBranch(manifestProjectInfo, toBranch);
+
+    Set<String> fromProjects =
+        getProjectsInManifest(getManifestProject(), getManifestFile(), fromBranch);
+    fromProjects.retainAll(toProjects);
+    return fromProjects;
   }
 
-  private Set<String> getManifestProjectsForBranch(
-      Map<String, String> manifestProjectInfo, String branch) throws RestApiException, IOException {
-    String manifestProject = manifestProjectInfo.get("project");
-    String manifestFile = manifestProjectInfo.get("file");
-    try {
-      BinaryResult manifestConfig =
-          gApi.projects().name(manifestProject).branch(branch).file(manifestFile);
+  private Set<String> getProjectsInManifest(
+      String manifestProject, String manifestFile, String branch)
+      throws RestApiException, IOException {
+    try (BinaryResult manifestConfig =
+        gApi.projects().name(manifestProject).branch(branch).file(manifestFile)) {
       ManifestReader manifestReader = new ManifestReader(branch, manifestConfig.asString());
       return manifestReader.getProjects();
     } catch (ResourceNotFoundException e) {
@@ -233,24 +234,32 @@
     }
   }
 
-  private void applyConfig(Set<String> projects, Map<String, Object> givenConfig) {
-    if (givenConfig == null) {
-      return;
-    }
-    if (givenConfig.containsKey("set_projects")) {
-      List<String> setProjects = (ArrayList<String>) givenConfig.get("set_projects");
+  private Set<String> applyConfig(String fromBranch, String toBranch, Set<String> inputProjects)
+      throws ConfigInvalidException {
+    Set<String> projects = new HashSet<>(inputProjects);
+    List<String> setProjects =
+        Arrays.asList(
+            getConfig()
+                .getStringList(
+                    "automerger", fromBranch + BRANCH_DELIMITER + toBranch, "setProjects"));
+    if (!setProjects.isEmpty()) {
       projects.clear();
       projects.addAll(setProjects);
       // if we set projects we can ignore the rest
-      return;
+      return projects;
     }
-    if (givenConfig.containsKey("add_projects")) {
-      List<String> addProjects = (List<String>) givenConfig.get("add_projects");
-      projects.addAll(addProjects);
-    }
-    if (givenConfig.containsKey("ignore_projects")) {
-      List<String> ignoreProjects = (List<String>) givenConfig.get("ignore_projects");
-      projects.removeAll(ignoreProjects);
-    }
+    List<String> addProjects =
+        Arrays.asList(
+            getConfig()
+                .getStringList(
+                    "automerger", fromBranch + BRANCH_DELIMITER + toBranch, "addProjects"));
+    projects.addAll(addProjects);
+    List<String> ignoreProjects =
+        Arrays.asList(
+            getConfig()
+                .getStringList(
+                    "automerger", fromBranch + BRANCH_DELIMITER + toBranch, "ignoreProjects"));
+    projects.removeAll(ignoreProjects);
+    return projects;
   }
 }
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 a067416..a2ce9fe 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.ChangeAbandonedListener;
-import com.google.gerrit.extensions.events.ChangeMergedListener;
 import com.google.gerrit.extensions.events.ChangeRestoredListener;
 import com.google.gerrit.extensions.events.CommentAddedListener;
 import com.google.gerrit.extensions.events.DraftPublishedListener;
@@ -47,6 +46,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -58,7 +58,6 @@
  */
 public class DownstreamCreator
     implements ChangeAbandonedListener,
-        ChangeMergedListener,
         ChangeRestoredListener,
         CommentAddedListener,
         DraftPublishedListener,
@@ -76,24 +75,6 @@
   }
 
   /**
-   * Updates the config in memory if the config project is updated.
-   *
-   * @param event Event we are listening to.
-   */
-  @Override
-  public void onChangeMerged(ChangeMergedListener.Event event) {
-    ChangeInfo change = event.getChange();
-    try {
-      if (change.project.equals(config.configProject)
-          && change.branch.equals(config.configProjectBranch)) {
-        loadConfig();
-      }
-    } catch (RestApiException | IOException e) {
-      log.error("Failed to reload config at {}", change.id, e);
-    }
-  }
-
-  /**
    * Abandons downstream changes if a change is abandoned.
    *
    * @param event Event we are listening to.
@@ -103,7 +84,11 @@
     ChangeInfo change = event.getChange();
     String revision = event.getRevision().commit.commit;
     log.debug("Detected revision {} abandoned on {}.", revision, change.project);
-    abandonDownstream(change, revision);
+    try {
+      abandonDownstream(change, revision);
+    } catch (ConfigInvalidException e) {
+      log.error("Automerger plugin failed onChangeAbandoned for {}", change.id, e);
+    }
   }
 
   /**
@@ -119,7 +104,7 @@
     Set<String> downstreamBranches;
     try {
       downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
-    } catch (RestApiException | IOException e) {
+    } catch (RestApiException | IOException | ConfigInvalidException e) {
       log.error("Failed to edit downstream topics of {}", change.id, e);
       return;
     }
@@ -161,7 +146,7 @@
     Set<String> downstreamBranches;
     try {
       downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
-    } catch (RestApiException | IOException e) {
+    } catch (RestApiException | IOException | ConfigInvalidException e) {
       log.error("Failed to update downstream votes of {}", change.id, e);
       return;
     }
@@ -184,8 +169,8 @@
             updateVote(downstreamChange, label.getKey(), label.getValue().value.shortValue());
           }
         }
-      } catch (RestApiException e) {
-        log.error("RestApiException when updating downstream votes of {}", change.id, e);
+      } catch (RestApiException | ConfigInvalidException e) {
+        log.error("Exception when updating downstream votes of {}", change.id, e);
       }
     }
   }
@@ -200,8 +185,8 @@
     ChangeInfo change = event.getChange();
     try {
       automergeChanges(change, event.getRevision());
-    } catch (RestApiException | IOException e) {
-      log.error("Failed to edit downstream topics of {}", change.id, e);
+    } catch (RestApiException | IOException | ConfigInvalidException e) {
+      log.error("Automerger plugin failed onChangeRestored for {}", change.id, e);
     }
   }
 
@@ -215,8 +200,8 @@
     ChangeInfo change = event.getChange();
     try {
       automergeChanges(change, event.getRevision());
-    } catch (RestApiException | IOException e) {
-      log.error("Failed to edit downstream topics of {}", change.id, e);
+    } catch (RestApiException | IOException | ConfigInvalidException e) {
+      log.error("Automerger plugin failed onDraftPublished for {}", change.id, e);
     }
   }
 
@@ -230,8 +215,8 @@
     ChangeInfo change = event.getChange();
     try {
       automergeChanges(change, event.getRevision());
-    } catch (RestApiException | IOException e) {
-      log.error("Failed to edit downstream topics of {}", change.id, e);
+    } catch (RestApiException | IOException | ConfigInvalidException e) {
+      log.error("Automerger plugin failed onRevisionCreated for {}", change.id, e);
     }
   }
 
@@ -240,9 +225,10 @@
    *
    * @param mdsMergeInput Input containing the downstream branch map and source change ID.
    * @throws RestApiException Throws if we fail a REST API call.
+   * @throws ConfigInvalidException Throws if we get a malformed configuration
    */
   public void createMergesAndHandleConflicts(MultipleDownstreamMergeInput mdsMergeInput)
-      throws RestApiException {
+      throws RestApiException, ConfigInvalidException {
     ReviewInput reviewInput = new ReviewInput();
     Map<String, Short> labels = new HashMap<String, Short>();
     short vote = 0;
@@ -274,9 +260,10 @@
    * @param mdsMergeInput Input containing the downstream branch map and source change ID.
    * @throws RestApiException Throws if we fail a REST API call.
    * @throws FailedMergeException Throws if we get a merge conflict when merging downstream.
+   * @throws ConfigInvalidException Throws if we get a malformed config file
    */
   public void createDownstreamMerges(MultipleDownstreamMergeInput mdsMergeInput)
-      throws RestApiException, FailedMergeException {
+      throws RestApiException, FailedMergeException, ConfigInvalidException {
     Map<String, String> failedMerges = new HashMap<String, String>();
 
     List<Integer> existingDownstream;
@@ -406,17 +393,8 @@
     gApi.changes().create(downstreamChangeInput);
   }
 
-  private void loadConfig() throws IOException, RestApiException {
-    try {
-      config.loadConfig();
-    } catch (IOException | RestApiException e) {
-      log.error("Config failed to sync!", e);
-      throw e;
-    }
-  }
-
   private void automergeChanges(ChangeInfo change, RevisionInfo revisionInfo)
-      throws RestApiException, IOException {
+      throws RestApiException, IOException, ConfigInvalidException {
     if (revisionInfo.draft != null && revisionInfo.draft) {
       log.debug("Patchset {} is draft change, ignoring.", revisionInfo.commit.commit);
       return;
@@ -456,7 +434,7 @@
     createMergesAndHandleConflicts(mdsMergeInput);
   }
 
-  private void abandonDownstream(ChangeInfo change, String revision) {
+  private void abandonDownstream(ChangeInfo change, String revision) throws ConfigInvalidException {
     try {
       Set<String> downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
       if (downstreamBranches.isEmpty()) {
@@ -477,7 +455,8 @@
     }
   }
 
-  private void updateVote(ChangeInfo change, String label, short vote) throws RestApiException {
+  private void updateVote(ChangeInfo change, String label, short vote)
+      throws RestApiException, ConfigInvalidException {
     if (label.equals(config.getAutomergeLabel())) {
       log.debug("Not updating automerge label, as it blocks when there is a merge conflict.");
       return;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/LoadedConfig.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/LoadedConfig.java
deleted file mode 100644
index 290b2bf..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/LoadedConfig.java
+++ /dev/null
@@ -1,197 +0,0 @@
-// Copyright (C) 2016 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;
-
-import com.google.common.base.Joiner;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.restapi.BinaryResult;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.re2j.Pattern;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.yaml.snakeyaml.Yaml;
-
-/** The loaded configuration stored in memory. */
-public class LoadedConfig {
-  private static final Logger log = LoggerFactory.getLogger(LoadedConfig.class);
-
-  private final Map<String, Object> global;
-  private final Map<String, Map<String, ?>> config;
-  private final Map<String, String> defaultManifestInfo;
-  private final Pattern blankMergePattern;
-  private final Pattern alwaysBlankMergePattern;
-
-  public LoadedConfig() {
-    global = Collections.emptyMap();
-    config = Collections.emptyMap();
-    defaultManifestInfo = Collections.emptyMap();
-    blankMergePattern = null;
-    alwaysBlankMergePattern = null;
-  }
-
-  public LoadedConfig(
-      GerritApi gApi,
-      String configProject,
-      String configProjectBranch,
-      String configFilename,
-      List<String> configOptionKeys)
-      throws IOException, RestApiException {
-    log.info(
-        "Loading config file from project {} on branch {} and filename {}",
-        configProject,
-        configProjectBranch,
-        configFilename);
-    BinaryResult configFile =
-        gApi.projects().name(configProject).branch(configProjectBranch).file(configFilename);
-    String configFileString = configFile.asString();
-    config = (Map<String, Map<String, ?>>) (new Yaml().load(configFileString));
-    global = (Map<String, Object>) config.get("global");
-    defaultManifestInfo = (Map<String, String>) global.get("manifest");
-
-    blankMergePattern = getConfigPattern("blank_merge");
-    alwaysBlankMergePattern = getConfigPattern("always_blank_merge");
-    log.info("Finished syncing automerger config.");
-  }
-
-  /**
-   * Checks to see if we should skip the change.
-   *
-   * <p>If the commit message matches the alwaysBlankMergePattern, always return true. If the commit
-   * message matches the blankMergePattern and merge_all is false for this pair of branches, return
-   * true.
-   *
-   * @param fromBranch Branch we are merging from.
-   * @param toBranch Branch we are merging to.
-   * @param commitMessage Commmit message of the original change.
-   * @return Whether or not to merge with "-s ours".
-   */
-  public boolean isSkipMerge(String fromBranch, String toBranch, String commitMessage) {
-    // If regex matches always_blank_merge (DO NOT MERGE ANYWHERE), skip.
-    if (alwaysBlankMergePattern != null && alwaysBlankMergePattern.matches(commitMessage)) {
-      return true;
-    }
-
-    // If regex matches blank_merge (DO NOT MERGE), skip iff merge_all is false
-    if (blankMergePattern != null && blankMergePattern.matches(commitMessage)) {
-      Map<String, Object> mergePairConfig = getMergeConfig(fromBranch, toBranch);
-      if (mergePairConfig != null) {
-        boolean isMergeAll = (boolean) mergePairConfig.getOrDefault("merge_all", false);
-        return !isMergeAll;
-      }
-      return true;
-    }
-    return false;
-  }
-
-  /**
-   * Gets the merge configuration for this branch.
-   *
-   * @param fromBranch Branch we are merging from.
-   * @return A map of config keys to their values, or a map of "to branches" to a map of config keys
-   *     to their values.
-   */
-  public Map<String, Object> getMergeConfig(String fromBranch) {
-    return getBranches().get(fromBranch);
-  }
-
-  /**
-   * Gets the merge configuration for a pair of branches.
-   *
-   * @param fromBranch Branch we are merging from.
-   * @param toBranch Branch we are merging to.
-   * @return Map of configuration keys to their values.
-   */
-  public Map<String, Object> getMergeConfig(String fromBranch, String toBranch) {
-    Map<String, Object> fromBranchConfig = getBranches().get(fromBranch);
-    if (fromBranchConfig == null) {
-      return Collections.emptyMap();
-    }
-    return (Map<String, Object>) fromBranchConfig.get(toBranch);
-  }
-
-  /**
-   * Gets all the branches and their configuration information.
-   *
-   * @return A map of from branches to their configuration maps.
-   */
-  public Map<String, Map<String, Object>> getBranches() {
-    return (Map<String, Map<String, Object>>)
-        config.getOrDefault("branches", Collections.<String, Map<String, Object>>emptyMap());
-  }
-
-  /**
-   * Gets the global config.
-   *
-   * @return A map of configuration keys to their values.
-   */
-  public Map<String, Object> getGlobal() {
-    return global;
-  }
-
-  /**
-   * Gets the default manifest information.
-   *
-   * @return A map of configuration keys to their default values.
-   */
-  public Map<String, String> getDefaultManifestInfo() {
-    return defaultManifestInfo;
-  }
-
-  /**
-   * Gets the automerge label (i.e. what to vote -1 on when we hit a merge conflict)
-   *
-   * @return The automerge label (by default, the String "Verified").
-   */
-  public String getAutomergeLabel() {
-    return (String) global.getOrDefault("automerge_label", "Verified");
-  }
-
-  /**
-   * Gets the value of a global attribute.
-   *
-   * @param key A configuration key that is defined in the config.
-   * @return The value of the global attribute
-   */
-  public Object getGlobalAttribute(String key) {
-    return global.get(key);
-  }
-
-  /**
-   * Gets the value of a global attribute, or the default value if it cannot be found.
-   *
-   * @param key A configuration key that is defined in the config.
-   * @param def The default value if we cannot find it in the config.
-   * @return The value of the global attribute, or the default value if it cannot be found.
-   */
-  public Object getGlobalAttributeOrDefault(String key, Object def) {
-    return global.getOrDefault(key, def);
-  }
-
-  private Pattern getConfigPattern(String key) {
-    Object patterns = global.get(key);
-    if (patterns != null) {
-      Set<String> mergeStrings = new HashSet<>((List<String>) patterns);
-      return Pattern.compile(Joiner.on("|").join(mergeStrings), Pattern.DOTALL);
-    }
-    return null;
-  }
-}
diff --git a/src/main/resources/config/config_keys.yaml b/src/main/resources/config/config_keys.yaml
deleted file mode 100644
index 6008124..0000000
--- a/src/main/resources/config/config_keys.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-config_project: tools/automerger
-config_project_branch: master
-config_filename: config.yaml
-global_keys:
-- always_blank_merge
-- blank_merge
-- manifest
-config_option_keys:
-- manifest
-- merge_all
-- merge_manifest
-- set_projects
-- ignore_projects
-- add_projects
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderTest.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderTest.java
index 4bf316a..56348db 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderTest.java
@@ -14,34 +14,46 @@
 
 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 com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-
-import java.io.IOException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.PluginConfigFactory;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.util.HashSet;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
 
-import static com.google.common.truth.Truth.assertThat;
-
+@RunWith(MockitoJUnitRunner.class)
 public class ConfigLoaderTest {
-  protected GerritApi gApiMock;
+  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+  private GerritApi gApiMock;
+
   private ConfigLoader configLoader;
-  private String configString;
-  private String manifestString;
-  private String firstDownstreamManifestString;
-  private String secondDownstreamManifestString;
+  private AllProjectsName allProjectsName;
+  @Mock private PluginConfigFactory cfgFactory;
+
+  @Rule public ExpectedException thrown = ExpectedException.none();
 
   @Before
   public void setUp() throws Exception {
-    gApiMock = Mockito.mock(GerritApi.class, Mockito.RETURNS_DEEP_STUBS);
-    mockFile("config.yaml", "tools/automerger", "master", "config.yaml");
+    allProjectsName = new AllProjectsName("All-Projects");
+    mockFile("automerger.config", allProjectsName.get(), RefNames.REFS_CONFIG, "automerger.config");
     mockFile("default.xml", "platform/manifest", "master", "default.xml");
     mockFile("ds_one.xml", "platform/manifest", "ds_one", "default.xml");
     mockFile("ds_two.xml", "platform/manifest", "ds_two", "default.xml");
@@ -58,7 +70,16 @@
   }
 
   private void loadConfig() throws Exception {
-    configLoader = new ConfigLoader(gApiMock);
+    Config cfg = new Config();
+    cfg.fromText(
+        gApiMock
+            .projects()
+            .name(allProjectsName.get())
+            .branch(RefNames.REFS_CONFIG)
+            .file("automerger.config")
+            .asString());
+    Mockito.when(cfgFactory.getProjectPluginConfig(allProjectsName, "automerger")).thenReturn(cfg);
+    configLoader = new ConfigLoader(gApiMock, allProjectsName, "automerger", cfgFactory);
   }
 
   @Test
@@ -81,6 +102,34 @@
   }
 
   @Test
+  public void getProjectsInScope_missingSourceManifest() throws Exception {
+    mockFile("alternate.config", allProjectsName.get(), RefNames.REFS_CONFIG, "automerger.config");
+    Mockito.when(gApiMock.projects().name("platform/manifest").branch("master"))
+        .thenThrow(new ResourceNotFoundException());
+    loadConfig();
+    assertThat(configLoader.getProjectsInScope("master", "ds_one").isEmpty()).isTrue();
+  }
+
+  @Test
+  public void getProjectsInScope_ignoreSourceManifest() throws Exception {
+    mockFile("alternate.config", allProjectsName.get(), RefNames.REFS_CONFIG, "automerger.config");
+    loadConfig();
+    Set<String> expectedProjects = new HashSet<String>();
+    expectedProjects.add("platform/whee");
+    expectedProjects.add("whuu");
+    assertThat(configLoader.getProjectsInScope("master", "ds_two")).isEqualTo(expectedProjects);
+  }
+
+  @Test
+  public void getProjectsInScope_ignoreSourceManifestWithMissingDestManifest() throws Exception {
+    mockFile("alternate.config", allProjectsName.get(), RefNames.REFS_CONFIG, "automerger.config");
+    Mockito.when(gApiMock.projects().name("platform/manifest").branch("ds_four"))
+        .thenThrow(new ResourceNotFoundException());
+    loadConfig();
+    assertThat(configLoader.getProjectsInScope("master", "ds_four").isEmpty()).isTrue();
+  }
+
+  @Test
   public void isSkipMergeTest_noSkip() throws Exception {
     loadConfig();
     assertThat(configLoader.isSkipMerge("ds_two", "ds_three", "bla")).isFalse();
@@ -109,11 +158,26 @@
   }
 
   @Test
-  public void isSkipMergeTest_alwaysBlankMergeNull() throws Exception {
-    mockFile("alternate_config.yaml", "tools/automerger", "master", "config.yaml");
+  public void isSkipMergeTest_alwaysBlankMergeDummy() throws Exception {
+    mockFile("alternate.config", allProjectsName.get(), RefNames.REFS_CONFIG, "automerger.config");
     loadConfig();
-    assertThat(
-            configLoader.isSkipMerge("master", "ds_two", "test test \n \n DO NOT MERGE ANYWHERE"))
+    assertThat(configLoader.isSkipMerge("master", "ds_two", "test test")).isFalse();
+  }
+
+  @Test
+  public void isSkipMergeTest_alwaysBlankMergeNull() throws Exception {
+    mockFile("alternate.config", allProjectsName.get(), RefNames.REFS_CONFIG, "automerger.config");
+    loadConfig();
+    assertThat(configLoader.isSkipMerge("master", "ds_two", "test test \n \n BLANK ANYWHERE"))
+        .isTrue();
+  }
+
+  @Test
+  public void isSkipMergeTest_noBlankMergeSpecified() throws Exception {
+    mockFile(
+        "empty_blank.config", allProjectsName.get(), RefNames.REFS_CONFIG, "automerger.config");
+    loadConfig();
+    assertThat(configLoader.isSkipMerge("master", "ds_one", "test test \n \n DO NOT MERGE"))
         .isFalse();
   }
 
@@ -134,27 +198,13 @@
         .isEqualTo(expectedBranches);
   }
 
-  @Test(expected = IOException.class)
-  public void downstreamBranchesTest_IOException() throws Exception {
-    Mockito.when(
-            gApiMock
-                .projects()
-                .name("platform/manifest")
-                .branch("master")
-                .file("default.xml")
-                .asString())
-        .thenThrow(new IOException("!"));
+  @Test
+  public void downstreamBranchesTest_configException() throws Exception {
+    mockFile("wrong.config", allProjectsName.get(), RefNames.REFS_CONFIG, "automerger.config");
     loadConfig();
-    Set<String> expectedBranches = new HashSet<String>();
 
-    configLoader.getDownstreamBranches("master", "platform/some/project");
-  }
-
-  @Test(expected = RestApiException.class)
-  public void downstreamBranchesTest_restApiException() throws Exception {
-    Mockito.when(gApiMock.projects().name("platform/manifest").branch("master"))
-        .thenThrow(new RestApiException("!"));
-    loadConfig();
+    thrown.expect(ConfigInvalidException.class);
+    thrown.expectMessage("Automerger config branch pair malformed: master..ds_one");
     configLoader.getDownstreamBranches("master", "platform/some/project");
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorTest.java
index 5be92c4..54198f8 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorTest.java
@@ -34,24 +34,28 @@
 import java.util.Map;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
 
+@RunWith(MockitoJUnitRunner.class)
 public class DownstreamCreatorTest {
   private final String changeId = "testid";
   private final String changeProject = "testproject";
-  private final String changeBranch = "testbranch";
   private final String changeTopic = "testtopic";
   private final String changeSubject = "testmessage";
+
+  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
   private GerritApi gApiMock;
+
   private DownstreamCreator ds;
-  private ConfigLoader configMock;
 
   @Before
   public void setUp() throws Exception {
-    gApiMock = Mockito.mock(GerritApi.class, Mockito.RETURNS_DEEP_STUBS);
-    configMock = Mockito.mock(ConfigLoader.class);
-    ds = new DownstreamCreator(gApiMock, configMock);
+    ds = new DownstreamCreator(gApiMock, Mockito.mock(ConfigLoader.class));
   }
 
   private List<ChangeInfo> mockChangeInfoList(String upstreamBranch) {
@@ -70,15 +74,14 @@
     ChangeInfo info = Mockito.mock(ChangeInfo.class);
     info._number = number;
     info.currentRevision = "info" + number;
-    info.revisions = Mockito.mock(Map.class);
+    info.revisions = new HashMap<>();
 
     RevisionInfo revisionInfoMock = Mockito.mock(RevisionInfo.class);
     CommitInfo commit = Mockito.mock(CommitInfo.class);
     commit.parents = ImmutableList.of(parent1, parent2);
     revisionInfoMock.commit = commit;
 
-    Mockito.when(info.revisions.get(info.currentRevision)).thenReturn(revisionInfoMock);
-
+    info.revisions.put(info.currentRevision, revisionInfoMock);
     return info;
   }
 
@@ -112,9 +115,9 @@
     ArgumentCaptor<ChangeInput> changeInputCaptor = ArgumentCaptor.forClass(ChangeInput.class);
     Mockito.verify(gApiMock.changes()).create(changeInputCaptor.capture());
     ChangeInput changeInput = changeInputCaptor.getValue();
-    assertThat(changeProject).isEqualTo(changeInput.project);
-    assertThat("testds").isEqualTo(changeInput.branch);
-    assertThat(changeTopic).isEqualTo(changeInput.topic);
+    assertThat(changeInput.project).isEqualTo(changeProject);
+    assertThat(changeInput.branch).isEqualTo("testds");
+    assertThat(changeInput.topic).isEqualTo(changeTopic);
     assertThat(changeInput.merge.source).isEqualTo(currentRevision);
     assertThat(changeInput.merge.strategy).isEqualTo("recursive");
 
@@ -152,9 +155,9 @@
     ArgumentCaptor<ChangeInput> changeInputCaptor = ArgumentCaptor.forClass(ChangeInput.class);
     Mockito.verify(gApiMock.changes()).create(changeInputCaptor.capture());
     ChangeInput changeInput = changeInputCaptor.getValue();
-    assertThat(changeProject).isEqualTo(changeInput.project);
-    assertThat("testds").isEqualTo(changeInput.branch);
-    assertThat(changeTopic).isEqualTo(changeInput.topic);
+    assertThat(changeInput.project).isEqualTo(changeProject);
+    assertThat(changeInput.branch).isEqualTo("testds");
+    assertThat(changeInput.topic).isEqualTo(changeTopic);
     assertThat(changeInput.merge.source).isEqualTo(currentRevision);
 
     // Check that it was actually skipped
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config b/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config
new file mode 100644
index 0000000..eaaf49f
--- /dev/null
+++ b/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate.config
@@ -0,0 +1,15 @@
+[automerger "master:ds_one"]
+[automerger "master:ds_two"]
+  mergeAll = true
+  ignoreSourceManifest = true
+[automerger "ds_two:ds_three"]
+  setProjects = platform/some/project
+[automerger "master:ds_four"]
+  mergeAll = true
+  ignoreSourceManifest = true
+[global]
+  alwaysBlankMerge = .*BLANK ANYWHERE.*
+  blankMerge = .*DO NOT MERGE.*
+  manifestFile = default.xml
+  manifestProject = platform/manifest
+  ignoreProjects = platform/ignore/me
\ No newline at end of file
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate_config.yaml b/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate_config.yaml
deleted file mode 100644
index 7c8c51a..0000000
--- a/src/test/resources/com/googlesource/gerrit/plugins/automerger/alternate_config.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-branches:
-  master:
-    ds_one:
-      add_projects:
-      - platform/added/project
-      ignore_projects:
-      - whoo
-    ds_two:
-      merge_all: true
-      add_projects:
-      - platform/added/project
-      ignore_projects:
-      - whoo
-      set_projects:
-      - platform/some/project
-      - platform/other/project
-  ds_two:
-    ds_three:
-      set_projects:
-      - platform/some/project
-global:
-  blank_merge:
-  - .*DO NOT MERGE.*
-  manifest:
-    file: default.xml
-    project: platform/manifest
-  ignore_projects:
-  - platform/ignore/me
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/automerger.config b/src/test/resources/com/googlesource/gerrit/plugins/automerger/automerger.config
new file mode 100644
index 0000000..ea54637
--- /dev/null
+++ b/src/test/resources/com/googlesource/gerrit/plugins/automerger/automerger.config
@@ -0,0 +1,18 @@
+[automerger "master:ds_one"]
+  addProjects = platform/added/project
+  ignoreProjects = whoo
+[automerger "master:ds_two"]
+  mergeAll = true
+  addProjects = platform/added/project
+  ignoreProjects = whoo
+  setProjects = platform/some/project
+  setProjects = platform/other/project
+[automerger "ds_two:ds_three"]
+  setProjects = platform/some/project
+[global]
+  alwaysBlankMerge = .*Import translations. DO NOT MERGE.*
+  alwaysBlankMerge = .*DO NOT MERGE ANYWHERE.*
+  blankMerge = .*DO NOT MERGE.*
+  manifestFile = default.xml
+  manifestProject = platform/manifest
+  ignoreProjects = platform/ignore/me
\ No newline at end of file
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/config.yaml b/src/test/resources/com/googlesource/gerrit/plugins/automerger/config.yaml
deleted file mode 100644
index 271d875..0000000
--- a/src/test/resources/com/googlesource/gerrit/plugins/automerger/config.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-branches:
-  master:
-    ds_one:
-      add_projects:
-      - platform/added/project
-      ignore_projects:
-      - whoo
-    ds_two:
-      merge_all: true
-      add_projects:
-      - platform/added/project
-      ignore_projects:
-      - whoo
-      set_projects:
-      - platform/some/project
-      - platform/other/project
-  ds_two:
-    ds_three:
-      set_projects:
-      - platform/some/project
-global:
-  always_blank_merge:
-  - .*Import translations\.\sDO NOT MERGE.*
-  - .*DO NOT MERGE ANYWHERE.*
-  blank_merge:
-  - .*DO NOT MERGE.*
-  manifest:
-    file: default.xml
-    project: platform/manifest
-  ignore_projects:
-  - platform/ignore/me
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/empty_blank.config b/src/test/resources/com/googlesource/gerrit/plugins/automerger/empty_blank.config
new file mode 100644
index 0000000..41efd40
--- /dev/null
+++ b/src/test/resources/com/googlesource/gerrit/plugins/automerger/empty_blank.config
@@ -0,0 +1,15 @@
+[automerger "master:ds_one"]
+  addProjects = platform/added/project
+  ignoreProjects = whoo
+[automerger "master:ds_two"]
+  mergeAll = true
+  addProjects = platform/added/project
+  ignoreProjects = whoo
+  setProjects = platform/some/project
+  setProjects = platform/other/project
+[automerger "ds_two:ds_three"]
+  setProjects = platform/some/project
+[global]
+  manifestFile = default.xml
+  manifestProject = platform/manifest
+  ignoreProjects = platform/ignore/me
\ No newline at end of file
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/wrong.config b/src/test/resources/com/googlesource/gerrit/plugins/automerger/wrong.config
new file mode 100644
index 0000000..dfd87b2
--- /dev/null
+++ b/src/test/resources/com/googlesource/gerrit/plugins/automerger/wrong.config
@@ -0,0 +1,17 @@
+[automerger "master..ds_one"]
+  addProjects = platform/added/project
+  ignoreProjects = whoo
+[automerger "master:ds_two"]
+  mergeAll = true
+  addProjects = platform/added/project
+  ignoreProjects = whoo
+  setProjects = platform/some/project
+  setProjects = platform/other/project
+[automerger "ds_two:ds_three"]
+  setProjects = platform/some/project
+[global]
+  alwaysBlankMerge = .*BLANK ANYWHERE.*
+  blankMerge = .*DO NOT MERGE.*
+  manifestFile = default.xml
+  manifestProject = platform/manifest
+  ignoreProjects = platform/ignore/me
\ No newline at end of file