checkout: Catch multiple changes for same remote

Catch when there are multiple changes for the same remote much earlier
than previously, and with a better error message than "fatal: a branch
named 'working' already exists".

Bug: b/388621007
Change-Id: I05934df6ad4d8f9f0b195fc7310e37b11748c132
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/260065
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Ted Pudlik <tpudlik@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed-service-accounts.iam.gserviceaccount.com>
diff --git a/recipe_modules/checkout/api.py b/recipe_modules/checkout/api.py
index 5df25df..2797ec2 100644
--- a/recipe_modules/checkout/api.py
+++ b/recipe_modules/checkout/api.py
@@ -1524,6 +1524,34 @@
                                     path = path / repo['strip_prefix']
                                 ctx.bazel_overrides[repo['name']] = path
 
+    def _check_gerrit_changes(self, ctx: CheckoutContext):
+        """Check that we don't have multiple changes for the same repo."""
+        seen = {}
+
+        for change in ctx.changes:
+            if change.submitted:
+                continue
+
+            for remote, prev_change in seen.items():
+                if ctx.remotes_equivalent(change.remote, remote):
+                    failure = (
+                        f'Change {prev_change.name} and change {change.name} '
+                        f'are from equivalent remotes ({prev_change.remote!r} '
+                        f'and {change.remote!r}). This is not supported. '
+                        'Instead, stack these changes, or only refer to the '
+                        "top from a 'patches.json' file in another repository."
+                    )
+                    pres = self.m.step.empty(
+                        'multiple changes for same repo',
+                    ).presentation
+                    pres.step_summary_text = failure
+                    pres.status = 'FAILURE'
+                    pres.links[prev_change.name] = prev_change.gerrit_url
+                    pres.links[change.name] = change.gerrit_url
+                    raise self.m.step.StepFailure(failure)
+
+            seen[change.remote] = change
+
     def _configure_insteadof(self, ctx: CheckoutContext):
         """Configure git to use some urls in place of others."""
         if not ctx.options.rewrites:
@@ -1627,6 +1655,8 @@
                     ctx, options.remote, options.branch
                 )
 
+            self._check_gerrit_changes(ctx)
+
             self._configure_insteadof(ctx)
 
             if options.use_repo:
diff --git a/recipe_modules/checkout/test_api.py b/recipe_modules/checkout/test_api.py
index 733dcae..36f4730 100644
--- a/recipe_modules/checkout/test_api.py
+++ b/recipe_modules/checkout/test_api.py
@@ -404,3 +404,9 @@
         ) + self.post_process(
             post_process.MustRunRE, r'.*excluding submodule {}'.format(name)
         )
