upload: add --ignore-remote-commits When uploading a branch that merges another remote-tracking branch (such as an upstream branch into the destination branch), ReviewableBranch only excludes the destination tracking branch (^base). As a result, git rev-list walks the merged parent and counts all merged commits even though they already exist on a remote branch, triggering the unusual commit count warning in _VerifyPendingCommits. Add --ignore-remote-commits to `repo upload` to pass --not --remotes to git rev-list in ReviewableBranch so commits already reachable from refs/remotes/* are excluded from the pending commit list. Test: ./run_tests tests/test_project.py tests/test_subcmds_upload.py Change-Id: Ib1fb864982729e20a621144580fe95daa478eaee Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/651284 Tested-by: Romaric Jodin <rjodin@google.com> Reviewed-by: Brian Gan <brgan@google.com> Commit-Queue: Romaric Jodin <rjodin@google.com>
diff --git a/completion.zsh b/completion.zsh index 2c37f46..9d6e871 100644 --- a/completion.zsh +++ b/completion.zsh
@@ -411,6 +411,7 @@ '(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \ '--ignore-untracked-files[Ignore untracked files]' \ '--no-ignore-untracked-files[Do not ignore untracked files]' \ + '--ignore-remote-commits[Ignore commits already in remote branches]' \ '--no-cert-checks[Do not check certificates]' \ '--no-verify[Do not verify]' \ '--verify[Verify]' \
diff --git a/man/repo-upload.1 b/man/repo-upload.1 index ad17b69..3d7abb9 100644 --- a/man/repo-upload.1 +++ b/man/repo-upload.1
@@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man. -.TH REPO "1" "August 2026" "repo upload" "Repo Manual" +.TH REPO "1" "October 2026" "repo upload" "Repo Manual" .SH NAME repo \- repo upload - manual page for repo upload .SH SYNOPSIS @@ -80,6 +80,9 @@ \fB\-\-no\-ignore\-untracked\-files\fR always ask about untracked files in the working copy .TP +\fB\-\-ignore\-remote\-commits\fR +ignore commits already in remote branches +.TP \fB\-\-no\-cert\-checks\fR disable verifying ssl certs (unsafe) .SS Logging options:
diff --git a/project.py b/project.py index 13361d1..4b23e8a 100644 --- a/project.py +++ b/project.py
@@ -36,6 +36,7 @@ import error import fetch import git_command +from git_config import Branch from git_config import GetSchemeFromUrl from git_config import GetUrlCookieFile from git_config import GitConfig @@ -180,10 +181,17 @@ _commit_cache = None _base_exists = None - def __init__(self, project, branch, base): + def __init__( + self, + project: "Project", + branch: Branch, + base: str, + ignore_remote_commits: bool = False, + ) -> None: self.project = project self.branch = branch self.base = base + self.ignore_remote_commits = ignore_remote_commits @property def name(self): @@ -204,6 +212,7 @@ "--date-order", not_rev(self.base), git_refs.R_HEADS + self.name, + *(("--not", "--remotes") if self.ignore_remote_commits else ()), "--", ) try: @@ -226,7 +235,10 @@ def unabbrev_commits(self): r = {} for commit in self.project.bare_git.rev_list( - not_rev(self.base), git_refs.R_HEADS + self.name, "--" + not_rev(self.base), + git_refs.R_HEADS + self.name, + *(("--not", "--remotes") if self.ignore_remote_commits else ()), + "--", ): r[commit[0:8]] = commit return r @@ -1424,7 +1436,11 @@ if git_refs.R_HEADS + n not in heads: self.bare_git.DeleteRef(name, ref_id) - def GetUploadableBranches(self, selected_branch=None): + def GetUploadableBranches( + self, + selected_branch: Optional[str] = None, + ignore_remote_commits: bool = False, + ) -> List[ReviewableBranch]: """List any branches which can be uploaded for review.""" if selected_branch: branch = self.GetBranch(selected_branch) @@ -1436,7 +1452,9 @@ pub_id = self.bare_ref.get(git_refs.R_PUB + selected_branch) if pub_id and pub_id == head_id: return [] - rb = self.GetUploadableBranch(selected_branch) + rb = self.GetUploadableBranch( + selected_branch, ignore_remote_commits=ignore_remote_commits + ) if rb: rb.branch.current = selected_branch == self.CurrentBranch return [rb] @@ -1454,18 +1472,26 @@ if branch_config.published == branch_config.revision: continue - rb = self.GetUploadableBranch(branch) + rb = self.GetUploadableBranch( + branch, ignore_remote_commits=ignore_remote_commits + ) if rb: rb.branch.current = branch_config.current ready.append(rb) return ready - def GetUploadableBranch(self, branch_name): + def GetUploadableBranch( + self, + branch_name: Optional[str], + ignore_remote_commits: bool = False, + ) -> Optional[ReviewableBranch]: """Get a single uploadable branch, or None.""" branch = self.GetBranch(branch_name) base = branch.LocalMerge if branch.LocalMerge: - rb = ReviewableBranch(self, branch, base) + rb = ReviewableBranch( + self, branch, base, ignore_remote_commits=ignore_remote_commits + ) if rb.commits: return rb return None
diff --git a/subcmds/upload.py b/subcmds/upload.py index bb4390d..2484869 100644 --- a/subcmds/upload.py +++ b/subcmds/upload.py
@@ -370,6 +370,12 @@ help="always ask about untracked files in the working copy", ) p.add_option( + "--ignore-remote-commits", + action="store_true", + default=False, + help="ignore commits already in remote branches", + ) + p.add_option( "--no-cert-checks", dest="validate_certs", action="store_false", @@ -738,10 +744,14 @@ cbr = None if opt.current_branch: cbr = project.CurrentBranch - up_branch = project.GetUploadableBranch(cbr) + up_branch = project.GetUploadableBranch( + cbr, ignore_remote_commits=opt.ignore_remote_commits + ) avail = [up_branch] if up_branch else None else: - avail = project.GetUploadableBranches(opt.branch) + avail = project.GetUploadableBranches( + opt.branch, ignore_remote_commits=opt.ignore_remote_commits + ) return (project_idx, avail, cbr) def ValidateOptions(self, opt: optparse.Values, args: List[str]) -> None:
diff --git a/tests/test_project.py b/tests/test_project.py index f65e83f..82a90a6 100644 --- a/tests/test_project.py +++ b/tests/test_project.py
@@ -137,6 +137,48 @@ self.assertTrue(rb.current) + def test_ignore_remote_commits(self) -> None: + """Check ignore_remote_commits excludes commits in refs/remotes/*.""" + with utils_for_test.TempGitTree() as tempdir: + fakeproj = FakeProject(tempdir) + + with open(os.path.join(tempdir, "readme"), "w") as fp: + fp.write("init") + fakeproj.work_git.add("readme") + fakeproj.work_git.commit("-mInit") + + # Create an upstream commit and record it under refs/remotes/. + fakeproj.work_git.checkout("-b", "upstream") + with open(os.path.join(tempdir, "upstream.txt"), "w") as fp: + fp.write("upstream") + fakeproj.work_git.add("upstream.txt") + fakeproj.work_git.commit("-mUpstream change") + fakeproj.work_git.update_ref( + "refs/remotes/upstream/main", "refs/heads/upstream" + ) + + # Merge the remote branch into work on top of main. + fakeproj.work_git.checkout("-b", "work", "main") + fakeproj.work_git.merge( + "--no-ff", "-mMerge upstream", "refs/remotes/upstream/main" + ) + + rb_default = project.ReviewableBranch( + fakeproj, fakeproj.config.GetBranch("work"), "main" + ) + self.assertEqual(2, len(rb_default.commits)) + self.assertEqual(2, len(rb_default.unabbrev_commits)) + + rb_ignored = project.ReviewableBranch( + fakeproj, + fakeproj.config.GetBranch("work"), + "main", + ignore_remote_commits=True, + ) + self.assertEqual(1, len(rb_ignored.commits)) + self.assertIn("Merge upstream", rb_ignored.commits[0]) + self.assertEqual(1, len(rb_ignored.unabbrev_commits)) + class ProjectTests(unittest.TestCase): """Check Project behavior.""" @@ -1277,7 +1319,9 @@ ) as mock_get_up: res = proj.GetUploadableBranches("feature") self.assertEqual(res, [mock_rb]) - mock_get_up.assert_called_once_with("feature") + mock_get_up.assert_called_once_with( + "feature", ignore_remote_commits=False + ) def _get_derived_subproject_url(self, submodule_url): with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir:
diff --git a/tests/test_subcmds_upload.py b/tests/test_subcmds_upload.py index 4c9008f..53cc651 100644 --- a/tests/test_subcmds_upload.py +++ b/tests/test_subcmds_upload.py
@@ -116,11 +116,46 @@ "get_parallel_context", lambda: {"projects": [project]}, ) - opt = mock.MagicMock(current_branch=True) + opt = mock.MagicMock(current_branch=True, ignore_remote_commits=False) assert upload.Upload._GatherOne(opt, 0) == (0, [branch], "topic") - project.GetUploadableBranch.assert_called_once_with("topic") + project.GetUploadableBranch.assert_called_once_with( + "topic", ignore_remote_commits=False + ) + + +def test_GatherOne_forwards_ignore_remote_commits( + cmd: upload.Upload, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """_GatherOne forwards --ignore-remote-commits to project methods.""" + project = mock.MagicMock() + project.CurrentBranch = "topic" + branch = mock.sentinel.branch + project.GetUploadableBranch.return_value = branch + project.GetUploadableBranches.return_value = [branch] + monkeypatch.setattr( + upload.Upload, + "get_parallel_context", + lambda: {"projects": [project]}, + ) + + opt_cbr, _ = cmd.OptionParser.parse_args( + ["--cbr", "--ignore-remote-commits"] + ) + assert upload.Upload._GatherOne(opt_cbr, 0) == (0, [branch], "topic") + project.GetUploadableBranch.assert_called_once_with( + "topic", ignore_remote_commits=True + ) + + opt_br, _ = cmd.OptionParser.parse_args( + ["--br", "topic", "--ignore-remote-commits"] + ) + assert upload.Upload._GatherOne(opt_br, 0) == (0, [branch], None) + project.GetUploadableBranches.assert_called_once_with( + "topic", ignore_remote_commits=True + ) def _create_mock_branch(