summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--completion.zsh1
-rw-r--r--man/repo-upload.15
-rw-r--r--project.py40
-rw-r--r--subcmds/upload.py14
-rw-r--r--tests/test_project.py46
-rw-r--r--tests/test_subcmds_upload.py39
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
4repo \- repo upload - manual page for repo upload 4repo \- 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
81always ask about untracked files in the working copy 81always ask about untracked files in the working copy
82.TP 82.TP
83\fB\-\-ignore\-remote\-commits\fR
84ignore commits already in remote branches
85.TP
83\fB\-\-no\-cert\-checks\fR 86\fB\-\-no\-cert\-checks\fR
84disable verifying ssl certs (unsafe) 87disable 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
36import error 36import error
37import fetch 37import fetch
38import git_command 38import git_command
39from git_config import Branch
39from git_config import GetSchemeFromUrl 40from git_config import GetSchemeFromUrl
40from git_config import GetUrlCookieFile 41from git_config import GetUrlCookieFile
41from git_config import GitConfig 42from 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
141class ProjectTests(unittest.TestCase): 183class 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
128def 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
126def _create_mock_branch( 161def _create_mock_branch(