diff --git a/docs/manifest-format.md b/docs/manifest-format.md
index edcb28cb..36dae6de 100644
--- a/docs/manifest-format.md
+++ b/docs/manifest-format.md
@@ -109,8 +109,9 @@ following DTD:
-
-
+
+
+
@@ -473,7 +474,7 @@ of the repo client.
### Element remove-project
-Deletes the named project from the internal manifest table, possibly
+Deletes a project from the internal manifest table, possibly
allowing a subsequent project element in the same manifest file to
replace the project with a different source.
@@ -481,6 +482,17 @@ This element is mostly useful in a local manifest file, where
the user can remove a project, and possibly replace it with their
own definition.
+The project `name` or project `path` can be used to specify the remove target
+meaning one of them is required. If only name is specified, all
+projects with that name are removed.
+
+If both name and path are specified, only projects with the same name and
+path are removed, meaning projects with the same name but in other
+locations are kept.
+
+If only path is specified, a matching project is removed regardless of its
+name. Logic otherwise behaves like both are specified.
+
Attribute `optional`: Set to true to ignore remove-project elements with no
matching `project` element.
diff --git a/manifest_xml.py b/manifest_xml.py
index 555bf736..73be1b6e 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -1535,22 +1535,45 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
self._contactinfo = ContactInfo(bugurl)
if node.nodeName == "remove-project":
- name = self._reqatt(node, "name")
+ name = node.getAttribute("name")
+ path = node.getAttribute("path")
- if name in self._projects:
- for p in self._projects[name]:
- del self._paths[p.relpath]
- del self._projects[name]
+ # Name or path needed.
+ if not name and not path:
+ raise ManifestParseError(
+ "remove-project must have name and/or path"
+ )
- # If the manifest removes the hooks project, treat it as if
- # it deleted
- # the repo-hooks element too.
- if repo_hooks_project == name:
- repo_hooks_project = None
- elif not XmlBool(node, "optional", False):
+ removed_project = ""
+
+ # Find and remove projects based on name and/or path.
+ for projname, projects in list(self._projects.items()):
+ for p in projects:
+ if name == projname and not path:
+ del self._paths[p.relpath]
+ if not removed_project:
+ del self._projects[name]
+ removed_project = name
+ elif path == p.relpath and (
+ name == projname or not name
+ ):
+ self._projects[projname].remove(p)
+ del self._paths[p.relpath]
+ removed_project = p.name
+
+ # If the manifest removes the hooks project, treat it as if
+ # it deleted the repo-hooks element too.
+ if (
+ removed_project
+ and removed_project not in self._projects
+ and repo_hooks_project == removed_project
+ ):
+ repo_hooks_project = None
+
+ if not removed_project and not XmlBool(node, "optional", False):
raise ManifestParseError(
"remove-project element specifies non-existent "
- "project: %s" % name
+ "project: %s" % node.toxml()
)
# Store repo hooks project information.
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py
index ef511055..1015e114 100644
--- a/tests/test_manifest_xml.py
+++ b/tests/test_manifest_xml.py
@@ -996,6 +996,44 @@ class RemoveProjectElementTests(ManifestParseTestCase):
)
self.assertEqual(manifest.projects, [])
+ def test_remove_using_path_attrib(self):
+ manifest = self.getXmlManifest(
+ """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ )
+ found_proj1_path1 = False
+ found_proj2 = False
+ for proj in manifest.projects:
+ if proj.name == "project1":
+ found_proj1_path1 = True
+ self.assertEqual(proj.relpath, "tests/path1")
+ if proj.name == "project2":
+ found_proj2 = True
+ self.assertNotEqual(proj.name, "project3")
+ self.assertNotEqual(proj.name, "project4")
+ self.assertNotEqual(proj.name, "project5")
+ self.assertNotEqual(proj.name, "project6")
+ self.assertTrue(found_proj1_path1)
+ self.assertTrue(found_proj2)
+
class ExtendProjectElementTests(ManifestParseTestCase):
"""Tests for ."""