summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--git_superproject.py16
-rw-r--r--man/repo-smartsync.16
-rw-r--r--man/repo-sync.16
-rw-r--r--subcmds/sync.py85
-rw-r--r--tests/test_subcmds_sync.py168
5 files changed, 271 insertions, 10 deletions
diff --git a/git_superproject.py b/git_superproject.py
index 81a6b2e59..27bc10e10 100644
--- a/git_superproject.py
+++ b/git_superproject.py
@@ -34,6 +34,7 @@ import urllib.parse
34 34
35from git_command import git_require 35from git_command import git_require
36from git_command import GitCommand 36from git_command import GitCommand
37from git_config import IsId
37from git_config import RepoConfig 38from git_config import RepoConfig
38from git_refs import GitRefs 39from git_refs import GitRefs
39import platform_utils 40import platform_utils
@@ -132,6 +133,10 @@ class Superproject:
132 """Set the _print_messages attribute.""" 133 """Set the _print_messages attribute."""
133 self._print_messages = value 134 self._print_messages = value
134 135
136 def SetRevisionId(self, revision_id: str) -> None:
137 """Set the revisionId of the superproject to sync to."""
138 self.revision = revision_id
139
135 @property 140 @property
136 def commit_id(self): 141 def commit_id(self):
137 """Returns the commit ID of the superproject checkout.""" 142 """Returns the commit ID of the superproject checkout."""
@@ -314,7 +319,14 @@ class Superproject:
314 cmd.extend(["--negotiation-tip", rev_commit]) 319 cmd.extend(["--negotiation-tip", rev_commit])
315 320
316 if self.revision: 321 if self.revision:
317 cmd += [self.revision + ":" + self.revision] 322 # If revision is a commit hash, fetch it directly to avoid
323 # creating a local branch of the same name.
324 refspec = (
325 self.revision
326 if IsId(self.revision)
327 else f"{self.revision}:{self.revision}"
328 )
329 cmd.append(refspec)
318 p = GitCommand( 330 p = GitCommand(
319 None, 331 None,
320 cmd, 332 cmd,
@@ -401,6 +413,8 @@ class Superproject:
401 413
402 if not self._Init(): 414 if not self._Init():
403 return SyncResult(False, should_exit) 415 return SyncResult(False, should_exit)
416 if IsId(self.revision) and self.commit_id:
417 return SyncResult(True, False)
404 if not self._Fetch(): 418 if not self._Fetch():
405 return SyncResult(False, should_exit) 419 return SyncResult(False, should_exit)
406 if not self._quiet: 420 if not self._quiet:
diff --git a/man/repo-smartsync.1 b/man/repo-smartsync.1
index 5ea063e6b..25b85f126 100644
--- a/man/repo-smartsync.1
+++ b/man/repo-smartsync.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 2025" "repo smartsync" "Repo Manual" 2.TH REPO "1" "May 2026" "repo smartsync" "Repo Manual"
3.SH NAME 3.SH NAME
4repo \- repo smartsync - manual page for repo smartsync 4repo \- repo smartsync - manual page for repo smartsync
5.SH SYNOPSIS 5.SH SYNOPSIS
@@ -101,6 +101,10 @@ implies \fB\-c\fR
101\fB\-\-no\-use\-superproject\fR 101\fB\-\-no\-use\-superproject\fR
102disable use of manifest superprojects 102disable use of manifest superprojects
103.TP 103.TP
104\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
105sync to superproject revision (applies to outer
106manifest)
107.TP
104\fB\-\-tags\fR 108\fB\-\-tags\fR
105fetch tags 109fetch tags
106.TP 110.TP
diff --git a/man/repo-sync.1 b/man/repo-sync.1
index 8f145a09e..4e115e200 100644
--- a/man/repo-sync.1
+++ b/man/repo-sync.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 2025" "repo sync" "Repo Manual" 2.TH REPO "1" "May 2026" "repo sync" "Repo Manual"
3.SH NAME 3.SH NAME
4repo \- repo sync - manual page for repo sync 4repo \- repo sync - manual page for repo sync
5.SH SYNOPSIS 5.SH SYNOPSIS
@@ -101,6 +101,10 @@ implies \fB\-c\fR
101\fB\-\-no\-use\-superproject\fR 101\fB\-\-no\-use\-superproject\fR
102disable use of manifest superprojects 102disable use of manifest superprojects
103.TP 103.TP
104\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
105sync to superproject revision (applies to outer
106manifest)
107.TP
104\fB\-\-tags\fR 108\fB\-\-tags\fR
105fetch tags 109fetch tags
106.TP 110.TP
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 8c2591180..7e0e7418c 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -64,6 +64,7 @@ from error import SyncError
64from error import UpdateManifestError 64from error import UpdateManifestError
65import event_log 65import event_log
66from git_command import git_require 66from git_command import git_require
67from git_command import GitCommand
67from git_config import GetUrlCookieFile 68from git_config import GetUrlCookieFile
68from git_refs import HEAD 69from git_refs import HEAD
69from git_refs import R_HEADS 70from git_refs import R_HEADS
@@ -565,6 +566,11 @@ later is required to fix a server side protocol bug.
565 dest="use_superproject", 566 dest="use_superproject",
566 help="disable use of manifest superprojects", 567 help="disable use of manifest superprojects",
567 ) 568 )
569 p.add_option(
570 "--superproject-revision",
571 action="store",
572 help="sync to superproject revision (applies to outer manifest)",
573 )
568 p.add_option("--tags", action="store_true", help="fetch tags") 574 p.add_option("--tags", action="store_true", help="fetch tags")
569 p.add_option( 575 p.add_option(
570 "--no-tags", 576 "--no-tags",
@@ -668,6 +674,24 @@ later is required to fix a server side protocol bug.
668 or opt.current_branch_only 674 or opt.current_branch_only
669 ) 675 )
670 676
677 def _ConfigureSuperproject(
678 self,
679 opt: optparse.Values,
680 manifest,
681 revision: Optional[str] = None,
682 ) -> bool:
683 """Configure superproject with options."""
684 if not manifest.superproject:
685 return False
686 manifest.superproject.SetQuiet(not opt.verbose)
687 print_messages = git_superproject.PrintMessages(
688 opt.use_superproject, manifest
689 )
690 manifest.superproject.SetPrintMessages(print_messages)
691 if revision:
692 manifest.superproject.SetRevisionId(revision)
693 return print_messages
694
671 def _UpdateProjectsRevisionId( 695 def _UpdateProjectsRevisionId(
672 self, opt, args, superproject_logging_data, manifest 696 self, opt, args, superproject_logging_data, manifest
673 ): 697 ):
@@ -741,11 +765,7 @@ later is required to fix a server side protocol bug.
741 765
742 if not use_super: 766 if not use_super:
743 continue 767 continue
744 m.superproject.SetQuiet(not opt.verbose) 768 print_messages = self._ConfigureSuperproject(opt, m)
745 print_messages = git_superproject.PrintMessages(
746 opt.use_superproject, m
747 )
748 m.superproject.SetPrintMessages(print_messages)
749 update_result = m.superproject.UpdateProjectsRevisionId( 769 update_result = m.superproject.UpdateProjectsRevisionId(
750 per_manifest[m.path_prefix], git_event_log=self.git_event_log 770 per_manifest[m.path_prefix], git_event_log=self.git_event_log
751 ) 771 )
@@ -1832,7 +1852,11 @@ later is required to fix a server side protocol bug.
1832 mp: the manifestProject to query. 1852 mp: the manifestProject to query.
1833 manifest_name: Manifest file to be reloaded. 1853 manifest_name: Manifest file to be reloaded.
1834 """ 1854 """
1835 if not mp.standalone_manifest_url: 1855 if opt.superproject_revision and mp.manifest == self.outer_manifest:
1856 self._SyncToSuperprojectRev(
1857 opt, mp.manifest, mp, manifest_name, errors
1858 )
1859 elif not mp.standalone_manifest_url:
1836 self._UpdateManifestProject(opt, mp, manifest_name, errors) 1860 self._UpdateManifestProject(opt, mp, manifest_name, errors)
1837 1861
1838 if mp.manifest.submanifests: 1862 if mp.manifest.submanifests:
@@ -2041,6 +2065,53 @@ later is required to fix a server side protocol bug.
2041 if not success: 2065 if not success:
2042 print("Warning: post-sync hook reported failure.") 2066 print("Warning: post-sync hook reported failure.")
2043 2067
2068 def _SyncToSuperprojectRev(
2069 self,
2070 opt: optparse.Values,
2071 manifest,
2072 mp: Project,
2073 manifest_name: Optional[str],
2074 errors: List[Exception],
2075 ) -> None:
2076 """Sync to a specific superproject commit."""
2077 if not manifest.superproject:
2078 raise SyncError("superproject not defined in manifest")
2079
2080 self._ConfigureSuperproject(
2081 opt, manifest, revision=opt.superproject_revision
2082 )
2083
2084 sync_result = manifest.superproject.Sync(self.git_event_log)
2085 if not sync_result.success:
2086 raise SyncError("failed to sync superproject")
2087
2088 cmd = ["show", f"{opt.superproject_revision}:.supermanifest"]
2089 p = GitCommand(
2090 None,
2091 cmd,
2092 gitdir=manifest.superproject._work_git,
2093 bare=True,
2094 capture_stdout=True,
2095 capture_stderr=True,
2096 )
2097 if p.Wait() != 0:
2098 raise SyncError(
2099 f"failed to read .supermanifest from superproject: {p.stderr}"
2100 )
2101
2102 try:
2103 _, _, manifest_commit = p.stdout.strip().split()
2104 except ValueError:
2105 raise SyncError("could not parse .supermanifest")
2106
2107 mp.SetRevision(manifest_commit)
2108 try:
2109 self._UpdateManifestProject(opt, mp, manifest_name, errors)
2110 except UpdateManifestError as e:
2111 raise SyncError(
2112 "failed to sync manifest project", aggregate_errors=[e]
2113 )
2114
2044 def _ExecuteHelper(self, opt, args, errors): 2115 def _ExecuteHelper(self, opt, args, errors):
2045 manifest = self.outer_manifest 2116 manifest = self.outer_manifest
2046 if not opt.outer_manifest: 2117 if not opt.outer_manifest:
@@ -2099,7 +2170,7 @@ later is required to fix a server side protocol bug.
2099 ): 2170 ):
2100 mp.ConfigureCloneFilterForDepth("blob:none") 2171 mp.ConfigureCloneFilterForDepth("blob:none")
2101 2172
2102 if opt.mp_update: 2173 if opt.mp_update or opt.superproject_revision:
2103 self._UpdateAllManifestProjects(opt, mp, manifest_name, errors) 2174 self._UpdateAllManifestProjects(opt, mp, manifest_name, errors)
2104 else: 2175 else:
2105 print("Skipping update of local manifest project.") 2176 print("Skipping update of local manifest project.")
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py
index ef162392b..785c0e629 100644
--- a/tests/test_subcmds_sync.py
+++ b/tests/test_subcmds_sync.py
@@ -1286,3 +1286,171 @@ class UpdateCopyLinkfileListTest(unittest.TestCase):
1286 self.assertFalse(os.path.lexists(os.path.join(llms_dir, "rules"))) 1286 self.assertFalse(os.path.lexists(os.path.join(llms_dir, "rules")))
1287 self.assertTrue(os.path.exists(os.path.join(llms_dir, "my-notes.txt"))) 1287 self.assertTrue(os.path.exists(os.path.join(llms_dir, "my-notes.txt")))
1288 self.assertTrue(os.path.isdir(llms_dir)) 1288 self.assertTrue(os.path.isdir(llms_dir))
1289
1290
1291class SyncToSuperprojectRevTests(unittest.TestCase):
1292 """Tests for Sync._SyncToSuperprojectRev."""
1293
1294 def setUp(self):
1295 self.repodir = tempfile.mkdtemp(".repo")
1296 self.manifest = mock.MagicMock(repodir=self.repodir)
1297 self.manifest.superproject = mock.MagicMock()
1298 self.manifest.path_prefix = ""
1299
1300 self.mp = mock.MagicMock()
1301 self.cmd = sync.Sync(manifest=self.manifest)
1302 self.cmd.outer_manifest = self.manifest
1303
1304 self.opt = mock.Mock()
1305 self.opt.verbose = False
1306 self.opt.superproject_revision = "deadbeef"
1307 self.opt.mp_update = True
1308
1309 self.errors = []
1310
1311 def tearDown(self):
1312 shutil.rmtree(self.repodir)
1313
1314 @mock.patch("subcmds.sync.GitCommand")
1315 def test_successful_sync(self, mock_git_command):
1316 """Test successful sync to superproject rev."""
1317 mock_superproject = self.manifest.superproject
1318 mock_superproject.Sync.return_value = mock.Mock(success=True)
1319
1320 mock_git = mock.Mock()
1321 mock_git.Wait.return_value = 0
1322 mock_git.stdout = "proj branch manifest_commit_hash\n"
1323 mock_git_command.return_value = mock_git
1324
1325 with mock.patch.object(
1326 self.cmd, "_UpdateManifestProject"
1327 ) as mock_update:
1328 self.cmd._SyncToSuperprojectRev(
1329 self.opt, self.manifest, self.mp, "name", self.errors
1330 )
1331
1332 mock_superproject.SetRevisionId.assert_called_with("deadbeef")
1333 mock_superproject.Sync.assert_called_once()
1334 mock_git_command.assert_called_once()
1335 self.mp.SetRevision.assert_called_with("manifest_commit_hash")
1336 mock_update.assert_called_once()
1337 self.assertEqual(self.errors, [])
1338
1339 @mock.patch("subcmds.sync.GitCommand")
1340 def test_parse_error(self, mock_git_command):
1341 """Test error when .supermanifest cannot be parsed."""
1342 mock_superproject = self.manifest.superproject
1343 mock_superproject.Sync.return_value = mock.Mock(success=True)
1344
1345 mock_git = mock.Mock()
1346 mock_git.Wait.return_value = 0
1347 # Invalid format (not 3 parts)
1348 mock_git.stdout = "invalid_content\n"
1349 mock_git_command.return_value = mock_git
1350
1351 with self.assertRaises(sync.SyncError) as e:
1352 self.cmd._SyncToSuperprojectRev(
1353 self.opt, self.manifest, self.mp, "name", self.errors
1354 )
1355 self.assertIn("could not parse .supermanifest", str(e.exception))
1356
1357 @mock.patch("subcmds.sync.GitCommand")
1358 def test_read_error(self, mock_git_command):
1359 """Test error when reading .supermanifest fails."""
1360 mock_superproject = self.manifest.superproject
1361 mock_superproject.Sync.return_value = mock.Mock(success=True)
1362
1363 mock_git = mock.Mock()
1364 mock_git.Wait.return_value = 1
1365 mock_git.stderr = "git error"
1366 mock_git_command.return_value = mock_git
1367
1368 with self.assertRaises(sync.SyncError) as e:
1369 self.cmd._SyncToSuperprojectRev(
1370 self.opt, self.manifest, self.mp, "name", self.errors
1371 )
1372 self.assertIn("failed to read .supermanifest", str(e.exception))
1373
1374 def test_no_superproject(self):
1375 """Test error when superproject is not defined."""
1376 self.manifest.superproject = None
1377
1378 with self.assertRaises(sync.SyncError) as e:
1379 self.cmd._SyncToSuperprojectRev(
1380 self.opt, self.manifest, self.mp, "name", self.errors
1381 )
1382 self.assertIn("superproject not defined", str(e.exception))
1383
1384 @mock.patch("subcmds.sync.GitCommand")
1385 def test_sync_failure(self, mock_git_command):
1386 """Test error when superproject sync fails."""
1387 mock_superproject = self.manifest.superproject
1388 mock_superproject.Sync.return_value = mock.Mock(success=False)
1389
1390 with self.assertRaises(sync.SyncError) as e:
1391 self.cmd._SyncToSuperprojectRev(
1392 self.opt, self.manifest, self.mp, "name", self.errors
1393 )
1394 self.assertIn("failed to sync superproject", str(e.exception))
1395
1396
1397class UpdateAllManifestProjectsTests(unittest.TestCase):
1398 """Tests for Sync._UpdateAllManifestProjects."""
1399
1400 def setUp(self):
1401 self.repodir = tempfile.mkdtemp(".repo")
1402 self.manifest = mock.MagicMock(repodir=self.repodir)
1403 self.manifest.superproject = mock.MagicMock()
1404 self.manifest.path_prefix = ""
1405 self.manifest.standalone_manifest_url = None
1406 self.manifest.submanifests = {}
1407
1408 self.mp = mock.MagicMock()
1409 self.mp.manifest = self.manifest
1410 self.mp.standalone_manifest_url = None
1411 self.cmd = sync.Sync(manifest=self.manifest)
1412 self.cmd.outer_manifest = self.manifest
1413
1414 self.opt = mock.Mock()
1415 self.opt.verbose = False
1416 self.opt.superproject_revision = None
1417 self.opt.mp_update = True
1418
1419 self.errors = []
1420
1421 def tearDown(self):
1422 shutil.rmtree(self.repodir)
1423
1424 def test_superproject_revision_outer_manifest(self):
1425 """Test that _SyncToSuperprojectRev is called for outer manifest."""
1426 self.opt.superproject_revision = "deadbeef"
1427
1428 with mock.patch.object(
1429 self.cmd, "_SyncToSuperprojectRev"
1430 ) as mock_sync_to_rev:
1431 self.cmd._UpdateAllManifestProjects(
1432 self.opt, self.mp, "name", self.errors
1433 )
1434 mock_sync_to_rev.assert_called_once_with(
1435 self.opt, self.manifest, self.mp, "name", self.errors
1436 )
1437
1438 def test_superproject_revision_submanifest(self):
1439 """Test that _SyncToSuperprojectRev is NOT called for submanifest."""
1440 self.opt.superproject_revision = "deadbeef"
1441 submanifest = mock.MagicMock()
1442 submanifest.path_prefix = "sub/"
1443 submanifest.standalone_manifest_url = None
1444 self.mp.manifest = submanifest
1445
1446 with mock.patch.object(
1447 self.cmd, "_SyncToSuperprojectRev"
1448 ) as mock_sync_to_rev:
1449 with mock.patch.object(
1450 self.cmd, "_UpdateManifestProject"
1451 ) as mock_update_manifest:
1452 self.cmd._UpdateAllManifestProjects(
1453 self.opt, self.mp, "name", self.errors
1454 )
1455 mock_sync_to_rev.assert_not_called()
1456 mock_update_manifest.assert_called_once()