Cross-host support.

Users are expected to inject their own ApiManager in order to make this
work. If an ApiManager is not injected, this should behave the same as
it did before this change.

Change-Id: I6a9c2e6aa39ab0ee650f23da97347274e557a1cf
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ApiManager.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ApiManager.java
new file mode 100644
index 0000000..8a44a07
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ApiManager.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2018 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.gerrit.extensions.api.GerritApi;
+import com.google.inject.Inject;
+
+/** Interface for sending REST API requests to another Gerrit host. */
+public interface ApiManager {
+  public GerritApi forHostname(String hostname);
+
+  class DefaultApiManager implements ApiManager {
+    protected GerritApi gApi;
+
+    @Inject
+    public DefaultApiManager(GerritApi gApi) {
+      this.gApi = gApi;
+    }
+
+    @Override
+    public GerritApi forHostname(String hostname) {
+      return gApi;
+    }
+  }
+}
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 92d8176..15ae823 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
@@ -27,6 +27,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
+import java.util.HashMap;
 import java.util.Map;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
@@ -83,6 +84,10 @@
     mdsMergeInput.obsoleteRevision = revision;
     mdsMergeInput.currentRevision = revision;
 
+    String changeBranch = change.getDest().get();
+    mdsMergeInput.fromCrossHostMap = config.getFromCrossHostMap(changeBranch, branchMap.keySet());
+    mdsMergeInput.toCrossHostMap = config.getToCrossHostMap(changeBranch, branchMap.keySet());
+
     log.debug("Multiple downstream merge input: {}", mdsMergeInput.dsBranchMap);
 
     try {
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 b588a2f..227b22c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergerModule.java
@@ -50,5 +50,6 @@
           }
         });
     DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new JavaScriptPlugin("automerger.js"));
+    bind(ApiManager.class).to(ApiManager.DefaultApiManager.class);
   }
 }
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..5ddf308 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
@@ -33,8 +33,10 @@
 import com.google.re2j.Pattern;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.HashMap;
 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;
@@ -113,6 +115,42 @@
     return false;
   }
 
