Merge "Cross-host support."
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.*