summaryrefslogtreecommitdiffstats
path: root/manifest_xml.py
diff options
context:
space:
mode:
Diffstat (limited to 'manifest_xml.py')
-rw-r--r--manifest_xml.py189
1 files changed, 144 insertions, 45 deletions
diff --git a/manifest_xml.py b/manifest_xml.py
index 0c2b45e5..68ead53c 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -12,6 +12,7 @@
12# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
13# limitations under the License. 13# limitations under the License.
14 14
15import collections
15import itertools 16import itertools
16import os 17import os
17import platform 18import platform
@@ -24,14 +25,21 @@ import gitc_utils
24from git_config import GitConfig, IsId 25from git_config import GitConfig, IsId
25from git_refs import R_HEADS, HEAD 26from git_refs import R_HEADS, HEAD
26import platform_utils 27import platform_utils
27from project import RemoteSpec, Project, MetaProject 28from project import Annotation, RemoteSpec, Project, MetaProject
28from error import (ManifestParseError, ManifestInvalidPathError, 29from error import (ManifestParseError, ManifestInvalidPathError,
29 ManifestInvalidRevisionError) 30 ManifestInvalidRevisionError)
31from wrapper import Wrapper
30 32
31MANIFEST_FILE_NAME = 'manifest.xml' 33MANIFEST_FILE_NAME = 'manifest.xml'
32LOCAL_MANIFEST_NAME = 'local_manifest.xml' 34LOCAL_MANIFEST_NAME = 'local_manifest.xml'
33LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' 35LOCAL_MANIFESTS_DIR_NAME = 'local_manifests'
34 36
37# Add all projects from local manifest into a group.
38LOCAL_MANIFEST_GROUP_PREFIX = 'local:'
39
40# ContactInfo has the self-registered bug url, supplied by the manifest authors.
41ContactInfo = collections.namedtuple('ContactInfo', 'bugurl')
42
35# urljoin gets confused if the scheme is not known. 43# urljoin gets confused if the scheme is not known.
36urllib.parse.uses_relative.extend([ 44urllib.parse.uses_relative.extend([
37 'ssh', 45 'ssh',
@@ -114,9 +122,13 @@ class _Default(object):
114 sync_tags = True 122 sync_tags = True
115 123
116 def __eq__(self, other): 124 def __eq__(self, other):
125 if not isinstance(other, _Default):
126 return False
117 return self.__dict__ == other.__dict__ 127 return self.__dict__ == other.__dict__
118 128
119 def __ne__(self, other): 129 def __ne__(self, other):
130 if not isinstance(other, _Default):
131 return True
120 return self.__dict__ != other.__dict__ 132 return self.__dict__ != other.__dict__
121 133
122 134
@@ -137,14 +149,22 @@ class _XmlRemote(object):
137 self.reviewUrl = review 149 self.reviewUrl = review
138 self.revision = revision 150 self.revision = revision
139 self.resolvedFetchUrl = self._resolveFetchUrl() 151 self.resolvedFetchUrl = self._resolveFetchUrl()
152 self.annotations = []
140 153
141 def __eq__(self, other): 154 def __eq__(self, other):
142 return self.__dict__ == other.__dict__ 155 if not isinstance(other, _XmlRemote):
156 return False
157 return (sorted(self.annotations) == sorted(other.annotations) and
158 self.name == other.name and self.fetchUrl == other.fetchUrl and
159 self.pushUrl == other.pushUrl and self.remoteAlias == other.remoteAlias
160 and self.reviewUrl == other.reviewUrl and self.revision == other.revision)
143 161
144 def __ne__(self, other): 162 def __ne__(self, other):
145 return self.__dict__ != other.__dict__ 163 return not self.__eq__(other)
146 164
147 def _resolveFetchUrl(self): 165 def _resolveFetchUrl(self):
166 if self.fetchUrl is None:
167 return ''
148 url = self.fetchUrl.rstrip('/') 168 url = self.fetchUrl.rstrip('/')
149 manifestUrl = self.manifestUrl.rstrip('/') 169 manifestUrl = self.manifestUrl.rstrip('/')
150 # urljoin will gets confused over quite a few things. The ones we care 170 # urljoin will gets confused over quite a few things. The ones we care
@@ -173,6 +193,9 @@ class _XmlRemote(object):
173 orig_name=self.name, 193 orig_name=self.name,
174 fetchUrl=self.fetchUrl) 194 fetchUrl=self.fetchUrl)
175 195
196 def AddAnnotation(self, name, value, keep):
197 self.annotations.append(Annotation(name, value, keep))
198
176 199
177class XmlManifest(object): 200class XmlManifest(object):
178 """manages the repo configuration file""" 201 """manages the repo configuration file"""
@@ -247,8 +270,7 @@ class XmlManifest(object):
247 self.Override(name) 270 self.Override(name)
248 271
249 # Old versions of repo would generate symlinks we need to clean up. 272 # Old versions of repo would generate symlinks we need to clean up.
250 if os.path.lexists(self.manifestFile): 273 platform_utils.remove(self.manifestFile, missing_ok=True)
251 platform_utils.remove(self.manifestFile)
252 # This file is interpreted as if it existed inside the manifest repo. 274 # This file is interpreted as if it existed inside the manifest repo.
253 # That allows us to use <include> with the relative file name. 275 # That allows us to use <include> with the relative file name.
254 with open(self.manifestFile, 'w') as fp: 276 with open(self.manifestFile, 'w') as fp:
@@ -282,6 +304,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
282 if r.revision is not None: 304 if r.revision is not None:
283 e.setAttribute('revision', r.revision) 305 e.setAttribute('revision', r.revision)
284 306
307 for a in r.annotations:
308 if a.keep == 'true':
309 ae = doc.createElement('annotation')
310 ae.setAttribute('name', a.name)
311 ae.setAttribute('value', a.value)
312 e.appendChild(ae)
313
285 def _ParseList(self, field): 314 def _ParseList(self, field):
286 """Parse fields that contain flattened lists. 315 """Parse fields that contain flattened lists.
287 316
@@ -477,6 +506,15 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
477 if not d.remote or remote.orig_name != remoteName: 506 if not d.remote or remote.orig_name != remoteName:
478 remoteName = remote.orig_name 507 remoteName = remote.orig_name
479 e.setAttribute('remote', remoteName) 508 e.setAttribute('remote', remoteName)
509 revision = remote.revision or d.revisionExpr
510 if not revision or revision != self._superproject['revision']:
511 e.setAttribute('revision', self._superproject['revision'])
512 root.appendChild(e)
513
514 if self._contactinfo.bugurl != Wrapper().BUG_URL:
515 root.appendChild(doc.createTextNode(''))
516 e = doc.createElement('contactinfo')
517 e.setAttribute('bugurl', self._contactinfo.bugurl)
480 root.appendChild(e) 518 root.appendChild(e)
481 519
482 return doc 520 return doc
@@ -490,6 +528,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
490 'manifest-server', 528 'manifest-server',
491 'repo-hooks', 529 'repo-hooks',
492 'superproject', 530 'superproject',
531 'contactinfo',
493 } 532 }
494 # Elements that may be repeated. 533 # Elements that may be repeated.
495 MULTI_ELEMENTS = { 534 MULTI_ELEMENTS = {
@@ -566,6 +605,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
566 return self._superproject 605 return self._superproject
567 606
568 @property 607 @property
608 def contactinfo(self):
609 self._Load()
610 return self._contactinfo
611
612 @property
569 def notice(self): 613 def notice(self):
570 self._Load() 614 self._Load()
571 return self._notice 615 return self._notice
@@ -596,6 +640,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
596 return set(x.strip() for x in exclude.split(',')) 640 return set(x.strip() for x in exclude.split(','))
597 641
598 @property 642 @property
643 def UseLocalManifests(self):
644 return self._load_local_manifests
645
646 def SetUseLocalManifests(self, value):
647 self._load_local_manifests = value
648
649 @property
650 def HasLocalManifests(self):
651 return self._load_local_manifests and self.local_manifests
652
653 @property
599 def IsMirror(self): 654 def IsMirror(self):
600 return self.manifestProject.config.GetBoolean('repo.mirror') 655 return self.manifestProject.config.GetBoolean('repo.mirror')
601 656
@@ -630,6 +685,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
630 self._default = None 685 self._default = None
631 self._repo_hooks_project = None 686 self._repo_hooks_project = None
632 self._superproject = {} 687 self._superproject = {}
688 self._contactinfo = ContactInfo(Wrapper().BUG_URL)
633 self._notice = None 689 self._notice = None
634 self.branch = None 690 self.branch = None
635 self._manifest_server = None 691 self._manifest_server = None
@@ -657,7 +713,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
657 # Since local manifests are entirely managed by the user, allow 713 # Since local manifests are entirely managed by the user, allow
658 # them to point anywhere the user wants. 714 # them to point anywhere the user wants.
659 nodes.append(self._ParseManifestXml( 715 nodes.append(self._ParseManifestXml(
660 local, self.repodir, restrict_includes=False)) 716 local, self.repodir,
717 parent_groups=f'{LOCAL_MANIFEST_GROUP_PREFIX}:{local_file[:-4]}',
718 restrict_includes=False))
661 except OSError: 719 except OSError:
662 pass 720 pass
663 721
@@ -754,9 +812,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
754 for node in itertools.chain(*node_list): 812 for node in itertools.chain(*node_list):
755 if node.nodeName == 'default': 813 if node.nodeName == 'default':
756 new_default = self._ParseDefault(node) 814 new_default = self._ParseDefault(node)
815 emptyDefault = not node.hasAttributes() and not node.hasChildNodes()
757 if self._default is None: 816 if self._default is None:
758 self._default = new_default 817 self._default = new_default
759 elif new_default != self._default: 818 elif not emptyDefault and new_default != self._default:
760 raise ManifestParseError('duplicate default in %s' % 819 raise ManifestParseError('duplicate default in %s' %
761 (self.manifestFile)) 820 (self.manifestFile))
762 821
@@ -795,6 +854,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
795 for subproject in project.subprojects: 854 for subproject in project.subprojects:
796 recursively_add_projects(subproject) 855 recursively_add_projects(subproject)
797 856
857 repo_hooks_project = None
858 enabled_repo_hooks = None
798 for node in itertools.chain(*node_list): 859 for node in itertools.chain(*node_list):
799 if node.nodeName == 'project': 860 if node.nodeName == 'project':
800 project = self._ParseProject(node) 861 project = self._ParseProject(node)
@@ -807,6 +868,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
807 'project: %s' % name) 868 'project: %s' % name)
808 869
809 path = node.getAttribute('path') 870 path = node.getAttribute('path')
871 dest_path = node.getAttribute('dest-path')
810 groups = node.getAttribute('groups') 872 groups = node.getAttribute('groups')
811 if groups: 873 if groups:
812 groups = self._ParseList(groups) 874 groups = self._ParseList(groups)
@@ -815,46 +877,37 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
815 if remote: 877 if remote:
816 remote = self._get_remote(node) 878 remote = self._get_remote(node)
817 879
880 named_projects = self._projects[name]
881 if dest_path and not path and len(named_projects) > 1:
882 raise ManifestParseError('extend-project cannot use dest-path when '
883 'matching multiple projects: %s' % name)
818 for p in self._projects[name]: 884 for p in self._projects[name]:
819 if path and p.relpath != path: 885 if path and p.relpath != path:
820 continue 886 continue
821 if groups: 887 if groups:
822 p.groups.extend(groups) 888 p.groups.extend(groups)
823 if revision: 889 if revision:
824 p.revisionExpr = revision 890 p.SetRevision(revision)
825 if IsId(revision): 891
826 p.revisionId = revision
827 else:
828 p.revisionId = None
829 if remote: 892 if remote:
830 p.remote = remote.ToRemoteSpec(name) 893 p.remote = remote.ToRemoteSpec(name)
831 if node.nodeName == 'repo-hooks':
832 # Get the name of the project and the (space-separated) list of enabled.
833 repo_hooks_project = self._reqatt(node, 'in-project')
834 enabled_repo_hooks = self._ParseList(self._reqatt(node, 'enabled-list'))
835 894
895 if dest_path:
896 del self._paths[p.relpath]
897 relpath, worktree, gitdir, objdir, _ = self.GetProjectPaths(name, dest_path)
898 p.UpdatePaths(relpath, worktree, gitdir, objdir)
899 self._paths[p.relpath] = p
900
901 if node.nodeName == 'repo-hooks':
836 # Only one project can be the hooks project 902 # Only one project can be the hooks project
837 if self._repo_hooks_project is not None: 903 if repo_hooks_project is not None:
838 raise ManifestParseError( 904 raise ManifestParseError(
839 'duplicate repo-hooks in %s' % 905 'duplicate repo-hooks in %s' %
840 (self.manifestFile)) 906 (self.manifestFile))
841 907
842 # Store a reference to the Project. 908 # Get the name of the project and the (space-separated) list of enabled.
843 try: 909 repo_hooks_project = self._reqatt(node, 'in-project')
844 repo_hooks_projects = self._projects[repo_hooks_project] 910 enabled_repo_hooks = self._ParseList(self._reqatt(node, 'enabled-list'))
845 except KeyError:
846 raise ManifestParseError(
847 'project %s not found for repo-hooks' %
848 (repo_hooks_project))
849
850 if len(repo_hooks_projects) != 1:
851 raise ManifestParseError(
852 'internal error parsing repo-hooks in %s' %
853 (self.manifestFile))
854 self._repo_hooks_project = repo_hooks_projects[0]
855
856 # Store the enabled hooks in the Project object.
857 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
858 if node.nodeName == 'superproject': 911 if node.nodeName == 'superproject':
859 name = self._reqatt(node, 'name') 912 name = self._reqatt(node, 'name')
860 # There can only be one superproject. 913 # There can only be one superproject.
@@ -872,21 +925,51 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
872 raise ManifestParseError("no remote for superproject %s within %s" % 925 raise ManifestParseError("no remote for superproject %s within %s" %
873 (name, self.manifestFile)) 926 (name, self.manifestFile))
874 self._superproject['remote'] = remote.ToRemoteSpec(name) 927 self._superproject['remote'] = remote.ToRemoteSpec(name)
928 revision = node.getAttribute('revision') or remote.revision
929 if not revision:
930 revision = self._default.revisionExpr
931 if not revision:
932 raise ManifestParseError('no revision for superproject %s within %s' %
933 (name, self.manifestFile))
934 self._superproject['revision'] = revision
935 if node.nodeName == 'contactinfo':
936 bugurl = self._reqatt(node, 'bugurl')
937 # This element can be repeated, later entries will clobber earlier ones.
938 self._contactinfo = ContactInfo(bugurl)
939
875 if node.nodeName == 'remove-project': 940 if node.nodeName == 'remove-project':
876 name = self._reqatt(node, 'name') 941 name = self._reqatt(node, 'name')
877 942
878 if name not in self._projects: 943 if name in self._projects:
944 for p in self._projects[name]:
945 del self._paths[p.relpath]
946 del self._projects[name]
947
948 # If the manifest removes the hooks project, treat it as if it deleted
949 # the repo-hooks element too.
950 if repo_hooks_project == name:
951 repo_hooks_project = None
952 elif not XmlBool(node, 'optional', False):
879 raise ManifestParseError('remove-project element specifies non-existent ' 953 raise ManifestParseError('remove-project element specifies non-existent '
880 'project: %s' % name) 954 'project: %s' % name)
881 955
882 for p in self._projects[name]: 956 # Store repo hooks project information.
883 del self._paths[p.relpath] 957 if repo_hooks_project:
884 del self._projects[name] 958 # Store a reference to the Project.
959 try:
960 repo_hooks_projects = self._projects[repo_hooks_project]
961 except KeyError:
962 raise ManifestParseError(
963 'project %s not found for repo-hooks' %
964 (repo_hooks_project))
885 965
886 # If the manifest removes the hooks project, treat it as if it deleted 966 if len(repo_hooks_projects) != 1:
887 # the repo-hooks element too. 967 raise ManifestParseError(
888 if self._repo_hooks_project and (self._repo_hooks_project.name == name): 968 'internal error parsing repo-hooks in %s' %
889 self._repo_hooks_project = None 969 (self.manifestFile))
970 self._repo_hooks_project = repo_hooks_projects[0]
971 # Store the enabled hooks in the Project object.
972 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
890 973
891 def _AddMetaProjectMirror(self, m): 974 def _AddMetaProjectMirror(self, m):
892 name = None 975 name = None
@@ -945,7 +1028,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
945 if revision == '': 1028 if revision == '':
946 revision = None 1029 revision = None
947 manifestUrl = self.manifestProject.config.GetString('remote.origin.url') 1030 manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
948 return _XmlRemote(name, alias, fetch, pushUrl, manifestUrl, review, revision) 1031
1032 remote = _XmlRemote(name, alias, fetch, pushUrl, manifestUrl, review, revision)
1033
1034 for n in node.childNodes:
1035 if n.nodeName == 'annotation':
1036 self._ParseAnnotation(remote, n)
1037
1038 return remote
949 1039
950 def _ParseDefault(self, node): 1040 def _ParseDefault(self, node):
951 """ 1041 """
@@ -1199,6 +1289,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
1199 if '~' in path: 1289 if '~' in path:
1200 return '~ not allowed (due to 8.3 filenames on Windows filesystems)' 1290 return '~ not allowed (due to 8.3 filenames on Windows filesystems)'
1201 1291
1292 path_codepoints = set(path)
1293
1202 # Some filesystems (like Apple's HFS+) try to normalize Unicode codepoints 1294 # Some filesystems (like Apple's HFS+) try to normalize Unicode codepoints
1203 # which means there are alternative names for ".git". Reject paths with 1295 # which means there are alternative names for ".git". Reject paths with
1204 # these in it as there shouldn't be any reasonable need for them here. 1296 # these in it as there shouldn't be any reasonable need for them here.
@@ -1222,10 +1314,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
1222 u'\u206F', # NOMINAL DIGIT SHAPES 1314 u'\u206F', # NOMINAL DIGIT SHAPES
1223 u'\uFEFF', # ZERO WIDTH NO-BREAK SPACE 1315 u'\uFEFF', # ZERO WIDTH NO-BREAK SPACE
1224 } 1316 }
1225 if BAD_CODEPOINTS & set(path): 1317 if BAD_CODEPOINTS & path_codepoints:
1226 # This message is more expansive than reality, but should be fine. 1318 # This message is more expansive than reality, but should be fine.
1227 return 'Unicode combining characters not allowed' 1319 return 'Unicode combining characters not allowed'
1228 1320
1321 # Reject newlines as there shouldn't be any legitmate use for them, they'll
1322 # be confusing to users, and they can easily break tools that expect to be
1323 # able to iterate over newline delimited lists. This even applies to our
1324 # own code like .repo/project.list.
1325 if {'\r', '\n'} & path_codepoints:
1326 return 'Newlines not allowed'
1327
1229 # Assume paths might be used on case-insensitive filesystems. 1328 # Assume paths might be used on case-insensitive filesystems.
1230 path = path.lower() 1329 path = path.lower()
1231 1330
@@ -1303,7 +1402,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
1303 self._ValidateFilePaths('linkfile', src, dest) 1402 self._ValidateFilePaths('linkfile', src, dest)
1304 project.AddLinkFile(src, dest, self.topdir) 1403 project.AddLinkFile(src, dest, self.topdir)
1305 1404
1306 def _ParseAnnotation(self, project, node): 1405 def _ParseAnnotation(self, element, node):
1307 name = self._reqatt(node, 'name') 1406 name = self._reqatt(node, 'name')
1308 value = self._reqatt(node, 'value') 1407 value = self._reqatt(node, 'value')
1309 try: 1408 try:
@@ -1313,7 +1412,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
1313 if keep != "true" and keep != "false": 1412 if keep != "true" and keep != "false":
1314 raise ManifestParseError('optional "keep" attribute must be ' 1413 raise ManifestParseError('optional "keep" attribute must be '
1315 '"true" or "false"') 1414 '"true" or "false"')
1316 project.AddAnnotation(name, value, keep) 1415 element.AddAnnotation(name, value, keep)
1317 1416
1318 def _get_remote(self, node): 1417 def _get_remote(self, node):
1319 name = node.getAttribute('remote') 1418 name = node.getAttribute('remote')