Skip to content

Commit 412367b

Browse files
Peter KjellerstedtLUCI
authored andcommitted
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 <[email protected]> Reviewed-by: Gavin Mak <[email protected]> Tested-by: Peter Kjellerstedt <[email protected]> Commit-Queue: Peter Kjellerstedt <[email protected]>
1 parent 47c24b5 commit 412367b

File tree

4 files changed

+131
-40
lines changed

4 files changed

+131
-40
lines changed

docs/manifest-format.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,10 +453,14 @@ Intermediate paths must not be symlinks either.
453453

454454
Parent directories of "dest" will be automatically created if missing.
455455

456+
The files are copied in the order they are specified in the manifests.
457+
If multiple elements specify the same source and destination, they will
458+
only be applied as one, based on the first occurence. Files are copied
459+
before any links specified via linkfile elements are created.
460+
456461
### Element linkfile
457462

458-
It's just like copyfile and runs at the same time as copyfile but
459-
instead of copying it creates a symlink.
463+
It's just like copyfile, but instead of copying it creates a symlink.
460464

461465
The symlink is created at "dest" (relative to the top of the tree) and
462466
points 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.
466470
The symlink target may be a file or directory, but it may not point outside
467471
of the repo client.
468472

473+
The links are created in the order they are specified in the manifests.
474+
If multiple elements specify the same source and destination, they will
475+
only be applied as one, based on the first occurence. Links are created
476+
after any files specified via copyfile elements are copied.
477+
469478
### Element remove-project
470479

471480
Deletes a project from the internal manifest table, possibly

man/repo-manifest.1

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,10 +521,14 @@ Intermediate paths must not be symlinks either.
521521
.PP
522522
Parent directories of "dest" will be automatically created if missing.
523523
.PP
524+
The files are copied in the order they are specified in the manifests. If
525+
multiple elements specify the same source and destination, they will only be
526+
applied as one, based on the first occurence. Files are copied before any links
527+
specified via linkfile elements are created.
528+
.PP
524529
Element linkfile
525530
.PP
526-
It's just like copyfile and runs at the same time as copyfile but instead of
527-
copying it creates a symlink.
531+
It's just like copyfile, but instead of copying it creates a symlink.
528532
.PP
529533
The symlink is created at "dest" (relative to the top of the tree) and points to
530534
the 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.
534538
The symlink target may be a file or directory, but it may not point outside of
535539
the repo client.
536540
.PP
541+
The links are created in the order they are specified in the manifests. If
542+
multiple elements specify the same source and destination, they will only be
543+
applied as one, based on the first occurence. Links are created after any files
544+
specified via copyfile elements are copied.
545+
.PP
537546
Element remove\-project
538547
.PP
539548
Deletes a project from the internal manifest table, possibly allowing a

project.py

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -390,22 +390,17 @@ def _SafeExpandPath(base, subpath, skipfinal=False):
390390
return path
391391

392392

393-
class _CopyFile:
393+
class _CopyFile(NamedTuple):
394394
"""Container for <copyfile> manifest element."""
395395

396-
def __init__(self, git_worktree, src, topdir, dest):
397-
"""Register a <copyfile> request.
398-
399-
Args:
400-
git_worktree: Absolute path to the git project checkout.
401-
src: Relative path under |git_worktree| of file to read.
402-
topdir: Absolute path to the top of the repo client checkout.
403-
dest: Relative path under |topdir| of file to write.
404-
"""
405-
self.git_worktree = git_worktree
406-
self.topdir = topdir
407-
self.src = src
408-
self.dest = dest
396+
# Absolute path to the git project checkout.
397+
git_worktree: str
398+
# Relative path under |git_worktree| of file to read.
399+
src: str
400+
# Absolute path to the top of the repo client checkout.
401+
topdir: str
402+
# Relative path under |topdir| of file to write.
403+
dest: str
409404

410405
def _Copy(self):
411406
src = _SafeExpandPath(self.git_worktree, self.src)
@@ -439,22 +434,17 @@ def _Copy(self):
439434
logger.error("error: Cannot copy file %s to %s", src, dest)
440435

441436

442-
class _LinkFile:
437+
class _LinkFile(NamedTuple):
443438
"""Container for <linkfile> manifest element."""
444439

