summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGavin Mak <gavinmak@google.com>2026-08-04 20:13:19 +0000
committergerrit-scoped@luci-project-accounts.iam.gserviceaccount.com <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2026-08-04 15:00:19 -0700
commitd9da609d8c120bb882a43196a4a6b7f183418304 (patch)
tree201579df7c4462b3a603448ed2bcc65a731e5162
parent4bec297eb6e5f30ea2fe2a2d8a92054cafa30e4f (diff)
downloadgit-repo-2.66.tar.gz
project: preserve -c optimization when revision is a SHA-1v2.66stablemain
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.py51
-rw-r--r--tests/test_project.py129
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):
1045class SyncOptimizationTests(unittest.TestCase): 1045class 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
1282class GetEnvVarsTests(unittest.TestCase): 1407class GetEnvVarsTests(unittest.TestCase):
1283 """Tests for GetEnvVars project environment variable generation.""" 1408 """Tests for GetEnvVars project environment variable generation."""