+  // Returns cross host URL if specified, null if not
+  public String getFromCrossHost(String fromBranch, String toBranch) throws ConfigInvalidException {
+    return getConfig()
+        .getString("automerger", fromBranch + BRANCH_DELIMITER + toBranch, "fromCrossHost");
+  }
+
+  public Map<String, String> getFromCrossHostMap(String fromBranch, Set<String> branches)
+      throws ConfigInvalidException {
+    Map<String, String> fromCrossHostMap = new HashMap<String, String>();
+    for (String branch : branches) {
+      String fromCrossHost = getFromCrossHost(fromBranch, branch);
+      if (fromCrossHost != null) {
+        fromCrossHostMap.put(fromBranch, fromCrossHost);
+      }
+    }
+    return fromCrossHostMap;
+  }
+
+  // Returns cross host URL if specified, null if not
+  public String getToCrossHost(String fromBranch, String toBranch) throws ConfigInvalidException {
+    return getConfig()
+        .getString("automerger", fromBranch + BRANCH_DELIMITER + toBranch, "toCrossHost");
+  }
+
+  public Map<String, String> getToCrossHostMap(String toBranch, Set<String> branches)
+      throws ConfigInvalidException {
+    Map<String, String> toCrossHostMap = new HashMap<String, String>();
+    for (String branch : branches) {
+      String toCrossHost = getToCrossHost(branch, toBranch);
+      if (toCrossHost != null) {
+        toCrossHostMap.put(branch, toCrossHost);
+      }
+    }
+    return toCrossHostMap;
+  }
+
   private Pattern getConfigPattern(String key) throws ConfigInvalidException {
     String[] patternList = getConfig().getStringList("global", null, key);
     Set<String> mergeStrings = new HashSet<>(Arrays.asList(patternList));
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 7de0782..7884411 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
@@ -78,6 +78,7 @@
   private static final String CURRENT = "current";
 
   protected GerritApi gApi;
+  protected ApiManager apiManager;
   protected ConfigLoader config;
   protected CurrentUser user;
 
@@ -85,8 +86,12 @@
 
   @Inject
   public DownstreamCreator(
-      GerritApi gApi, ConfigLoader config, OneOffRequestContext oneOffRequestContext) {
+      GerritApi gApi,
+      ApiManager apiManager,
+      ConfigLoader config,
+      OneOffRequestContext oneOffRequestContext) {
     this.gApi = gApi;
+    this.apiManager = apiManager;
     this.config = config;
     this.oneOffRequestContext = oneOffRequestContext;
   }
@@ -159,11 +164,12 @@
       } else {
         for (String downstreamBranch : downstreamBranches) {
           try {
+            String toCrossHost = config.getToCrossHost(change.branch, downstreamBranch);
             List<Integer> existingDownstream =
-                getExistingMergesOnBranch(revision, oldTopic, downstreamBranch);
+                getExistingMergesOnBranch(revision, oldTopic, downstreamBranch, toCrossHost);
             for (Integer changeNumber : existingDownstream) {
               log.debug("Setting topic {} on {}", change.topic, changeNumber);
-              gApi.changes().id(changeNumber).topic(change.topic);
+              apiManager.forHostname(toCrossHost).changes().id(changeNumber).topic(change.topic);
             }
           } catch (RestApiException | InvalidQueryParameterException e) {
             log.error("Failed to edit downstream topics of {}", change.id, e);
@@ -208,11 +214,16 @@
 
       for (String downstreamBranch : downstreamBranches) {
         try {
+          String toCrossHost = config.getToCrossHost(change.branch, downstreamBranch);
           List<Integer> existingDownstream =
-              getExistingMergesOnBranch(revision, change.topic, downstreamBranch);
+              getExistingMergesOnBranch(revision, change.topic, downstreamBranch, toCrossHost);
           for (Integer changeNumber : existingDownstream) {
             ChangeInfo downstreamChange =
-                gApi.changes().id(changeNumber).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
+                apiManager
+                    .forHostname(toCrossHost)
+                    .changes()
+                    .id(changeNumber)
+                    .get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
             for (Map.Entry<String, LabelInfo> labelEntry : labels.entrySet()) {
               if (labelEntry.getValue().all.size() > 0) {
                 OptionalInt maxVote =
@@ -225,7 +236,11 @@
                         .max();
 
                 if (maxVote.isPresent()) {
-                  updateVote(downstreamChange, labelEntry.getKey(), (short) maxVote.getAsInt());
+                  updateVote(
+                      downstreamChange,
+                      labelEntry.getKey(),
+                      (short) maxVote.getAsInt(),
+                      toCrossHost);
                 }
               }
             }
@@ -315,13 +330,27 @@
 
       // Make the vote on the original change
       ChangeInfo originalChange =
-          getOriginalChange(mdsMergeInput.changeNumber, mdsMergeInput.currentRevision);
+          getOriginalChange(
+              mdsMergeInput.changeNumber,
+              mdsMergeInput.currentRevision,
+              mdsMergeInput.sourceBranch,
+              null);
       // if this fails, i.e. -2 is restricted, catch it and still post message without a vote.
       try {
-        gApi.changes().id(originalChange._number).revision(CURRENT).review(reviewInput);
+        apiManager
+            .forHostname(mdsMergeInput.fromCrossHostMap.get(originalChange.branch))
+            .changes()
+            .id(originalChange._number)
+            .revision(CURRENT)
+            .review(reviewInput);
       } catch (AuthException e) {
         reviewInput.labels = null;
-        gApi.changes().id(originalChange._number).revision(CURRENT).review(reviewInput);
+        apiManager
+            .forHostname(mdsMergeInput.fromCrossHostMap.get(originalChange.branch))
+            .changes()
+            .id(originalChange._number)
+            .revision(CURRENT)
+            .review(reviewInput);
       }
     }
   }
@@ -348,10 +377,14 @@
         // If there are existing downstream merges, update them
         // Otherwise, create them.
         boolean createDownstreams = true;
+        String toCrossHost = config.getToCrossHost(mdsMergeInput.sourceBranch, downstreamBranch);
         if (mdsMergeInput.obsoleteRevision != null) {
           existingDownstream =
               getExistingMergesOnBranch(
-                  mdsMergeInput.obsoleteRevision, mdsMergeInput.topic, downstreamBranch);
+                  mdsMergeInput.obsoleteRevision,
+                  mdsMergeInput.topic,
+                  downstreamBranch,
+                  toCrossHost);
           if (!existingDownstream.isEmpty()) {
             log.debug(
                 "Attempting to update downstream merge of {} on branch {}",
@@ -361,19 +394,26 @@
             // it's possible to construct it so that it's not
             for (Integer dsChangeNumber : existingDownstream) {
               try {
+                log.info("mds merge input: {}", mdsMergeInput);
+                log.info("mds merge input crosshmap: {}", mdsMergeInput.toCrossHostMap);
+                log.info(
+                    "mds merge input crosshmap: {}",
+                    mdsMergeInput.toCrossHostMap.get(downstreamBranch));
                 updateDownstreamMerge(
                     mdsMergeInput.currentRevision,
                     mdsMergeInput.subject,
                     dsChangeNumber,
                     mdsMergeInput.dsBranchMap.get(downstreamBranch),
                     mdsMergeInput.changeNumber,
-                    downstreamBranch);
+                    downstreamBranch,
+                    mdsMergeInput.toCrossHostMap.get(downstreamBranch),
+                    mdsMergeInput.fromCrossHostMap.get(mdsMergeInput.sourceBranch));
                 createDownstreams = false;
               } catch (MergeConflictException e) {
                 failedMergeBranchMap.put(downstreamBranch, e.getMessage());
                 log.debug(
                     "Abandoning existing, obsolete {} due to merge conflict.", dsChangeNumber);
-                abandonChange(dsChangeNumber);
+                abandonChange(dsChangeNumber, toCrossHost);
               }
             }
           }
@@ -391,6 +431,8 @@
           sdsMergeInput.subject = mdsMergeInput.subject;
           sdsMergeInput.downstreamBranch = downstreamBranch;
           sdsMergeInput.doMerge = mdsMergeInput.dsBranchMap.get(downstreamBranch);
+          sdsMergeInput.fromCrossHost = mdsMergeInput.fromCrossHostMap.get(downstreamBranch);
+          sdsMergeInput.toCrossHost = mdsMergeInput.toCrossHostMap.get(downstreamBranch);
           try {
             createSingleDownstreamMerge(sdsMergeInput);
           } catch (MergeConflictException e) {
@@ -426,12 +468,12 @@
    * @throws OrmException Throws if we fail to open the request context
    */
   public List<Integer> getExistingMergesOnBranch(
-      String upstreamRevision, String topic, String downstreamBranch)
+      String upstreamRevision, String topic, String downstreamBranch, String hostname)
       throws RestApiException, InvalidQueryParameterException, OrmException,
           ConfigInvalidException {
     try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
       List<Integer> downstreamChangeNumbers = new ArrayList<>();
-      List<ChangeInfo> changes = getChangesInTopicAndBranch(topic, downstreamBranch);
+      List<ChangeInfo> changes = getChangesInTopicAndBranch(topic, downstreamBranch, hostname);
 
       for (ChangeInfo change : changes) {
         String changeRevision = change.currentRevision;
@@ -488,7 +530,9 @@
       downstreamChangeInput.baseChange =
           getBaseChangeId(
               getChangeParents(sdsMergeInput.changeNumber, sdsMergeInput.currentRevision),
-              sdsMergeInput.downstreamBranch);
+              sdsMergeInput.downstreamBranch,
+              sdsMergeInput.fromCrossHost,
+              sdsMergeInput.toCrossHost);
 
       if (!sdsMergeInput.doMerge) {
         mergeInput.strategy = "ours";
@@ -501,8 +545,9 @@
             sdsMergeInput.downstreamBranch);
       }
 
-      ChangeApi downstreamChange = gApi.changes().create(downstreamChangeInput);
-      tagChange(downstreamChange.get(), "Automerger change created!");
+      ChangeApi downstreamChange =
+          apiManager.forHostname(sdsMergeInput.toCrossHost).changes().create(downstreamChangeInput);
+      tagChange(downstreamChange.get(), "Automerger change created!", sdsMergeInput.toCrossHost);
     }
   }
 
@@ -530,19 +575,21 @@
    * @throws InvalidQueryParameterException
    * @throws RestApiException
    */
-  private String getBaseChangeId(List<String> parents, String branch)
+  private String getBaseChangeId(
+      List<String> parents, String branch, String fromCrossHost, String toCrossHost)
       throws InvalidQueryParameterException, RestApiException {
     if (parents.isEmpty()) {
       log.info("No base change id for change with no parents.");
       return null;
     }
     // 1) Get topic of first parent
-    String firstParentTopic = getTopic(parents.get(0));
+    String firstParentTopic = getTopic(parents.get(0), fromCrossHost);
     if (firstParentTopic == null) {
       return null;
     }
     // 2) query that topic and use that to find A'
-    List<ChangeInfo> changesInTopic = getChangesInTopicAndBranch(firstParentTopic, branch);
+    List<ChangeInfo> changesInTopic =
+        getChangesInTopicAndBranch(firstParentTopic, branch, toCrossHost);
     String firstParent = parents.get(0);
     for (ChangeInfo change : changesInTopic) {
       List<CommitInfo> topicChangeParents =
@@ -568,8 +615,9 @@
       return;
     }
 
-    // Map whether or not we should merge it or skip it for each downstream
+    // Map of whether or not we should merge it or skip it for each downstream
     Map<String, Boolean> dsBranchMap = new HashMap<String, Boolean>();
+    // Map of downstream branch to corresponding API to use.
     for (String downstreamBranch : downstreamBranches) {
       boolean isSkipMerge = config.isSkipMerge(change.branch, downstreamBranch, change.subject);
       dsBranchMap.put(downstreamBranch, !isSkipMerge);
@@ -583,11 +631,14 @@
     mdsMergeInput.dsBranchMap = dsBranchMap;
     mdsMergeInput.changeNumber = change._number;
     mdsMergeInput.patchsetNumber = revisionInfo._number;
+    mdsMergeInput.sourceBranch = change.branch;
     mdsMergeInput.project = change.project;
     mdsMergeInput.topic = getOrSetTopic(change._number, change.topic);
     mdsMergeInput.subject = change.subject;
     mdsMergeInput.obsoleteRevision = previousRevision;
     mdsMergeInput.currentRevision = currentRevision;
+    mdsMergeInput.fromCrossHostMap = config.getFromCrossHostMap(change.branch, downstreamBranches);
+    mdsMergeInput.toCrossHostMap = config.getToCrossHostMap(change.branch, downstreamBranches);
 
     createMergesAndHandleConflicts(mdsMergeInput);
   }
@@ -602,11 +653,13 @@
       }
 
       for (String downstreamBranch : downstreamBranches) {
+        String toCrossHost = config.getToCrossHost(change.branch, downstreamBranch);
+
         List<Integer> existingDownstream =
-            getExistingMergesOnBranch(revision, change.topic, downstreamBranch);
+            getExistingMergesOnBranch(revision, change.topic, downstreamBranch, toCrossHost);
         log.debug("Abandoning existing downstreams: {}", existingDownstream);
         for (Integer changeNumber : existingDownstream) {
-          abandonChange(changeNumber);
+          abandonChange(changeNumber, toCrossHost);
         }
       }
     } catch (RestApiException | IOException | InvalidQueryParameterException e) {
@@ -614,7 +667,8 @@
     }
   }
 
