summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPeter Kjellerstedt <pkj@axis.com>2025-11-08 00:06:16 +0100
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2025-11-26 02:07:35 -0800
commit412367bfafd9de014cfeb37475e0f97b1f6a2509 (patch)
tree100e674dea649546145dbb81532a8f432cc09f52
parent47c24b5c4092ce397c94c05d8686c8bcac64fbb2 (diff)
downloadgit-repo-412367bfafd9de014cfeb37475e0f97b1f6a2509.tar.gz
project: Use dicts to keep track of copyfiles and linkfiles
This avoids copying/linking the same file/link multiple times if a copyfile/linkfile element with the same values has been specifed multiple times. This can happen when including a common manifest that uses an extend-project element that has a copyfile/linkfile element. This uses dicts rather than sets to store the copyfiles and linkfiles to make sure the order they are specified in the manifest is maintained. For Python 3.7+, maintaining the order that keys are added to dicts is guaranteed, and for Python 3.6 it happened to be true. The _CopyFile class and the _LinkFile class are changed to inherit from NamedTuple to be able to store them in dicts. Change-Id: I9f5a80298b875251a81c5fe7d353e262d104fae4 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/525322 Reviewed-by: Mike Frysinger <vapier@google.com> Reviewed-by: Gavin Mak <gavinmak@google.com> Tested-by: Peter Kjellerstedt <peter.kjellerstedt@axis.com> Commit-Queue: Peter Kjellerstedt <peter.kjellerstedt@axis.com>
-rw-r--r--docs/manifest-format.md13
-rw-r--r--man/repo-manifest.113
-rw-r--r--project.py55
-rw-r--r--tests/test_manifest_xml.py90
4 files changed, 131 insertions, 40 deletions
diff --git a/docs/manifest-format.md b/docs/manifest-format.md
index d1a11cc9..42e1ab18 100644
--- a/docs/manifest-format.md
+++ b/docs/manifest-format.md
@@ -453,10 +453,14 @@ Intermediate paths must not be symlinks either.
453 453
454Parent directories of "dest" will be automatically created if missing. 454Parent directories of "dest" will be automatically created if missing.
455 455
456The files are copied in the order they are specified in the manifests.
457If multiple elements specify the same source and destination, they will
458only be applied as one, based on the first occurence. Files are copied
459before any links specified via linkfile elements are created.
460
456### Element linkfile 461### Element linkfile
457 462
458It's just like copyfile and runs at the same time as copyfile but 463It's just like copyfile, but instead of copying it creates a symlink.
459instead of copying it creates a symlink.
460 464
461The symlink is created at "dest" (relative to the top of the tree) and 465The symlink is created at "dest" (relative to the top of the tree) and
462points to the path specified by "src" which is a path in the project. 466points to the path specified by "src" which is a path in the project.
@@ -466,6 +470,11 @@ Parent directories of "dest" will be automatically created if missing.
466The symlink target may be a file or directory, but it may not point outside 470The symlink target may be a file or directory, but it may not point outside
467of the repo client. 471of the repo client.
468 472
473The links are created in the order they are specified in the manifests.
474If multiple elements specify the same source and destination, they will
475only be applied as one, based on the first occurence. Links are created
476after any files specified via copyfile elements are copied.
477
469### Element remove-project 478### Element remove-project
470 479
471Deletes a project from the internal manifest table, possibly 480Deletes a project from the internal manifest table, possibly
diff --git a/man/repo-manifest.1 b/man/repo-manifest.1
index df3943ce..1a97ff7d 100644
--- a/man/repo-manifest.1
+++ b/man/repo-manifest.1
@@ -521,10 +521,14 @@ Intermediate paths must not be symlinks either.
521.PP 521.PP
522Parent directories of "dest" will be automatically created if missing. 522Parent directories of "dest" will be automatically created if missing.
523.PP 523.PP
524The files are copied in the order they are specified in the manifests. If
525multiple elements specify the same source and destination, they will only be
526applied as one, based on the first occurence. Files are copied before any links
527specified via linkfile elements are created.
528.PP
524Element linkfile 529Element linkfile
525.PP 530.PP
526It's just like copyfile and runs at the same time as copyfile but instead of 531It's just like copyfile, but instead of copying it creates a symlink.
527copying it creates a symlink.
528.PP 532.PP
529The symlink is created at "dest" (relative to the top of the tree) and points to 533The symlink is created at "dest" (relative to the top of the tree) and points to
530the path specified by "src" which is a path in the project. 534the path specified by "src" which is a path in the project.
@@ -534,6 +538,11 @@ Parent directories of "dest" will be automatically created if missing.
534The symlink target may be a file or directory, but it may not point outside of 538The symlink target may be a file or directory, but it may not point outside of
535the repo client. 539the repo client.
536.PP 540.PP
541The links are created in the order they are specified in the manifests. If
542multiple elements specify the same source and destination, they will only be
543applied as one, based on the first occurence. Links are created after any files
544specified via copyfile elements are copied.
545.PP
537Element remove\-project 546Element remove\-project
538.PP 547.PP
539Deletes a project from the internal manifest table, possibly allowing a 548Deletes a project from the internal manifest table, possibly allowing a
diff --git a/project.py b/project.py
index 8e430175..41606442 100644
--- a/project.py
+++ b/project.py
@@ -390,22 +390,17 @@ def _SafeExpandPath(base, subpath, skipfinal=False):
390 return path 390 return path
391 391
392 392
393class _CopyFile: 393class _CopyFile(NamedTuple):
394 """Container for <copyfile> manifest element.""" 394 """Container for <copyfile> manifest element."""
395 395
396 def __init__(self, git_worktree, src, topdir, dest): 396 # Absolute path to the git project checkout.
397 """Register a <copyfile> request. 397 git_worktree: str
398 398 # Relative path under |git_worktree| of file to read.
399 Args: 399 src: str
400 git_worktree: Absolute path to the git project checkout. 400 # Absolute path to the top of the repo client checkout.
401 src: Relative path under |git_worktree| of file to read. 401 topdir: str
402 topdir: Absolute path to the top of the repo client checkout. 402 # Relative path under |topdir| of file to write.
403 dest: Relative path under |topdir| of file to write. 403 dest: str
404 """
405 self.git_worktree = git_worktree
406 self.topdir = topdir
407 self.src = src
408 self.dest = dest
409 404
410 def _Copy(self): 405 def _Copy(self):
411 src = _SafeExpandPath(self.git_worktree, self.src) 406 src = _SafeExpandPath(self.git_worktree, self.src)
@@ -439,22 +434,17 @@ class _CopyFile:
439 logger.error("error: Cannot copy file %s to %s", src, dest) 434 logger.error("error: Cannot copy file %s to %s", src, dest)
440 435
441 436
442class _LinkFile: 437class _LinkFile(NamedTuple):
443 """Container for <linkfile> manifest element.""" 438 """Container for <linkfile> manifest element."""
444 439
445 def __init__(self, git_worktree, src, topdir, dest): 440 # Absolute path to the git project checkout.
446 """Register a <linkfile> request. 441 git_worktree: str
447 442 # Target of symlink relative to path under |git_worktree|.
448 Args: 443 src: str
449 git_worktree: Absolute path to the git project checkout. 444 # Absolute path to the top of the repo client checkout.
450 src: Target of symlink relative to path under |git_worktree|. 445 topdir: str
451 topdir: Absolute path to the top of the repo client checkout. 446 # Relative path under |topdir| of symlink to create.
452 dest: Relative path under |topdir| of symlink to create. 447 dest: str
453 """
454 self.git_worktree = git_worktree
455 self.topdir = topdir
456 self.src = src
457 self.dest = dest
458 448
459 def __linkIt(self, relSrc, absDest): 449 def __linkIt(self, relSrc, absDest):
460 # Link file if it does not exist or is out of date. 450 # Link file if it does not exist or is out of date.
@@ -633,8 +623,9 @@ class Project:
633 self.subprojects = [] 623 self.subprojects = []
634 624
635 self.snapshots = {} 625 self.snapshots = {}
636 self.copyfiles = [] 626 # Use dicts to dedupe while maintaining declared order.
637 self.linkfiles = [] 627 self.copyfiles = {}
628 self.linkfiles = {}
638 self.annotations = [] 629 self.annotations = []
639 self.dest_branch = dest_branch 630 self.dest_branch = dest_branch
640 631
@@ -1794,7 +1785,7 @@ class Project:
1794 Paths should have basic validation run on them before being queued. 1785 Paths should have basic validation run on them before being queued.
1795 Further checking will be handled when the actual copy happens. 1786 Further checking will be handled when the actual copy happens.
1796 """ 1787 """
1797 self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest)) 1788 self.copyfiles[_CopyFile(self.worktree, src, topdir, dest)] = True
1798 1789
1799 def AddLinkFile(self, src, dest, topdir): 1790 def AddLinkFile(self, src, dest, topdir):
1800 """Mark |dest| to create a symlink (relative to |topdir|) pointing to 1791 """Mark |dest| to create a symlink (relative to |topdir|) pointing to
@@ -1805,7 +1796,7 @@ class Project:
1805 Paths should have basic validation run on them before being queued. 1796 Paths should have basic validation run on them before being queued.
1806 Further checking will be handled when the actual link happens. 1797 Further checking will be handled when the actual link happens.
1807 """ 1798 """
1808 self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest)) 1799 self.linkfiles[_LinkFile(self.worktree, src, topdir, dest)] = True
1809 1800
1810 def AddAnnotation(self, name, value, keep): 1801 def AddAnnotation(self, name, value, keep):
1811 self.annotations.append(Annotation(name, value, keep)) 1802 self.annotations.append(Annotation(name, value, keep))
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py
index d4bf76a9..f5991515 100644
--- a/tests/test_manifest_xml.py
+++ b/tests/test_manifest_xml.py
@@ -1254,8 +1254,8 @@ class ExtendProjectElementTests(ManifestParseTestCase):
1254</manifest> 1254</manifest>
1255""" 1255"""
1256 ) 1256 )
1257 self.assertEqual(manifest.projects[0].copyfiles[0].src, "foo") 1257 self.assertEqual(list(manifest.projects[0].copyfiles)[0].src, "foo")
1258 self.assertEqual(manifest.projects[0].copyfiles[0].dest, "bar") 1258 self.assertEqual(list(manifest.projects[0].copyfiles)[0].dest, "bar")
1259 self.assertEqual( 1259 self.assertEqual(
1260 sort_attributes(manifest.ToXml().toxml()), 1260 sort_attributes(manifest.ToXml().toxml()),
1261 '<?xml version="1.0" ?><manifest>' 1261 '<?xml version="1.0" ?><manifest>'
@@ -1267,6 +1267,47 @@ class ExtendProjectElementTests(ManifestParseTestCase):
1267 "</manifest>", 1267 "</manifest>",
1268 ) 1268 )
1269 1269
1270 def test_extend_project_duplicate_copyfiles(self):
1271 root_m = self.manifest_dir / "root.xml"
1272 root_m.write_text(
1273 """
1274<manifest>
1275 <remote name="test-remote" fetch="http://localhost" />
1276 <default remote="test-remote" revision="refs/heads/main" />
1277 <project name="myproject" />
1278 <include name="man1.xml" />
1279 <include name="man2.xml" />
1280</manifest>
1281"""
1282 )
1283 (self.manifest_dir / "man1.xml").write_text(
1284 """
1285<manifest>
1286 <include name="common.xml" />
1287</manifest>
1288"""
1289 )
1290 (self.manifest_dir / "man2.xml").write_text(
1291 """
1292<manifest>
1293 <include name="common.xml" />
1294</manifest>
1295"""
1296 )
1297 (self.manifest_dir / "common.xml").write_text(
1298 """
1299<manifest>
1300 <extend-project name="myproject">
1301 <copyfile dest="bar" src="foo"/>
1302 </extend-project>
1303</manifest>
1304"""
1305 )
1306 manifest = manifest_xml.XmlManifest(str(self.repodir), str(root_m))
1307 self.assertEqual(len(manifest.projects[0].copyfiles), 1)
1308 self.assertEqual(list(manifest.projects[0].copyfiles)[0].src, "foo")
1309 self.assertEqual(list(manifest.projects[0].copyfiles)[0].dest, "bar")
1310
1270 def test_extend_project_linkfiles(self): 1311 def test_extend_project_linkfiles(self):
1271 manifest = self.getXmlManifest( 1312 manifest = self.getXmlManifest(
1272 """ 1313 """
@@ -1280,8 +1321,8 @@ class ExtendProjectElementTests(ManifestParseTestCase):
1280</manifest> 1321</manifest>
1281""" 1322"""
1282 ) 1323 )
1283 self.assertEqual(manifest.projects[0].linkfiles[0].src, "foo") 1324 self.assertEqual(list(manifest.projects[0].linkfiles)[0].src, "foo")
1284 self.assertEqual(manifest.projects[0].linkfiles[0].dest, "bar") 1325 self.assertEqual(list(manifest.projects[0].linkfiles)[0].dest, "bar")
1285 self.assertEqual( 1326 self.assertEqual(
1286 sort_attributes(manifest.ToXml().toxml()), 1327 sort_attributes(manifest.ToXml().toxml()),
1287 '<?xml version="1.0" ?><manifest>' 1328 '<?xml version="1.0" ?><manifest>'
@@ -1293,6 +1334,47 @@ class ExtendProjectElementTests(ManifestParseTestCase):
1293 "</manifest>", 1334 "</manifest>",
1294 ) 1335 )
1295 1336
1337 def test_extend_project_duplicate_linkfiles(self):
1338 root_m = self.manifest_dir / "root.xml"
1339 root_m.write_text(
1340 """
1341<manifest>
1342 <remote name="test-remote" fetch="http://localhost" />
1343 <default remote="test-remote" revision="refs/heads/main" />
1344 <project name="myproject" />
1345 <include name="man1.xml" />
1346 <include name="man2.xml" />
1347</manifest>
1348"""
1349 )
1350 (self.manifest_dir / "man1.xml").write_text(
1351 """
1352<manifest>
1353 <include name="common.xml" />
1354</manifest>
1355"""
1356 )
1357 (self.manifest_dir / "man2.xml").write_text(
1358 """
1359<manifest>
1360 <include name="common.xml" />
1361</manifest>
1362"""
1363 )
1364 (self.manifest_dir / "common.xml").write_text(
1365 """
1366<manifest>
1367 <extend-project name="myproject">
1368 <linkfile dest="bar" src="foo"/>
1369 </extend-project>
1370</manifest>
1371"""
1372 )
1373 manifest = manifest_xml.XmlManifest(str(self.repodir), str(root_m))
1374 self.assertEqual(len(manifest.projects[0].linkfiles), 1)
1375 self.assertEqual(list(manifest.projects[0].linkfiles)[0].src, "foo")
1376 self.assertEqual(list(manifest.projects[0].linkfiles)[0].dest, "bar")
1377
1296 def test_extend_project_annotations(self): 1378 def test_extend_project_annotations(self):
1297 manifest = self.getXmlManifest( 1379 manifest = self.getXmlManifest(
1298 """ 1380 """