diff options
| author | Gavin Mak <gavinmak@google.com> | 2026-08-04 20:13:19 +0000 |
|---|---|---|
| committer | gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2026-08-04 15:00:19 -0700 |
| commit | d9da609d8c120bb882a43196a4a6b7f183418304 (patch) | |
| tree | 201579df7c4462b3a603448ed2bcc65a731e5162 | |
| parent | 4bec297eb6e5f30ea2fe2a2d8a92054cafa30e4f (diff) | |
| download | git-repo-2.66.tar.gz | |
Avoid disabling --current-branch when syncing a SHA-1 revision without
an explicit project upstream (e.g., smart tags). Resolve a fallback
upstream from dest-branch or manifest defaults so -c only fetches the
target branch.
Bug: 541240657
Change-Id: Ib44b6a732131210e1ec3a3136747d1a19bc5aa18
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/614762
Reviewed-by: Brian Gan <brgan@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
| -rw-r--r-- | project.py | 51 | ||||
| -rw-r--r-- | tests/test_project.py | 129 |
2 files changed, 161 insertions, 19 deletions
diff --git a/project.py b/project.py index 45860d228..7186e58ad 100644 --- a/project.py +++ b/project.py | |||
| @@ -2857,6 +2857,18 @@ class Project: | |||
| 2857 | 2857 | ||
| 2858 | return True | 2858 | return True |
| 2859 | 2859 | ||
| 2860 | def _GetUpstreamFallback(self) -> Optional[str]: | ||
| 2861 | """Resolve a fallback upstream ref when revisionExpr is a SHA-1.""" | ||
| 2862 | for cand in ( | ||
| 2863 | self.dest_branch, | ||
| 2864 | self.manifest.default.upstreamExpr, | ||
| 2865 | self.manifest.default.destBranchExpr, | ||
| 2866 | self.manifest.default.revisionExpr, | ||
| 2867 | ): | ||
| 2868 | if cand and not IsId(cand): | ||
| 2869 | return cand | ||
| 2870 | return None | ||
| 2871 | |||
| 2860 | def _RemoteFetch( | 2872 | def _RemoteFetch( |
| 2861 | self, | 2873 | self, |
| 2862 | name=None, | 2874 | name=None, |
| @@ -2890,14 +2902,31 @@ class Project: | |||
| 2890 | current_branch_only = True | 2902 | current_branch_only = True |
| 2891 | 2903 | ||
| 2892 | is_sha1 = IsId(self.revisionExpr) | 2904 | is_sha1 = IsId(self.revisionExpr) |
| 2905 | upstream = self.upstream | ||
| 2893 | 2906 | ||
| 2894 | if current_branch_only: | 2907 | if current_branch_only: |
| 2908 | if is_sha1 and not depth: | ||
| 2909 | # When syncing a specific commit and --depth is not set: | ||
| 2910 | # * if upstream is explicitly specified and is not a sha1, fetch | ||
| 2911 | # only upstream as users expect only upstream to be fetch. | ||
| 2912 | # Note: The commit might not be in upstream in which case the | ||
| 2913 | # sync will fail. | ||
| 2914 | # * otherwise, fetch all branches to make sure we end up with | ||
| 2915 | # the specific commit. | ||
| 2916 | if not upstream: | ||
| 2917 | upstream = self._GetUpstreamFallback() | ||
| 2918 | |||
| 2919 | if upstream: | ||
| 2920 | current_branch_only = not IsId(upstream) | ||
| 2921 | else: | ||
| 2922 | current_branch_only = False | ||
| 2923 | |||
| 2895 | if self.revisionExpr.startswith(R_TAGS): | 2924 | if self.revisionExpr.startswith(R_TAGS): |
| 2896 | # This is a tag and its commit id should never change. | 2925 | # This is a tag and its commit id should never change. |
| 2897 | tag_name = self.revisionExpr[len(R_TAGS) :] | 2926 | tag_name = self.revisionExpr[len(R_TAGS) :] |
| 2898 | elif self.upstream and self.upstream.startswith(R_TAGS): | 2927 | elif upstream and upstream.startswith(R_TAGS): |
| 2899 | # This is a tag and its commit id should never change. | 2928 | # This is a tag and its commit id should never change. |
| 2900 | tag_name = self.upstream[len(R_TAGS) :] | 2929 | tag_name = upstream[len(R_TAGS) :] |
| 2901 | 2930 | ||
| 2902 | if is_sha1 or tag_name is not None: | 2931 | if is_sha1 or tag_name is not None: |
| 2903 | has_shallow = os.path.exists( | 2932 | has_shallow = os.path.exists( |
| @@ -2915,18 +2944,6 @@ class Project: | |||
| 2915 | "persistent ref)" % self.name | 2944 | "persistent ref)" % self.name |
| 2916 | ) | 2945 | ) |
| 2917 | return True | 2946 | return True |
| 2918 | if is_sha1 and not depth: | ||
| 2919 | # When syncing a specific commit and --depth is not set: | ||
| 2920 | # * if upstream is explicitly specified and is not a sha1, fetch | ||
| 2921 | # only upstream as users expect only upstream to be fetch. | ||
| 2922 | # Note: The commit might not be in upstream in which case the | ||
| 2923 | # sync will fail. | ||
| 2924 | # * otherwise, fetch all branches to make sure we end up with | ||
| 2925 | # the specific commit. | ||
| 2926 | if self.upstream: | ||
| 2927 | current_branch_only = not IsId(self.upstream) | ||
| 2928 | else: | ||
| 2929 | current_branch_only = False | ||
| 2930 | 2947 | ||
| 2931 | if not name: | 2948 | if not name: |
| 2932 | name = self.remote.name | 2949 | name = self.remote.name |
| @@ -3038,11 +3055,11 @@ class Project: | |||
| 3038 | # Shallow checkout of a specific commit, fetch from that commit and | 3055 | # Shallow checkout of a specific commit, fetch from that commit and |
| 3039 | # not the heads only as the commit might be deeper in the history. | 3056 | # not the heads only as the commit might be deeper in the history. |
| 3040 | spec.append(branch) | 3057 | spec.append(branch) |
| 3041 | if self.upstream: | 3058 | if upstream: |
| 3042 | spec.append(self.upstream) | 3059 | spec.append(upstream) |
| 3043 | else: | 3060 | else: |
| 3044 | if is_sha1: | 3061 | if is_sha1: |
| 3045 | branch = self.upstream | 3062 | branch = upstream |
| 3046 | if branch is not None and branch.strip(): | 3063 | if branch is not None and branch.strip(): |
| 3047 | if not branch.startswith("refs/"): | 3064 | if not branch.startswith("refs/"): |
| 3048 | branch = R_HEADS + branch | 3065 | branch = R_HEADS + branch |
diff --git a/tests/test_project.py b/tests/test_project.py index 342ef6b8a..19da4f09e 100644 --- a/tests/test_project.py +++ b/tests/test_project.py | |||
| @@ -1045,13 +1045,20 @@ class StatelessSyncTests(unittest.TestCase): | |||
| 1045 | class SyncOptimizationTests(unittest.TestCase): | 1045 | class SyncOptimizationTests(unittest.TestCase): |
| 1046 | """Tests for sync optimization logic involving shallow clones.""" | 1046 | """Tests for sync optimization logic involving shallow clones.""" |
| 1047 | 1047 | ||
| 1048 | def _get_project(self, tempdir, depth=None): | 1048 | def _get_project( |
| 1049 | self, | ||
| 1050 | tempdir: str, | ||
| 1051 | depth: Optional[int] = None, | ||
| 1052 | revisionExpr: Optional[str] = None, | ||
| 1053 | ) -> project.Project: | ||
| 1054 | if revisionExpr is None: | ||
| 1055 | revisionExpr = "0123456789abcdef0123456789abcdef01234567" | ||
| 1049 | proj = _create_mock_project( | 1056 | proj = _create_mock_project( |
| 1050 | tempdir, | 1057 | tempdir, |
| 1051 | depth=depth, | 1058 | depth=depth, |
| 1052 | gitdir=os.path.join(tempdir, "gitdir"), | 1059 | gitdir=os.path.join(tempdir, "gitdir"), |
| 1053 | objdir=os.path.join(tempdir, "objdir"), | 1060 | objdir=os.path.join(tempdir, "objdir"), |
| 1054 | revisionExpr="0123456789abcdef0123456789abcdef01234567", | 1061 | revisionExpr=revisionExpr, |
| 1055 | ) | 1062 | ) |
| 1056 | proj._CheckForImmutableRevision = mock.MagicMock(return_value=True) | 1063 | proj._CheckForImmutableRevision = mock.MagicMock(return_value=True) |
| 1057 | proj.DeleteWorktree = mock.MagicMock() | 1064 | proj.DeleteWorktree = mock.MagicMock() |
| @@ -1278,6 +1285,124 @@ class SyncOptimizationTests(unittest.TestCase): | |||
| 1278 | self.assertTrue(res) | 1285 | self.assertTrue(res) |
| 1279 | mock_git_cmd.assert_not_called() | 1286 | mock_git_cmd.assert_not_called() |
| 1280 | 1287 | ||
| 1288 | def test_remote_fetch_sha1_upstream_fallback(self) -> None: | ||
| 1289 | """Test _RemoteFetch resolves upstream fallback for SHA-1 revisions.""" | ||
| 1290 | sha = "4f8a3c0000000000000000000000000000000000" | ||
| 1291 | with utils_for_test.TempGitTree() as tempdir: | ||
| 1292 | proj = self._get_project(tempdir, revisionExpr=sha) | ||
| 1293 | proj._CheckForImmutableRevision.side_effect = [False, True] | ||
| 1294 | proj.upstream = None | ||
| 1295 | proj.dest_branch = "my-dest-branch" | ||
| 1296 | |||
| 1297 | mock_remote = mock.MagicMock() | ||
| 1298 | mock_remote.name = "origin" | ||
| 1299 | |||
| 1300 | def _to_local(r: str) -> str: | ||
| 1301 | if r.startswith("refs/heads/"): | ||
| 1302 | return "refs/remotes/origin/" + r[11:] | ||
| 1303 | return r | ||
| 1304 | |||
| 1305 | mock_remote.ToLocal.side_effect = _to_local | ||
| 1306 | mock_remote.PreConnectFetch.return_value = True | ||
| 1307 | proj.GetRemote = mock.MagicMock(return_value=mock_remote) | ||
| 1308 | |||
| 1309 | with mock.patch("project.GitCommand") as mock_git_cmd: | ||
| 1310 | mock_cmd_instance = mock.MagicMock() | ||
| 1311 | mock_cmd_instance.Wait.return_value = 0 | ||
| 1312 | mock_git_cmd.return_value = mock_cmd_instance | ||
| 1313 | |||
| 1314 | res = proj._RemoteFetch(current_branch_only=True) | ||
| 1315 | |||
| 1316 | self.assertTrue(res) | ||
| 1317 | mock_git_cmd.assert_called_once() | ||
| 1318 | cmd_args = mock_git_cmd.call_args[0][1] | ||
| 1319 | self.assertIn( | ||
| 1320 | "+refs/heads/my-dest-branch:" | ||
| 1321 | "refs/remotes/origin/my-dest-branch", | ||
| 1322 | cmd_args, | ||
| 1323 | ) | ||
| 1324 | self.assertNotIn( | ||
| 1325 | "+refs/heads/*:refs/remotes/origin/*", cmd_args | ||
| 1326 | ) | ||
| 1327 | |||
| 1328 | def test_remote_fetch_sha1_manifest_default_fallback(self) -> None: | ||
| 1329 | """Test _RemoteFetch upstream fallback from manifest defaults.""" | ||
| 1330 | sha = "4f8a3c0000000000000000000000000000000000" | ||
| 1331 | with utils_for_test.TempGitTree() as tempdir: | ||
| 1332 | proj = self._get_project(tempdir, revisionExpr=sha) | ||
| 1333 | proj._CheckForImmutableRevision.side_effect = [False, True] | ||
| 1334 | proj.upstream = None | ||
| 1335 | proj.dest_branch = None | ||
| 1336 | proj.manifest.default.upstreamExpr = "manifest-upstream" | ||
| 1337 | |||
| 1338 | mock_remote = mock.MagicMock() | ||
| 1339 | mock_remote.name = "origin" | ||
| 1340 | |||
| 1341 | def _to_local(r: str) -> str: | ||
| 1342 | if r.startswith("refs/heads/"): | ||
| 1343 | return "refs/remotes/origin/" + r[11:] | ||
| 1344 | return r | ||
| 1345 | |||
| 1346 | mock_remote.ToLocal.side_effect = _to_local | ||
| 1347 | mock_remote.PreConnectFetch.return_value = True | ||
| 1348 | proj.GetRemote = mock.MagicMock(return_value=mock_remote) | ||
| 1349 | |||
| 1350 | with mock.patch("project.GitCommand") as mock_git_cmd: | ||
| 1351 | mock_cmd_instance = mock.MagicMock() | ||
| 1352 | mock_cmd_instance.Wait.return_value = 0 | ||
| 1353 | mock_git_cmd.return_value = mock_cmd_instance | ||
| 1354 | |||
| 1355 | res = proj._RemoteFetch(current_branch_only=True) | ||
| 1356 | |||
| 1357 | self.assertTrue(res) | ||
| 1358 | mock_git_cmd.assert_called_once() | ||
| 1359 | cmd_args = mock_git_cmd.call_args[0][1] | ||
| 1360 | self.assertIn( | ||
| 1361 | "+refs/heads/manifest-upstream:" | ||
| 1362 | "refs/remotes/origin/manifest-upstream", | ||
| 1363 | cmd_args, | ||
| 1364 | ) | ||
| 1365 | self.assertNotIn( | ||
| 1366 | "+refs/heads/*:refs/remotes/origin/*", cmd_args | ||
| 1367 | ) | ||
| 1368 | |||
| 1369 | def test_remote_fetch_sha1_tag_fallback(self) -> None: | ||
| 1370 | """Test _RemoteFetch resolves upstream fallback to tag correctly.""" | ||
| 1371 | sha = "4f8a3c0000000000000000000000000000000000" | ||
| 1372 | with utils_for_test.TempGitTree() as tempdir: | ||
| 1373 | proj = self._get_project(tempdir, revisionExpr=sha) | ||
| 1374 | proj._CheckForImmutableRevision.side_effect = [False, True] | ||
| 1375 | proj.upstream = None | ||
| 1376 | proj.dest_branch = "refs/tags/v1.0" | ||
| 1377 | |||
| 1378 | mock_remote = mock.MagicMock() | ||
| 1379 | mock_remote.name = "origin" | ||
| 1380 | |||
| 1381 | def _to_local(r: str) -> str: | ||
| 1382 | if r.startswith("refs/tags/"): | ||
| 1383 | return "refs/tags/" + r[10:] | ||
| 1384 | return r | ||
| 1385 | |||
| 1386 | mock_remote.ToLocal.side_effect = _to_local | ||
| 1387 | mock_remote.PreConnectFetch.return_value = True | ||
| 1388 | proj.GetRemote = mock.MagicMock(return_value=mock_remote) | ||
| 1389 | |||
| 1390 | with mock.patch("project.GitCommand") as mock_git_cmd: | ||
| 1391 | mock_cmd_instance = mock.MagicMock() | ||
| 1392 | mock_cmd_instance.Wait.return_value = 0 | ||
| 1393 | mock_git_cmd.return_value = mock_cmd_instance | ||
| 1394 | |||
| 1395 | res = proj._RemoteFetch(current_branch_only=True) | ||
| 1396 | |||
| 1397 | self.assertTrue(res) | ||
| 1398 | mock_git_cmd.assert_called_once() | ||
| 1399 | cmd_args = mock_git_cmd.call_args[0][1] | ||
| 1400 | self.assertIn("tag", cmd_args) | ||
| 1401 | self.assertIn("v1.0", cmd_args) | ||
| 1402 | self.assertNotIn( | ||
| 1403 | "+refs/heads/*:refs/remotes/origin/*", cmd_args | ||
| 1404 | ) | ||
| 1405 | |||
| 1281 | 1406 | ||
| 1282 | class GetEnvVarsTests(unittest.TestCase): | 1407 | class GetEnvVarsTests(unittest.TestCase): |
| 1283 | """Tests for GetEnvVars project environment variable generation.""" | 1408 | """Tests for GetEnvVars project environment variable generation.""" |