+
+    def multiple_changes_for_same_repo(self):
+        return self.post_process(
+            post_process.MustRunRE,
+            r'.*multiple changes for same repo',
+        )
diff --git a/recipe_modules/checkout/tests/git.expected/multiple-equiv-remote.json b/recipe_modules/checkout/tests/git.expected/multiple-equiv-remote.json
new file mode 100644
index 0000000..1f4770f
--- /dev/null
+++ b/recipe_modules/checkout/tests/git.expected/multiple-equiv-remote.json
@@ -0,0 +1,423 @@
+[
+  {
+    "cmd": [],
+    "name": "checkout pigweed",
+    "~followup_annotations": [
+      "@@@STEP_FAILURE@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.options",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_SUMMARY_TEXT@remote: \"https://pigweed.googlesource.com/pigweed/pigweed\"\nbranch: \"main\"\ninitialize_submodules: true\nmatch_branch: true\nuse_trigger: true\nequivalent_remotes {\n  remotes: \"https://pigweed.googlesource.com/foo\"\n  remotes: \"https://pigweed.googlesource.com/bar\"\n}\n@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.options with defaults",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_SUMMARY_TEXT@remote: \"https://pigweed.googlesource.com/pigweed/pigweed\"\nbranch: \"main\"\nmanifest_file: \"default.xml\"\ninitialize_submodules: true\nrepo_init_timeout_sec: 20\nrepo_sync_timeout_sec: 120\nnumber_of_attempts: 3\nmatch_branch: true\nsubmodule_timeout_sec: 600\nuse_trigger: true\nequivalent_remotes {\n  remotes: \"https://pigweed.googlesource.com/foo\"\n  remotes: \"https://pigweed.googlesource.com/bar\"\n}\n@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.0",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.get packages",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@5@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "RECIPE_MODULE[fuchsia::gerrit]/resources/cipd.ensure",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.get packages.read ensure file",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@6@@@",
+      "@@@STEP_LOG_LINE@cipd.ensure@infra/tools/luci/gerrit/${platform} version:pinned-version@@@",
+      "@@@STEP_LOG_END@cipd.ensure@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.install infra/tools/luci/gerrit",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@5@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0o777",
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.install infra/tools/luci/gerrit.ensure package directory",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@6@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07",
+      "-ensure-file",
+      "infra/tools/luci/gerrit/${platform} version:pinned-version",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.install infra/tools/luci/gerrit.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@6@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-version:pinned-v\",@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/gerrit/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
+      "change-detail",
+      "-host",
+      "https://pigweed-review.googlesource.com",
+      "-input",
+      "{\"change_id\": \"1000\", \"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_REVISION\"]}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.0.details",
+    "timeout": 30,
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"branch\": \"main\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"ffffffffffffffffffffffffffffffffffffffff\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"project\": \"foo\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"revisions\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ffffffffffffffffffffffffffffffffffffffff\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"_number\": 3,@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"commit\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"message\": \"\",@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"parents\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@          {}@@@",
+      "@@@STEP_LOG_LINE@json.output@        ]@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@json.input@{@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"1000\",@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
+      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\",@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
+      "@@@STEP_LOG_LINE@json.input@    ]@@@",
+      "@@@STEP_LOG_LINE@json.input@  }@@@",
+      "@@@STEP_LOG_LINE@json.input@}@@@",
+      "@@@STEP_LOG_END@json.input@@@",
+      "@@@STEP_LINK@gerrit link@https://pigweed-review.googlesource.com/q/1000@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.1",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
+      "change-detail",
+      "-host",
+      "https://pigweed-review.googlesource.com",
+      "-input",
+      "{\"change_id\": \"2000\", \"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_REVISION\"]}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.1.details",
+    "timeout": 30,
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"branch\": \"main\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"ffffffffffffffffffffffffffffffffffffffff\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"project\": \"bar\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"revisions\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ffffffffffffffffffffffffffffffffffffffff\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"_number\": 3,@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"commit\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"message\": \"\",@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"parents\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@          {}@@@",
+      "@@@STEP_LOG_LINE@json.output@        ]@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@json.input@{@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"2000\",@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
+      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\",@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
+      "@@@STEP_LOG_LINE@json.input@    ]@@@",
+      "@@@STEP_LOG_LINE@json.input@  }@@@",
+      "@@@STEP_LOG_LINE@json.input@}@@@",
+      "@@@STEP_LOG_END@json.input@@@",
+      "@@@STEP_LINK@gerrit link@https://pigweed-review.googlesource.com/q/2000@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
+      "change-detail",
+      "-host",
+      "https://pigweed-review.googlesource.com",
+      "-input",
+      "{\"change_id\": \"2000\", \"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_FILES\", \"CURRENT_REVISION\"]}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.details pigweed:2000",
+    "timeout": 30,
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"_number\": 2000,@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"HASH\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"project\": \"project\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"revisions\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"HASH\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"_number\": 1,@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"commit\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"message\": \"\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      },@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"files\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"filename\": {}@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  },@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"status\": \"NEW\"@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@json.input@{@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"2000\",@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
+      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\",@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_FILES\",@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
+      "@@@STEP_LOG_LINE@json.input@    ]@@@",
+      "@@@STEP_LOG_LINE@json.input@  }@@@",
+      "@@@STEP_LOG_LINE@json.input@}@@@",
+      "@@@STEP_LOG_END@json.input@@@",
+      "@@@STEP_LINK@gerrit link@https://pigweed-review.googlesource.com/q/2000@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.resolve deps for pigweed:2000",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_SUMMARY_TEXT@no dependencies@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.no topic",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.pass",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.changes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.changes.pigweed:1000",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_SUMMARY_TEXT@Change(number=1000, remote='https://pigweed.googlesource.com/foo', ref='refs/changes/00/1000/1', rebase=True, project='foo', branch='main', gerrit_name='pigweed', submitted=False, patchset=1, path=None, base=None, base_type=None, is_merge=False, commit_message='', topic=None, current_revision='ffffffffffffffffffffffffffffffffffffffff')@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.changes.pigweed:2000",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_SUMMARY_TEXT@Change(number=2000, remote='https://pigweed.googlesource.com/bar', ref='refs/changes/00/2000/1', rebase=True, project='bar', branch='main', gerrit_name='pigweed', submitted=False, patchset=1, path=None, base=None, base_type=None, is_merge=False, commit_message='', topic=None, current_revision='ffffffffffffffffffffffffffffffffffffffff')@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.multiple changes for same repo",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_SUMMARY_TEXT@Change pigweed:1000 and change pigweed:2000 are from equivalent remotes ('https://pigweed.googlesource.com/foo' and 'https://pigweed.googlesource.com/bar'). This is not supported. Instead, stack these changes, or only refer to the top from a 'patches.json' file in another repository.@@@",
+      "@@@STEP_LINK@pigweed:1000@https://pigweed-review.googlesource.com/c/1000@@@",
+      "@@@STEP_LINK@pigweed:2000@https://pigweed-review.googlesource.com/c/2000@@@",
+      "@@@STEP_FAILURE@@@"
+    ]
+  },
+  {
+    "failure": {
+      "failure": {},
+      "humanReason": "Change pigweed:1000 and change pigweed:2000 are from equivalent remotes ('https://pigweed.googlesource.com/foo' and 'https://pigweed.googlesource.com/bar'). This is not supported. Instead, stack these changes, or only refer to the top from a 'patches.json' file in another repository."
+    },
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/checkout/tests/git.expected/multiple-same-remote.json b/recipe_modules/checkout/tests/git.expected/multiple-same-remote.json
new file mode 100644
index 0000000..637a6d6
--- /dev/null
+++ b/recipe_modules/checkout/tests/git.expected/multiple-same-remote.json
@@ -0,0 +1,423 @@
+[
+  {
+    "cmd": [],
+    "name": "checkout pigweed",
+    "~followup_annotations": [
+      "@@@STEP_FAILURE@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.options",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_SUMMARY_TEXT@remote: \"https://pigweed.googlesource.com/pigweed/pigweed\"\nbranch: \"main\"\ninitialize_submodules: true\nmatch_branch: true\nuse_trigger: true\n@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.options with defaults",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_SUMMARY_TEXT@remote: \"https://pigweed.googlesource.com/pigweed/pigweed\"\nbranch: \"main\"\nmanifest_file: \"default.xml\"\ninitialize_submodules: true\nrepo_init_timeout_sec: 20\nrepo_sync_timeout_sec: 120\nnumber_of_attempts: 3\nmatch_branch: true\nsubmodule_timeout_sec: 600\nuse_trigger: true\n@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.0",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.get packages",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@5@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "RECIPE_MODULE[fuchsia::gerrit]/resources/cipd.ensure",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.get packages.read ensure file",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@6@@@",
+      "@@@STEP_LOG_LINE@cipd.ensure@infra/tools/luci/gerrit/${platform} version:pinned-version@@@",
+      "@@@STEP_LOG_END@cipd.ensure@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.install infra/tools/luci/gerrit",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@5@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0o777",
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.install infra/tools/luci/gerrit.ensure package directory",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@6@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07",
+      "-ensure-file",
+      "infra/tools/luci/gerrit/${platform} version:pinned-version",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.0.ensure infra/tools/luci/gerrit/${platform}.install infra/tools/luci/gerrit.ensure_installed",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@6@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-version:pinned-v\",@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/luci/gerrit/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
+      "change-detail",
+      "-host",
+      "https://pigweed-review.googlesource.com",
+      "-input",
+      "{\"change_id\": \"1000\", \"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_REVISION\"]}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.0.details",
+    "timeout": 30,
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"branch\": \"main\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"ffffffffffffffffffffffffffffffffffffffff\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"project\": \"foo\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"revisions\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ffffffffffffffffffffffffffffffffffffffff\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"_number\": 3,@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"commit\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"message\": \"\",@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"parents\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@          {}@@@",
+      "@@@STEP_LOG_LINE@json.output@        ]@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@json.input@{@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"1000\",@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
+      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\",@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
+      "@@@STEP_LOG_LINE@json.input@    ]@@@",
+      "@@@STEP_LOG_LINE@json.input@  }@@@",
+      "@@@STEP_LOG_LINE@json.input@}@@@",
+      "@@@STEP_LOG_END@json.input@@@",
+      "@@@STEP_LINK@gerrit link@https://pigweed-review.googlesource.com/q/1000@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.1",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
+      "change-detail",
+      "-host",
+      "https://pigweed-review.googlesource.com",
+      "-input",
+      "{\"change_id\": \"2000\", \"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_REVISION\"]}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.1.details",
+    "timeout": 30,
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"branch\": \"main\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"ffffffffffffffffffffffffffffffffffffffff\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"project\": \"foo\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"revisions\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"ffffffffffffffffffffffffffffffffffffffff\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"_number\": 3,@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"commit\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"message\": \"\",@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"parents\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@          {}@@@",
+      "@@@STEP_LOG_LINE@json.output@        ]@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@json.input@{@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"2000\",@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
+      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\",@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
+      "@@@STEP_LOG_LINE@json.input@    ]@@@",
+      "@@@STEP_LOG_LINE@json.input@  }@@@",
+      "@@@STEP_LOG_LINE@json.input@}@@@",
+      "@@@STEP_LOG_END@json.input@@@",
+      "@@@STEP_LINK@gerrit link@https://pigweed-review.googlesource.com/q/2000@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[START_DIR]/cipd_tool/infra/tools/luci/gerrit/0e548aa33f8113a45a5b3b62201e114e98e63d00f97296912380138f44597b07/gerrit",
+      "change-detail",
+      "-host",
+      "https://pigweed-review.googlesource.com",
+      "-input",
+      "{\"change_id\": \"2000\", \"params\": {\"o\": [\"CURRENT_COMMIT\", \"CURRENT_FILES\", \"CURRENT_REVISION\"]}}",
+      "-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "project:try"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.details pigweed:2000",
+    "timeout": 30,
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"_number\": 2000,@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"current_revision\": \"HASH\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"project\": \"project\",@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"revisions\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"HASH\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"_number\": 1,@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"commit\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"message\": \"\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      },@@@",
+      "@@@STEP_LOG_LINE@json.output@      \"files\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"filename\": {}@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    }@@@",
+      "@@@STEP_LOG_LINE@json.output@  },@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"status\": \"NEW\"@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@",
+      "@@@STEP_LOG_LINE@json.input@{@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"change_id\": \"2000\",@@@",
+      "@@@STEP_LOG_LINE@json.input@  \"params\": {@@@",
+      "@@@STEP_LOG_LINE@json.input@    \"o\": [@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_COMMIT\",@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_FILES\",@@@",
+      "@@@STEP_LOG_LINE@json.input@      \"CURRENT_REVISION\"@@@",
+      "@@@STEP_LOG_LINE@json.input@    ]@@@",
+      "@@@STEP_LOG_LINE@json.input@  }@@@",
+      "@@@STEP_LOG_LINE@json.input@}@@@",
+      "@@@STEP_LOG_END@json.input@@@",
+      "@@@STEP_LINK@gerrit link@https://pigweed-review.googlesource.com/q/2000@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.resolve deps for pigweed:2000",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_SUMMARY_TEXT@no dependencies@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.no topic",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.process gerrit changes.resolve CL deps.pass",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.changes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.changes.pigweed:1000",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_SUMMARY_TEXT@Change(number=1000, remote='https://pigweed.googlesource.com/foo', ref='refs/changes/00/1000/1', rebase=True, project='foo', branch='main', gerrit_name='pigweed', submitted=False, patchset=1, path=None, base=None, base_type=None, is_merge=False, commit_message='', topic=None, current_revision='ffffffffffffffffffffffffffffffffffffffff')@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.change data.changes.pigweed:2000",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_SUMMARY_TEXT@Change(number=2000, remote='https://pigweed.googlesource.com/foo', ref='refs/changes/00/2000/1', rebase=True, project='foo', branch='main', gerrit_name='pigweed', submitted=False, patchset=1, path=None, base=None, base_type=None, is_merge=False, commit_message='', topic=None, current_revision='ffffffffffffffffffffffffffffffffffffffff')@@@"
+    ]
+  },
+  {
+    "cmd": [],
+    "name": "checkout pigweed.multiple changes for same repo",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_SUMMARY_TEXT@Change pigweed:1000 and change pigweed:2000 are from equivalent remotes ('https://pigweed.googlesource.com/foo' and 'https://pigweed.googlesource.com/foo'). This is not supported. Instead, stack these changes, or only refer to the top from a 'patches.json' file in another repository.@@@",
+      "@@@STEP_LINK@pigweed:1000@https://pigweed-review.googlesource.com/c/1000@@@",
+      "@@@STEP_LINK@pigweed:2000@https://pigweed-review.googlesource.com/c/2000@@@",
+      "@@@STEP_FAILURE@@@"
+    ]
+  },
+  {
+    "failure": {
+      "failure": {},
+      "humanReason": "Change pigweed:1000 and change pigweed:2000 are from equivalent remotes ('https://pigweed.googlesource.com/foo' and 'https://pigweed.googlesource.com/foo'). This is not supported. Instead, stack these changes, or only refer to the top from a 'patches.json' file in another repository."
+    },
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/checkout/tests/git.py b/recipe_modules/checkout/tests/git.py
index 6cb2ef6..0bb292b 100644
--- a/recipe_modules/checkout/tests/git.py
+++ b/recipe_modules/checkout/tests/git.py
@@ -112,3 +112,36 @@
         api.step_data('checkout pigweed.cache.git fetch', retcode=1),
         status='INFRA_FAILURE',
     )
+
+    yield api.test(
+        'multiple-same-remote',
+        properties(),
+        api.checkout.try_test_data(
+            gerrit_changes=[
+                api.checkout.cl('pigweed-review.googlesource.com', 'foo', 1000),
+                api.checkout.cl('pigweed-review.googlesource.com', 'foo', 2000),
+            ]
+        ),
+        api.checkout.multiple_changes_for_same_repo(),
+        status='FAILURE',
+    )
+
+    yield api.test(
+        'multiple-equiv-remote',
+        properties(
+            equivalent_remotes=(
+                (
+                    'https://pigweed.googlesource.com/foo',
+                    'https://pigweed.googlesource.com/bar',
+                ),
+            ),
+        ),
+        api.checkout.try_test_data(
+            gerrit_changes=[
+                api.checkout.cl('pigweed-review.googlesource.com', 'foo', 1000),
+                api.checkout.cl('pigweed-review.googlesource.com', 'bar', 2000),
+            ]
+        ),
+        api.checkout.multiple_changes_for_same_repo(),
+        status='FAILURE',
+    )