445-
def __init__(self, git_worktree, src, topdir, dest):
446-
"""Register a <linkfile> request.
447-
448-
Args:
449-
git_worktree: Absolute path to the git project checkout.
450-
src: Target of symlink relative to path under |git_worktree|.
451-
topdir: Absolute path to the top of the repo client checkout.
452-
dest: Relative path under |topdir| of symlink to create.
453-
"""
454-
self.git_worktree = git_worktree
455-
self.topdir = topdir
456-
self.src = src
457-
self.dest = dest
440+
# Absolute path to the git project checkout.
441+
git_worktree: str
442+
# Target of symlink relative to path under |git_worktree|.
443+
src: str
444+
# Absolute path to the top of the repo client checkout.
445+
topdir: str
446+
# Relative path under |topdir| of symlink to create.
447+
dest: str
458448

459449
def __linkIt(self, relSrc, absDest):
460450
# Link file if it does not exist or is out of date.
@@ -633,8 +623,9 @@ def __init__(
633623
self.subprojects = []
634624

635625
self.snapshots = {}
636-
self.copyfiles = []
637-
self.linkfiles = []
626+
# Use dicts to dedupe while maintaining declared order.
627+
self.copyfiles = {}
628+
self.linkfiles = {}
638629
self.annotations = []
639630
self.dest_branch = dest_branch
640631

@@ -1794,7 +1785,7 @@ def AddCopyFile(self, src, dest, topdir):
17941785
Paths should have basic validation run on them before being queued.
17951786
Further checking will be handled when the actual copy happens.
17961787
"""
1797-
self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest))
1788+
self.copyfiles[_CopyFile(self.worktree, src, topdir, dest)] = True
17981789

17991790
def AddLinkFile(self, src, dest, topdir):
18001791
"""Mark |dest| to create a symlink (relative to |topdir|) pointing to
@@ -1805,7 +1796,7 @@ def AddLinkFile(self, src, dest, topdir):
18051796
Paths should have basic validation run on them before being queued.
18061797
Further checking will be handled when the actual link happens.
18071798
"""
1808-
self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
1799+
self.linkfiles[_LinkFile(self.worktree, src, topdir, dest)] = True
18091800

18101801
def AddAnnotation(self, name, value, keep):
18111802
self.annotations.append(Annotation(name, value, keep))

tests/test_manifest_xml.py

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,8 +1254,8 @@ def test_extend_project_copyfiles(self):
12541254
</manifest>
12551255
"""
12561256
)
1257-
self.assertEqual(manifest.projects[0].copyfiles[0].src, "foo")
1258-
self.assertEqual(manifest.projects[0].copyfiles[0].dest, "bar")
1257+
self.assertEqual(list(manifest.projects[0].copyfiles)[0].src, "foo")
1258+
self.assertEqual(list(manifest.projects[0].copyfiles)[0].dest, "bar")
12591259
self.assertEqual(
12601260
sort_attributes(manifest.ToXml().toxml()),
12611261
'<?xml version="1.0" ?><manifest>'
@@ -1267,6 +1267,47 @@ def test_extend_project_copyfiles(self):
12671267
"</manifest>",
12681268
)
12691269

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+
12701311
def test_extend_project_linkfiles(self):
12711312
manifest = self.getXmlManifest(
12721313
"""
@@ -1280,8 +1321,8 @@ def test_extend_project_linkfiles(self):
12801321
</manifest>
12811322
"""
12821323
)
1283-
self.assertEqual(manifest.projects[0].linkfiles[0].src, "foo")
1284-
self.assertEqual(manifest.projects[0].linkfiles[0].dest, "bar")
1324+
self.assertEqual(list(manifest.projects[0].linkfiles)[0].src, "foo")
1325+
self.assertEqual(list(manifest.projects[0].linkfiles)[0].dest, "bar")
12851326
self.assertEqual(
12861327
sort_attributes(manifest.ToXml().toxml()),
12871328
'<?xml version="1.0" ?><manifest>'
@@ -1293,6 +1334,47 @@ def test_extend_project_linkfiles(self):
12931334
"</manifest>",
12941335
)
12951336

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+
12961378
def test_extend_project_annotations(self):
12971379
manifest = self.getXmlManifest(
12981380
"""

0 commit comments

Comments
 (0)