-  private void updateVote(ChangeInfo change, String label, short vote) throws RestApiException {
+  private void updateVote(ChangeInfo change, String label, short vote, String hostname)
+      throws RestApiException {
     log.debug("Giving {} for label {} to {}", vote, label, change.id);
     // Vote on all downstream branches unless merge conflict.
     ReviewInput reviewInput = new ReviewInput();
@@ -624,19 +678,30 @@
     reviewInput.notify = NotifyHandling.NONE;
     reviewInput.tag = AUTOMERGER_TAG;
     try {
-      gApi.changes().id(change.id).revision(CURRENT).review(reviewInput);
+      apiManager
+          .forHostname(hostname)
+          .changes()
+          .id(change.id)
+          .revision(CURRENT)
+          .review(reviewInput);
     } catch (AuthException e) {
       log.error("Automerger could not set label, but still continuing.", e);
     }
   }
 
-  private void tagChange(ChangeInfo change, String message) throws RestApiException {
+  private void tagChange(ChangeInfo change, String message, String hostname)
+      throws RestApiException {
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.message(message);
     reviewInput.notify = NotifyHandling.NONE;
     reviewInput.tag = AUTOMERGER_TAG;
     try {
-      gApi.changes().id(change.id).revision(CURRENT).review(reviewInput);
+      apiManager
+          .forHostname(hostname)
+          .changes()
+          .id(change.id)
+          .revision(CURRENT)
+          .review(reviewInput);
     } catch (AuthException e) {
       log.error("Automerger could not set label, but still continuing.", e);
     }
@@ -645,10 +710,12 @@
   private void updateDownstreamMerge(
       String newParentRevision,
       String upstreamSubject,
-      Integer sourceNum,
+      Integer existingDownstreamNum,
       boolean doMerge,
       Integer upstreamChangeNumber,
-      String downstreamBranch)
+      String downstreamBranch,
+      String fromCrossHost,
+      String toCrossHost)
       throws RestApiException, InvalidQueryParameterException {
     MergeInput mergeInput = new MergeInput();
     mergeInput.source = newParentRevision;
@@ -661,15 +728,19 @@
       mergeInput.strategy = "ours";
       mergePatchSetInput.subject =
           getSubjectForDownstreamMerge(upstreamSubject, newParentRevision, true);
-      log.debug("Skipping merge for {} on {}", newParentRevision, sourceNum);
+      log.debug("Skipping merge for {} on {}", newParentRevision, existingDownstreamNum);
     }
     mergePatchSetInput.merge = mergeInput;
 
     mergePatchSetInput.baseChange =
         getBaseChangeId(
-            getChangeParents(upstreamChangeNumber, newParentRevision), downstreamBranch);
+            getChangeParents(upstreamChangeNumber, newParentRevision),
+            downstreamBranch,
+            fromCrossHost,
+            toCrossHost);
 
-    ChangeApi originalChange = gApi.changes().id(sourceNum);
+    ChangeApi originalChange =
+        apiManager.forHostname(toCrossHost).changes().id(existingDownstreamNum);
 
     if (originalChange.info().status == ChangeStatus.ABANDONED) {
       RestoreInput restoreInput = new RestoreInput();
@@ -699,20 +770,28 @@
     return previousRevision;
   }
 
-  private ChangeInfo getOriginalChange(int changeNumber, String currentRevision)
-      throws RestApiException, InvalidQueryParameterException {
+  private ChangeInfo getOriginalChange(
+      int changeNumber, String currentRevision, String downstreamBranch, String hostname)
+      throws RestApiException, InvalidQueryParameterException, ConfigInvalidException {
     List<String> parents = getChangeParents(changeNumber, currentRevision);
     if (parents.size() >= 2) {
       String secondParentRevision = parents.get(1);
-      String topic = gApi.changes().id(changeNumber).topic();
-      List<ChangeInfo> changesInTopic = getChangesInTopic(topic);
+      String topic = apiManager.forHostname(hostname).changes().id(changeNumber).topic();
+      List<ChangeInfo> changesInTopic = getChangesInTopic(topic, hostname);
       for (ChangeInfo change : changesInTopic) {
+        String newHostname = hostname;
+        String fromCrossHost = config.getFromCrossHost(change.branch, downstreamBranch);
+        if (fromCrossHost != null) {
+          newHostname = fromCrossHost;
+        }
         if (change.currentRevision.equals(secondParentRevision)) {
-          return getOriginalChange(change._number, secondParentRevision);
+          return getOriginalChange(
+              change._number, secondParentRevision, change.branch, newHostname);
         }
       }
     }
-    return gApi.changes().id(changeNumber).get();
+
+    return apiManager.forHostname(hostname).changes().id(changeNumber).get();
   }
 
   private List<String> getChangeParents(int changeNumber, String currentRevision)
@@ -729,19 +808,22 @@
     return parents;
   }
 
-  private void abandonChange(Integer changeNumber) throws RestApiException {
+  private void abandonChange(Integer changeNumber, String toCrossHost) throws RestApiException {
     log.debug("Abandoning change: {}", changeNumber);
     AbandonInput abandonInput = new AbandonInput();
     abandonInput.notify = NotifyHandling.NONE;
     abandonInput.message = "Merge parent updated; abandoning due to upstream conflict.";
-    gApi.changes().id(changeNumber).abandon(abandonInput);
+    apiManager.forHostname(toCrossHost).changes().id(changeNumber).abandon(abandonInput);
   }
 
-  private String getTopic(String revision) throws InvalidQueryParameterException, RestApiException {
+  private String getTopic(String revision, String hostname)
+      throws InvalidQueryParameterException, RestApiException {
     QueryBuilder queryBuilder = new QueryBuilder();
     queryBuilder.addParameter("commit", revision);
     List<ChangeInfo> changes =
-        gApi.changes()
+        apiManager
+            .forHostname(hostname)
+            .changes()
             .query(queryBuilder.get())
             .withOption(ListChangesOption.CURRENT_REVISION)
             .get();
@@ -762,20 +844,25 @@
     return queryBuilder;
   }
 
-  private List<ChangeInfo> getChangesInTopic(String topic)
+  private List<ChangeInfo> getChangesInTopic(String topic, String hostname)
       throws InvalidQueryParameterException, RestApiException {
     QueryBuilder queryBuilder = constructTopicQuery(topic);
-    return gApi.changes()
+    return apiManager
+        .forHostname(hostname)
+        .changes()
         .query(queryBuilder.get())
         .withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT)
         .get();
   }
 
-  private List<ChangeInfo> getChangesInTopicAndBranch(String topic, String downstreamBranch)
+  private List<ChangeInfo> getChangesInTopicAndBranch(
+      String topic, String downstreamBranch, String hostname)
       throws InvalidQueryParameterException, RestApiException {
     QueryBuilder queryBuilder = constructTopicQuery(topic);
     queryBuilder.addParameter("branch", downstreamBranch);
-    return gApi.changes()
+    return apiManager
+        .forHostname(hostname)
+        .changes()
         .query(queryBuilder.get())
         .withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT)
         .get();
@@ -785,7 +872,8 @@
       throws InvalidQueryParameterException, RestApiException {
     // If we've already merged this commit to this branch, don't do it again.
     List<ChangeInfo> changes =
-        getChangesInTopicAndBranch(currentTopic, sdsMergeInput.downstreamBranch);
+        getChangesInTopicAndBranch(
+            currentTopic, sdsMergeInput.downstreamBranch, sdsMergeInput.toCrossHost);
     for (ChangeInfo change : changes) {
       if (change.branch.equals(sdsMergeInput.downstreamBranch)) {
         List<CommitInfo> parents = change.revisions.get(change.currentRevision).commit.parents;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamMergeInput.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamMergeInput.java
index a62d9a7..d420d57 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamMergeInput.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/MultipleDownstreamMergeInput.java
@@ -23,9 +23,12 @@
   public Map<String, Boolean> dsBranchMap;
   public int changeNumber;
   public int patchsetNumber;
+  public String sourceBranch;
   public String project;
   public String topic;
   public String subject;
   public String obsoleteRevision;
   public String currentRevision;
+  public Map<String, String> fromCrossHostMap;
+  public Map<String, String> toCrossHostMap;
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamMergeInput.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamMergeInput.java
index 5a5f0e7..4e4740c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamMergeInput.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/SingleDownstreamMergeInput.java
@@ -24,4 +24,6 @@
   public String subject;
   public String downstreamBranch;
   public boolean doMerge;
+  public String fromCrossHost;
+  public String toCrossHost;
 }
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..91bb93a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java
@@ -181,6 +181,20 @@
         .isEqualTo(expectedBranches);
   }
 
+  @Test
+  public void getFromCrossHostTest() throws Exception {
+    defaultSetup("automerger.config");
+    assertThat(configLoader.getFromCrossHost("other_host_branch", "this_host_branch"))
+        .isEqualTo("https://boogieplex-android.example.com");
+  }
+
+  @Test
+  public void getToCrossHostTest() throws Exception {
+    defaultSetup("automerger.config");
+    assertThat(configLoader.getToCrossHost("this_host_branch", "that_host_branch"))
+        .isEqualTo("https://boogieplex-chrome.example.com");
+  }
+
   private void defaultSetup(String resourceName) throws Exception {
     createProject("All-Projects");
     manifestNameKey = createProject("platform/manifest");
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/automerger.config b/src/test/resources/com/googlesource/gerrit/plugins/automerger/automerger.config
index ea54637..82c4b6a 100644
--- a/src/test/resources/com/googlesource/gerrit/plugins/automerger/automerger.config
+++ b/src/test/resources/com/googlesource/gerrit/plugins/automerger/automerger.config
@@ -9,6 +9,12 @@
   setProjects = platform/other/project
 [automerger "ds_two:ds_three"]
   setProjects = platform/some/project
+[automerger "other_host_branch:this_host_branch"]
+  setProjects = platform/some/project
+  fromCrossHost = https://boogieplex-android.example.com
+[automerger "this_host_branch:that_host_branch"]
+  setProjects = platform/some/project
+  toCrossHost = https://boogieplex-chrome.example.com
 [global]
   alwaysBlankMerge = .*Import translations. DO NOT MERGE.*
   alwaysBlankMerge = .*DO NOT MERGE ANYWHERE.*