diff options
| -rw-r--r-- | completion.zsh | 1 | ||||
| -rw-r--r-- | man/repo-upload.1 | 5 | ||||
| -rw-r--r-- | project.py | 40 | ||||
| -rw-r--r-- | subcmds/upload.py | 14 | ||||
| -rw-r--r-- | tests/test_project.py | 46 | ||||
| -rw-r--r-- | tests/test_subcmds_upload.py | 39 |
6 files changed, 132 insertions, 13 deletions
diff --git a/completion.zsh b/completion.zsh index 2c37f4667..9d6e871d7 100644 --- a/completion.zsh +++ b/completion.zsh | |||
| @@ -411,6 +411,7 @@ _repo() { | |||
| 411 | '(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \ | 411 | '(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \ |
| 412 | '--ignore-untracked-files[Ignore untracked files]' \ | 412 | '--ignore-untracked-files[Ignore untracked files]' \ |
| 413 | '--no-ignore-untracked-files[Do not ignore untracked files]' \ | 413 | '--no-ignore-untracked-files[Do not ignore untracked files]' \ |
| 414 | '--ignore-remote-commits[Ignore commits already in remote branches]' \ | ||
| 414 | '--no-cert-checks[Do not check certificates]' \ | 415 | '--no-cert-checks[Do not check certificates]' \ |
| 415 | '--no-verify[Do not verify]' \ | 416 | '--no-verify[Do not verify]' \ |
| 416 | '--verify[Verify]' \ | 417 | '--verify[Verify]' \ |
diff --git a/man/repo-upload.1 b/man/repo-upload.1 index ad17b6987..3d7abb91e 100644 --- a/man/repo-upload.1 +++ b/man/repo-upload.1 | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man. | 1 | .\" DO NOT MODIFY THIS FILE! It was generated by help2man. |
| 2 | .TH REPO "1" "August 2026" "repo upload" "Repo Manual" | 2 | .TH REPO "1" "October 2026" "repo upload" "Repo Manual" |
| 3 | .SH NAME | 3 | .SH NAME |
| 4 | repo \- repo upload - manual page for repo upload | 4 | repo \- repo upload - manual page for repo upload |
| 5 | .SH SYNOPSIS | 5 | .SH SYNOPSIS |
| @@ -80,6 +80,9 @@ ignore untracked files in the working copy | |||
| 80 | \fB\-\-no\-ignore\-untracked\-files\fR | 80 | \fB\-\-no\-ignore\-untracked\-files\fR |
| 81 | always ask about untracked files in the working copy | 81 | always ask about untracked files in the working copy |
| 82 | .TP | 82 | .TP |
| 83 | \fB\-\-ignore\-remote\-commits\fR | ||
| 84 | ignore commits already in remote branches | ||
| 85 | .TP | ||
| 83 | \fB\-\-no\-cert\-checks\fR | 86 | \fB\-\-no\-cert\-checks\fR |
| 84 | disable verifying ssl certs (unsafe) | 87 | disable verifying ssl certs (unsafe) |
| 85 | .SS Logging options: | 88 | .SS Logging options: |
diff --git a/project.py b/project.py index 13361d1e3..4b23e8a5f 100644 --- a/project.py +++ b/project.py | |||
| @@ -36,6 +36,7 @@ from color import Coloring | |||
| 36 | import error | 36 | import error |
| 37 | import fetch | 37 | import fetch |
| 38 | import git_command | 38 | import git_command |
| 39 | from git_config import Branch | ||
| 39 | from git_config import GetSchemeFromUrl | 40 | from git_config import GetSchemeFromUrl |
| 40 | from git_config import GetUrlCookieFile | 41 | from git_config import GetUrlCookieFile |
| 41 | from git_config import GitConfig | 42 | from git_config import GitConfig |
| @@ -180,10 +181,17 @@ class ReviewableBranch: | |||
| 180 | _commit_cache = None | 181 | _commit_cache = None |
| 181 | _base_exists = None | 182 | _base_exists = None |
| 182 | 183 | ||
| 183 | def __init__(self, project, branch, base): | 184 | def __init__( |
| 185 | self, | ||
| 186 | project: "Project", | ||
| 187 | branch: Branch, | ||
| 188 | base: str, | ||
| 189 | ignore_remote_commits: bool = False, | ||
| 190 | ) -> None: | ||
| 184 | self.project = project | 191 | self.project = project |
| 185 | self.branch = branch | 192 | self.branch = branch |
| 186 | self.base = base | 193 | self.base = base |
| 194 | self.ignore_remote_commits = ignore_remote_commits | ||
| 187 | 195 | ||
| 188 | @property | 196 | @property |
| 189 | def name(self): | 197 | def name(self): |
| @@ -204,6 +212,7 @@ class ReviewableBranch: | |||
| 204 | "--date-order", | 212 | "--date-order", |
| 205 | not_rev(self.base), | 213 | not_rev(self.base), |
| 206 | git_refs.R_HEADS + self.name, | 214 | git_refs.R_HEADS + self.name, |
| 215 | *(("--not", "--remotes") if self.ignore_remote_commits else ()), | ||
| 207 | "--", | 216 | "--", |
| 208 | ) | 217 | ) |
| 209 | try: | 218 | try: |
| @@ -226,7 +235,10 @@ class ReviewableBranch: | |||
| 226 | def unabbrev_commits(self): | 235 | def unabbrev_commits(self): |
| 227 | r = {} | 236 | r = {} |
| 228 | for commit in self.project.bare_git.rev_list( | 237 | for commit in self.project.bare_git.rev_list( |
| 229 | not_rev(self.base), git_refs.R_HEADS + self.name, "--" | 238 | not_rev(self.base), |
| 239 | git_refs.R_HEADS + self.name, | ||
| 240 | *(("--not", "--remotes") if self.ignore_remote_commits else ()), | ||
| 241 | "--", | ||
| 230 | ): | 242 | ): |
| 231 | r[commit[0:8]] = commit | 243 | r[commit[0:8]] = commit |
| 232 | return r | 244 | return r |
| @@ -1424,7 +1436,11 @@ class Project: | |||
| 1424 | if git_refs.R_HEADS + n not in heads: | 1436 | if git_refs.R_HEADS + n not in heads: |
| 1425 | self.bare_git.DeleteRef(name, ref_id) | 1437 | self.bare_git.DeleteRef(name, ref_id) |
| 1426 | 1438 | ||
| 1427 | def GetUploadableBranches(self, selected_branch=None): | 1439 | def GetUploadableBranches( |
| 1440 | self, | ||
| 1441 | selected_branch: Optional[str] = None, | ||
| 1442 | ignore_remote_commits: bool = False, | ||
| 1443 | ) -> List[ReviewableBranch]: | ||
| 1428 | """List any branches which can be uploaded for review.""" | 1444 | """List any branches which can be uploaded for review.""" |
| 1429 | if selected_branch: | 1445 | if selected_branch: |
| 1430 | branch = self.GetBranch(selected_branch) | 1446 | branch = self.GetBranch(selected_branch) |
| @@ -1436,7 +1452,9 @@ class Project: | |||
| 1436 | pub_id = self.bare_ref.get(git_refs.R_PUB + selected_branch) | 1452 | pub_id = self.bare_ref.get(git_refs.R_PUB + selected_branch) |
| 1437 | if pub_id and pub_id == head_id: | 1453 | if pub_id and pub_id == head_id: |
| 1438 | return [] | 1454 | return [] |
| 1439 | rb = self.GetUploadableBranch(selected_branch) | 1455 | rb = self.GetUploadableBranch( |
| 1456 | selected_branch, ignore_remote_commits=ignore_remote_commits | ||
| 1457 | ) | ||
| 1440 | if rb: | 1458 | if rb: |
| 1441 | rb.branch.current = selected_branch == self.CurrentBranch | 1459 | rb.branch.current = selected_branch == self.CurrentBranch |
| 1442 | return [rb] | 1460 | return [rb] |
| @@ -1454,18 +1472,26 @@ class Project: | |||
| 1454 | if branch_config.published == branch_config.revision: | 1472 | if branch_config.published == branch_config.revision: |
| 1455 | continue | 1473 | continue |
| 1456 | 1474 | ||
| 1457 | rb = self.GetUploadableBranch(branch) | 1475 | rb = self.GetUploadableBranch( |
| 1476 | branch, ignore_remote_commits=ignore_remote_commits | ||
| 1477 | ) | ||
| 1458 | if rb: | 1478 | if rb: |
| 1459 | rb.branch.current = branch_config.current | 1479 | rb.branch.current = branch_config.current |
| 1460 | ready.append(rb) | 1480 | ready.append(rb) |
| 1461 | return ready | 1481 | return ready |
| 1462 | 1482 | ||
| 1463 | def GetUploadableBranch(self, branch_name): | 1483 | def GetUploadableBranch( |
| 1484 | self, | ||
| 1485 | branch_name: Optional[str], | ||
| 1486 | ignore_remote_commits: bool = False, | ||
| 1487 | ) -> Optional[ReviewableBranch]: | ||
| 1464 | """Get a single uploadable branch, or None.""" | 1488 | """Get a single uploadable branch, or None.""" |
| 1465 | branch = self.GetBranch(branch_name) | 1489 | branch = self.GetBranch(branch_name) |
| 1466 | base = branch.LocalMerge | 1490 | base = branch.LocalMerge |
| 1467 | if branch.LocalMerge: | 1491 | if branch.LocalMerge: |
| 1468 | rb = ReviewableBranch(self, branch, base) | 1492 | rb = ReviewableBranch( |
| 1493 | self, branch, base, ignore_remote_commits=ignore_remote_commits | ||
| 1494 | ) | ||
| 1469 | if rb.commits: | 1495 | if rb.commits: |
| 1470 | return rb | 1496 | return rb |
| 1471 | return None | 1497 | return None |
diff --git a/subcmds/upload.py b/subcmds/upload.py index bb4390d22..248486979 100644 --- a/subcmds/upload.py +++ b/subcmds/upload.py | |||
| @@ -370,6 +370,12 @@ Gerrit Code Review: https://www.gerritcodereview.com/ | |||
| 370 | help="always ask about untracked files in the working copy", | 370 | help="always ask about untracked files in the working copy", |
| 371 | ) | 371 | ) |
| 372 | p.add_option( | 372 | p.add_option( |
| 373 | "--ignore-remote-commits", | ||
| 374 | action="store_true", | ||
| 375 | default=False, | ||
| 376 | help="ignore commits already in remote branches", | ||
| 377 | ) | ||
| 378 | p.add_option( | ||
| 373 | "--no-cert-checks", | 379 | "--no-cert-checks", |
| 374 | dest="validate_certs", | 380 | dest="validate_certs", |
| 375 | action="store_false", | 381 | action="store_false", |
| @@ -738,10 +744,14 @@ Gerrit Code Review: https://www.gerritcodereview.com/ | |||
| 738 | cbr = None | 744 | cbr = None |
| 739 | if opt.current_branch: | 745 | if opt.current_branch: |
| 740 | cbr = project.CurrentBranch | 746 | cbr = project.CurrentBranch |
| 741 | up_branch = project.GetUploadableBranch(cbr) | 747 | up_branch = project.GetUploadableBranch( |
| 748 | cbr, ignore_remote_commits=opt.ignore_remote_commits | ||
| 749 | ) | ||
| 742 | avail = [up_branch] if up_branch else None | 750 | avail = [up_branch] if up_branch else None |
| 743 | else: | 751 | else: |
| 744 | avail = project.GetUploadableBranches(opt.branch) | 752 | avail = project.GetUploadableBranches( |
| 753 | opt.branch, ignore_remote_commits=opt.ignore_remote_commits | ||
| 754 | ) | ||
| 745 | return (project_idx, avail, cbr) | 755 | return (project_idx, avail, cbr) |
| 746 | 756 | ||
| 747 | def ValidateOptions(self, opt: optparse.Values, args: List[str]) -> None: | 757 | def ValidateOptions(self, opt: optparse.Values, args: List[str]) -> None: |
diff --git a/tests/test_project.py b/tests/test_project.py index f65e83f95..82a90a6dc 100644 --- a/tests/test_project.py +++ b/tests/test_project.py | |||
| @@ -137,6 +137,48 @@ class ReviewableBranchTests(unittest.TestCase): | |||
| 137 | 137 | ||
| 138 | self.assertTrue(rb.current) | 138 | self.assertTrue(rb.current) |
| 139 | 139 | ||
| 140 | def test_ignore_remote_commits(self) -> None: | ||
| 141 | """Check ignore_remote_commits excludes commits in refs/remotes/*.""" | ||
| 142 | with utils_for_test.TempGitTree() as tempdir: | ||
| 143 | fakeproj = FakeProject(tempdir) | ||
| 144 | |||
| 145 | with open(os.path.join(tempdir, "readme"), "w") as fp: | ||
| 146 | fp.write("init") | ||
| 147 | fakeproj.work_git.add("readme") | ||
| 148 | fakeproj.work_git.commit("-mInit") | ||
| 149 | |||
| 150 | # Create an upstream commit and record it under refs/remotes/. | ||
| 151 | fakeproj.work_git.checkout("-b", "upstream") | ||
| 152 | with open(os.path.join(tempdir, "upstream.txt"), "w") as fp: | ||
| 153 | fp.write("upstream") | ||
| 154 | fakeproj.work_git.add("upstream.txt") | ||
| 155 | fakeproj.work_git.commit("-mUpstream change") | ||
| 156 | fakeproj.work_git.update_ref( | ||
| 157 | "refs/remotes/upstream/main", "refs/heads/upstream" | ||
| 158 | ) | ||
| 159 | |||
| 160 | # Merge the remote branch into work on top of main. | ||
| 161 | fakeproj.work_git.checkout("-b", "work", "main") | ||
| 162 | fakeproj.work_git.merge( | ||
| 163 | "--no-ff", "-mMerge upstream", "refs/remotes/upstream/main" | ||
| 164 | ) | ||
| 165 | |||
| 166 | rb_default = project.ReviewableBranch( | ||
| 167 | fakeproj, fakeproj.config.GetBranch("work"), "main" | ||
| 168 | ) | ||
| 169 | self.assertEqual(2, len(rb_default.commits)) | ||
| 170 | self.assertEqual(2, len(rb_default.unabbrev_commits)) | ||
| 171 | |||
| 172 | rb_ignored = project.ReviewableBranch( | ||
| 173 | fakeproj, | ||
| 174 | fakeproj.config.GetBranch("work"), | ||
| 175 | "main", | ||
| 176 | ignore_remote_commits=True, | ||
| 177 | ) | ||
| 178 | self.assertEqual(1, len(rb_ignored.commits)) | ||
| 179 | self.assertIn("Merge upstream", rb_ignored.commits[0]) | ||
| 180 | self.assertEqual(1, len(rb_ignored.unabbrev_commits)) | ||
| 181 | |||
| 140 | 182 | ||
| 141 | class ProjectTests(unittest.TestCase): | 183 | class ProjectTests(unittest.TestCase): |
| 142 | """Check Project behavior.""" | 184 | """Check Project behavior.""" |
| @@ -1277,7 +1319,9 @@ class ProjectTests(unittest.TestCase): | |||
| 1277 | ) as mock_get_up: | 1319 | ) as mock_get_up: |
| 1278 | res = proj.GetUploadableBranches("feature") | 1320 | res = proj.GetUploadableBranches("feature") |
| 1279 | self.assertEqual(res, [mock_rb]) | 1321 | self.assertEqual(res, [mock_rb]) |
| 1280 | mock_get_up.assert_called_once_with("feature") | 1322 | mock_get_up.assert_called_once_with( |
| 1323 | "feature", ignore_remote_commits=False | ||
| 1324 | ) | ||
| 1281 | 1325 | ||
| 1282 | def _get_derived_subproject_url(self, submodule_url): | 1326 | def _get_derived_subproject_url(self, submodule_url): |
| 1283 | with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir: | 1327 | with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir: |
diff --git a/tests/test_subcmds_upload.py b/tests/test_subcmds_upload.py index 4c9008f55..53cc651a1 100644 --- a/tests/test_subcmds_upload.py +++ b/tests/test_subcmds_upload.py | |||
| @@ -116,11 +116,46 @@ def test_GatherOne_returns_resolved_current_branch( | |||
| 116 | "get_parallel_context", | 116 | "get_parallel_context", |
| 117 | lambda: {"projects": [project]}, | 117 | lambda: {"projects": [project]}, |
| 118 | ) | 118 | ) |
| 119 | opt = mock.MagicMock(current_branch=True) | 119 | opt = mock.MagicMock(current_branch=True, ignore_remote_commits=False) |
| 120 | 120 | ||
| 121 | assert upload.Upload._GatherOne(opt, 0) == (0, [branch], "topic") | 121 | assert upload.Upload._GatherOne(opt, 0) == (0, [branch], "topic") |
| 122 | 122 | ||
| 123 | project.GetUploadableBranch.assert_called_once_with("topic") | 123 | project.GetUploadableBranch.assert_called_once_with( |
| 124 | "topic", ignore_remote_commits=False | ||
| 125 | ) | ||
| 126 | |||
| 127 | |||
| 128 | def test_GatherOne_forwards_ignore_remote_commits( | ||
| 129 | cmd: upload.Upload, | ||
| 130 | monkeypatch: pytest.MonkeyPatch, | ||
| 131 | ) -> None: | ||
| 132 | """_GatherOne forwards --ignore-remote-commits to project methods.""" | ||
| 133 | project = mock.MagicMock() | ||
| 134 | project.CurrentBranch = "topic" | ||
| 135 | branch = mock.sentinel.branch | ||
| 136 | project.GetUploadableBranch.return_value = branch | ||
| 137 | project.GetUploadableBranches.return_value = [branch] | ||
| 138 | monkeypatch.setattr( | ||
| 139 | upload.Upload, | ||
| 140 | "get_parallel_context", | ||
| 141 | lambda: {"projects": [project]}, | ||
| 142 | ) | ||
| 143 | |||
| 144 | opt_cbr, _ = cmd.OptionParser.parse_args( | ||
| 145 | ["--cbr", "--ignore-remote-commits"] | ||
| 146 | ) | ||
| 147 | assert upload.Upload._GatherOne(opt_cbr, 0) == (0, [branch], "topic") | ||
| 148 | project.GetUploadableBranch.assert_called_once_with( | ||
| 149 | "topic", ignore_remote_commits=True | ||
| 150 | ) | ||
| 151 | |||
| 152 | opt_br, _ = cmd.OptionParser.parse_args( | ||
| 153 | ["--br", "topic", "--ignore-remote-commits"] | ||
| 154 | ) | ||
| 155 | assert upload.Upload._GatherOne(opt_br, 0) == (0, [branch], None) | ||
| 156 | project.GetUploadableBranches.assert_called_once_with( | ||
| 157 | "topic", ignore_remote_commits=True | ||
| 158 | ) | ||
| 124 | 159 | ||
| 125 | 160 | ||
| 126 | def _create_mock_branch( | 161 | def _create_mock_branch( |
