summaryrefslogtreecommitdiffstats
path: root/manifest_xml.py
diff options
context:
space:
mode:
Diffstat (limited to 'manifest_xml.py')
-rw-r--r--manifest_xml.py888
1 files changed, 678 insertions, 210 deletions
diff --git a/manifest_xml.py b/manifest_xml.py
index 3814a25a..68ead53c 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,33 +12,34 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function 15import collections
18import itertools 16import itertools
19import os 17import os
18import platform
20import re 19import re
21import sys 20import sys
22import xml.dom.minidom 21import xml.dom.minidom
23 22import urllib.parse
24from pyversion import is_python3
25if is_python3():
26 import urllib.parse
27else:
28 import imp
29 import urlparse
30 urllib = imp.new_module('urllib')
31 urllib.parse = urlparse
32 23
33import gitc_utils 24import gitc_utils
34from git_config import GitConfig 25from git_config import GitConfig, IsId
35from git_refs import R_HEADS, HEAD 26from git_refs import R_HEADS, HEAD
36import platform_utils 27import platform_utils
37from project import RemoteSpec, Project, MetaProject 28from project import Annotation, RemoteSpec, Project, MetaProject
38from error import ManifestParseError, ManifestInvalidRevisionError 29from error import (ManifestParseError, ManifestInvalidPathError,
30 ManifestInvalidRevisionError)
31from wrapper import Wrapper
39 32
40MANIFEST_FILE_NAME = 'manifest.xml' 33MANIFEST_FILE_NAME = 'manifest.xml'
41LOCAL_MANIFEST_NAME = 'local_manifest.xml' 34LOCAL_MANIFEST_NAME = 'local_manifest.xml'
42LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' 35LOCAL_MANIFESTS_DIR_NAME = 'local_manifests'
43 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
44# urljoin gets confused if the scheme is not known. 43# urljoin gets confused if the scheme is not known.
45urllib.parse.uses_relative.extend([ 44urllib.parse.uses_relative.extend([
46 'ssh', 45 'ssh',
@@ -55,6 +54,61 @@ urllib.parse.uses_netloc.extend([
55 'sso', 54 'sso',
56 'rpc']) 55 'rpc'])
57 56
57
58def XmlBool(node, attr, default=None):
59 """Determine boolean value of |node|'s |attr|.
60
61 Invalid values will issue a non-fatal warning.
62
63 Args:
64 node: XML node whose attributes we access.
65 attr: The attribute to access.
66 default: If the attribute is not set (value is empty), then use this.
67
68 Returns:
69 True if the attribute is a valid string representing true.
70 False if the attribute is a valid string representing false.
71 |default| otherwise.
72 """
73 value = node.getAttribute(attr)
74 s = value.lower()
75 if s == '':
76 return default
77 elif s in {'yes', 'true', '1'}:
78 return True
79 elif s in {'no', 'false', '0'}:
80 return False
81 else:
82 print('warning: manifest: %s="%s": ignoring invalid XML boolean' %
83 (attr, value), file=sys.stderr)
84 return default
85
86
87def XmlInt(node, attr, default=None):
88 """Determine integer value of |node|'s |attr|.
89
90 Args:
91 node: XML node whose attributes we access.
92 attr: The attribute to access.
93 default: If the attribute is not set (value is empty), then use this.
94
95 Returns:
96 The number if the attribute is a valid number.
97
98 Raises:
99 ManifestParseError: The number is invalid.
100 """
101 value = node.getAttribute(attr)
102 if not value:
103 return default
104
105 try:
106 return int(value)
107 except ValueError:
108 raise ManifestParseError('manifest: invalid %s="%s" integer' %
109 (attr, value))
110
111
58class _Default(object): 112class _Default(object):
59 """Project defaults within the manifest.""" 113 """Project defaults within the manifest."""
60 114
@@ -68,11 +122,16 @@ class _Default(object):
68 sync_tags = True 122 sync_tags = True
69 123
70 def __eq__(self, other): 124 def __eq__(self, other):
125 if not isinstance(other, _Default):
126 return False
71 return self.__dict__ == other.__dict__ 127 return self.__dict__ == other.__dict__
72 128
73 def __ne__(self, other): 129 def __ne__(self, other):
130 if not isinstance(other, _Default):
131 return True
74 return self.__dict__ != other.__dict__ 132 return self.__dict__ != other.__dict__
75 133
134
76class _XmlRemote(object): 135class _XmlRemote(object):
77 def __init__(self, 136 def __init__(self,
78 name, 137 name,
@@ -90,14 +149,22 @@ class _XmlRemote(object):
90 self.reviewUrl = review 149 self.reviewUrl = review
91 self.revision = revision 150 self.revision = revision
92 self.resolvedFetchUrl = self._resolveFetchUrl() 151 self.resolvedFetchUrl = self._resolveFetchUrl()
152 self.annotations = []
93 153
94 def __eq__(self, other): 154 def __eq__(self, other):
95 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)
96 161
97 def __ne__(self, other): 162 def __ne__(self, other):
98 return self.__dict__ != other.__dict__ 163 return not self.__eq__(other)
99 164
100 def _resolveFetchUrl(self): 165 def _resolveFetchUrl(self):
166 if self.fetchUrl is None:
167 return ''
101 url = self.fetchUrl.rstrip('/') 168 url = self.fetchUrl.rstrip('/')
102 manifestUrl = self.manifestUrl.rstrip('/') 169 manifestUrl = self.manifestUrl.rstrip('/')
103 # 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
@@ -126,25 +193,48 @@ class _XmlRemote(object):
126 orig_name=self.name, 193 orig_name=self.name,
127 fetchUrl=self.fetchUrl) 194 fetchUrl=self.fetchUrl)
128 195
196 def AddAnnotation(self, name, value, keep):
197 self.annotations.append(Annotation(name, value, keep))
198
199
129class XmlManifest(object): 200class XmlManifest(object):
130 """manages the repo configuration file""" 201 """manages the repo configuration file"""
131 202
132 def __init__(self, repodir): 203 def __init__(self, repodir, manifest_file, local_manifests=None):
204 """Initialize.
205
206 Args:
207 repodir: Path to the .repo/ dir for holding all internal checkout state.
208 It must be in the top directory of the repo client checkout.
209 manifest_file: Full path to the manifest file to parse. This will usually
210 be |repodir|/|MANIFEST_FILE_NAME|.
211 local_manifests: Full path to the directory of local override manifests.
212 This will usually be |repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
213 """
214 # TODO(vapier): Move this out of this class.
215 self.globalConfig = GitConfig.ForUser()
216
133 self.repodir = os.path.abspath(repodir) 217 self.repodir = os.path.abspath(repodir)
134 self.topdir = os.path.dirname(self.repodir) 218 self.topdir = os.path.dirname(self.repodir)
135 self.manifestFile = os.path.join(self.repodir, MANIFEST_FILE_NAME) 219 self.manifestFile = manifest_file
136 self.globalConfig = GitConfig.ForUser() 220 self.local_manifests = local_manifests
137 self.localManifestWarning = False
138 self.isGitcClient = False
139 self._load_local_manifests = True 221 self._load_local_manifests = True
140 222
141 self.repoProject = MetaProject(self, 'repo', 223 self.repoProject = MetaProject(self, 'repo',
142 gitdir = os.path.join(repodir, 'repo/.git'), 224 gitdir=os.path.join(repodir, 'repo/.git'),
143 worktree = os.path.join(repodir, 'repo')) 225 worktree=os.path.join(repodir, 'repo'))
144 226
145 self.manifestProject = MetaProject(self, 'manifests', 227 mp = MetaProject(self, 'manifests',
146 gitdir = os.path.join(repodir, 'manifests.git'), 228 gitdir=os.path.join(repodir, 'manifests.git'),
147 worktree = os.path.join(repodir, 'manifests')) 229 worktree=os.path.join(repodir, 'manifests'))
230 self.manifestProject = mp
231
232 # This is a bit hacky, but we're in a chicken & egg situation: all the
233 # normal repo settings live in the manifestProject which we just setup
234 # above, so we couldn't easily query before that. We assume Project()
235 # init doesn't care if this changes afterwards.
236 if os.path.exists(mp.gitdir) and mp.config.GetBoolean('repo.worktree'):
237 mp.use_git_worktrees = True
148 238
149 self._Unload() 239 self._Unload()
150 240
@@ -179,12 +269,26 @@ class XmlManifest(object):
179 """ 269 """
180 self.Override(name) 270 self.Override(name)
181 271
182 try: 272 # Old versions of repo would generate symlinks we need to clean up.
183 if os.path.lexists(self.manifestFile): 273 platform_utils.remove(self.manifestFile, missing_ok=True)
184 platform_utils.remove(self.manifestFile) 274 # This file is interpreted as if it existed inside the manifest repo.
185 platform_utils.symlink(os.path.join('manifests', name), self.manifestFile) 275 # That allows us to use <include> with the relative file name.
186 except OSError as e: 276 with open(self.manifestFile, 'w') as fp:
187 raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e))) 277 fp.write("""<?xml version="1.0" encoding="UTF-8"?>
278<!--
279DO NOT EDIT THIS FILE! It is generated by repo and changes will be discarded.
280If you want to use a different manifest, use `repo init -m <file>` instead.
281
282If you want to customize your checkout by overriding manifest settings, use
283the local_manifests/ directory instead.
284
285For more information on repo manifests, check out:
286https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
287-->
288<manifest>
289 <include name="%s" />
290</manifest>
291""" % (name,))
188 292
189 def _RemoteToXml(self, r, doc, root): 293 def _RemoteToXml(self, r, doc, root):
190 e = doc.createElement('remote') 294 e = doc.createElement('remote')
@@ -200,18 +304,28 @@ class XmlManifest(object):
200 if r.revision is not None: 304 if r.revision is not None:
201 e.setAttribute('revision', r.revision) 305 e.setAttribute('revision', r.revision)
202 306
203 def _ParseGroups(self, groups): 307 for a in r.annotations:
204 return [x for x in re.split(r'[,\s]+', groups) if x] 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
314 def _ParseList(self, field):
315 """Parse fields that contain flattened lists.
205 316
206 def Save(self, fd, peg_rev=False, peg_rev_upstream=True, groups=None): 317 These are whitespace & comma separated. Empty elements will be discarded.
207 """Write the current manifest out to the given file descriptor.
208 """ 318 """
319 return [x for x in re.split(r'[,\s]+', field) if x]
320
321 def ToXml(self, peg_rev=False, peg_rev_upstream=True, peg_rev_dest_branch=True, groups=None):
322 """Return the current manifest XML."""
209 mp = self.manifestProject 323 mp = self.manifestProject
210 324
211 if groups is None: 325 if groups is None:
212 groups = mp.config.GetString('manifest.groups') 326 groups = mp.config.GetString('manifest.groups')
213 if groups: 327 if groups:
214 groups = self._ParseGroups(groups) 328 groups = self._ParseList(groups)
215 329
216 doc = xml.dom.minidom.Document() 330 doc = xml.dom.minidom.Document()
217 root = doc.createElement('manifest') 331 root = doc.createElement('manifest')
@@ -223,7 +337,7 @@ class XmlManifest(object):
223 if self.notice: 337 if self.notice:
224 notice_element = root.appendChild(doc.createElement('notice')) 338 notice_element = root.appendChild(doc.createElement('notice'))
225 notice_lines = self.notice.splitlines() 339 notice_lines = self.notice.splitlines()
226 indented_notice = ('\n'.join(" "*4 + line for line in notice_lines))[4:] 340 indented_notice = ('\n'.join(" " * 4 + line for line in notice_lines))[4:]
227 notice_element.appendChild(doc.createTextNode(indented_notice)) 341 notice_element.appendChild(doc.createTextNode(indented_notice))
228 342
229 d = self.default 343 d = self.default
@@ -308,10 +422,19 @@ class XmlManifest(object):
308 # Only save the origin if the origin is not a sha1, and the default 422 # Only save the origin if the origin is not a sha1, and the default
309 # isn't our value 423 # isn't our value
310 e.setAttribute('upstream', p.revisionExpr) 424 e.setAttribute('upstream', p.revisionExpr)
425
426 if peg_rev_dest_branch:
427 if p.dest_branch:
428 e.setAttribute('dest-branch', p.dest_branch)
429 elif value != p.revisionExpr:
430 e.setAttribute('dest-branch', p.revisionExpr)
431
311 else: 432 else:
312 revision = self.remotes[p.remote.orig_name].revision or d.revisionExpr 433 revision = self.remotes[p.remote.orig_name].revision or d.revisionExpr
313 if not revision or revision != p.revisionExpr: 434 if not revision or revision != p.revisionExpr:
314 e.setAttribute('revision', p.revisionExpr) 435 e.setAttribute('revision', p.revisionExpr)
436 elif p.revisionId:
437 e.setAttribute('revision', p.revisionId)
315 if (p.upstream and (p.upstream != p.revisionExpr or 438 if (p.upstream and (p.upstream != p.revisionExpr or
316 p.upstream != d.upstreamExpr)): 439 p.upstream != d.upstreamExpr)):
317 e.setAttribute('upstream', p.upstream) 440 e.setAttribute('upstream', p.upstream)
@@ -372,11 +495,84 @@ class XmlManifest(object):
372 ' '.join(self._repo_hooks_project.enabled_repo_hooks)) 495 ' '.join(self._repo_hooks_project.enabled_repo_hooks))
373 root.appendChild(e) 496 root.appendChild(e)
374 497
498 if self._superproject:
499 root.appendChild(doc.createTextNode(''))
500 e = doc.createElement('superproject')
501 e.setAttribute('name', self._superproject['name'])
502 remoteName = None
503 if d.remote:
504 remoteName = d.remote.name
505 remote = self._superproject.get('remote')
506 if not d.remote or remote.orig_name != remoteName:
507 remoteName = remote.orig_name
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)
518 root.appendChild(e)
519
520 return doc
521
522 def ToDict(self, **kwargs):
523 """Return the current manifest as a dictionary."""
524 # Elements that may only appear once.
525 SINGLE_ELEMENTS = {
526 'notice',
527 'default',
528 'manifest-server',
529 'repo-hooks',
530 'superproject',
531 'contactinfo',
532 }
533 # Elements that may be repeated.
534 MULTI_ELEMENTS = {
535 'remote',
536 'remove-project',
537 'project',
538 'extend-project',
539 'include',
540 # These are children of 'project' nodes.
541 'annotation',
542 'project',
543 'copyfile',
544 'linkfile',
545 }
546
547 doc = self.ToXml(**kwargs)
548 ret = {}
549
550 def append_children(ret, node):
551 for child in node.childNodes:
552 if child.nodeType == xml.dom.Node.ELEMENT_NODE:
553 attrs = child.attributes
554 element = dict((attrs.item(i).localName, attrs.item(i).value)
555 for i in range(attrs.length))
556 if child.nodeName in SINGLE_ELEMENTS:
557 ret[child.nodeName] = element
558 elif child.nodeName in MULTI_ELEMENTS:
559 ret.setdefault(child.nodeName, []).append(element)
560 else:
561 raise ManifestParseError('Unhandled element "%s"' % (child.nodeName,))
562
563 append_children(element, child)
564
565 append_children(ret, doc.firstChild)
566
567 return ret
568
569 def Save(self, fd, **kwargs):
570 """Write the current manifest out to the given file descriptor."""
571 doc = self.ToXml(**kwargs)
375 doc.writexml(fd, '', ' ', '\n', 'UTF-8') 572 doc.writexml(fd, '', ' ', '\n', 'UTF-8')
376 573
377 def _output_manifest_project_extras(self, p, e): 574 def _output_manifest_project_extras(self, p, e):
378 """Manifests can modify e if they support extra project attributes.""" 575 """Manifests can modify e if they support extra project attributes."""
379 pass
380 576
381 @property 577 @property
382 def paths(self): 578 def paths(self):
@@ -404,6 +600,16 @@ class XmlManifest(object):
404 return self._repo_hooks_project 600 return self._repo_hooks_project
405 601
406 @property 602 @property
603 def superproject(self):
604 self._Load()
605 return self._superproject
606
607 @property
608 def contactinfo(self):
609 self._Load()
610 return self._contactinfo
611
612 @property
407 def notice(self): 613 def notice(self):
408 self._Load() 614 self._Load()
409 return self._notice 615 return self._notice
@@ -414,16 +620,45 @@ class XmlManifest(object):
414 return self._manifest_server 620 return self._manifest_server
415 621
416 @property 622 @property
623 def CloneBundle(self):
624 clone_bundle = self.manifestProject.config.GetBoolean('repo.clonebundle')
625 if clone_bundle is None:
626 return False if self.manifestProject.config.GetBoolean('repo.partialclone') else True
627 else:
628 return clone_bundle
629
630 @property
417 def CloneFilter(self): 631 def CloneFilter(self):
418 if self.manifestProject.config.GetBoolean('repo.partialclone'): 632 if self.manifestProject.config.GetBoolean('repo.partialclone'):
419 return self.manifestProject.config.GetString('repo.clonefilter') 633 return self.manifestProject.config.GetString('repo.clonefilter')
420 return None 634 return None
421 635
422 @property 636 @property
637 def PartialCloneExclude(self):
638 exclude = self.manifest.manifestProject.config.GetString(
639 'repo.partialcloneexclude') or ''
640 return set(x.strip() for x in exclude.split(','))
641
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
423 def IsMirror(self): 654 def IsMirror(self):
424 return self.manifestProject.config.GetBoolean('repo.mirror') 655 return self.manifestProject.config.GetBoolean('repo.mirror')
425 656
426 @property 657 @property
658 def UseGitWorktrees(self):
659 return self.manifestProject.config.GetBoolean('repo.worktree')
660
661 @property
427 def IsArchive(self): 662 def IsArchive(self):
428 return self.manifestProject.config.GetBoolean('repo.archive') 663 return self.manifestProject.config.GetBoolean('repo.archive')
429 664
@@ -431,6 +666,17 @@ class XmlManifest(object):
431 def HasSubmodules(self): 666 def HasSubmodules(self):
432 return self.manifestProject.config.GetBoolean('repo.submodules') 667 return self.manifestProject.config.GetBoolean('repo.submodules')
433 668
669 def GetDefaultGroupsStr(self):
670 """Returns the default group string for the platform."""
671 return 'default,platform-' + platform.system().lower()
672
673 def GetGroupsStr(self):
674 """Returns the manifest group string that should be synced."""
675 groups = self.manifestProject.config.GetString('manifest.groups')
676 if not groups:
677 groups = self.GetDefaultGroupsStr()
678 return groups
679
434 def _Unload(self): 680 def _Unload(self):
435 self._loaded = False 681 self._loaded = False
436 self._projects = {} 682 self._projects = {}
@@ -438,6 +684,8 @@ class XmlManifest(object):
438 self._remotes = {} 684 self._remotes = {}
439 self._default = None 685 self._default = None
440 self._repo_hooks_project = None 686 self._repo_hooks_project = None
687 self._superproject = {}
688 self._contactinfo = ContactInfo(Wrapper().BUG_URL)
441 self._notice = None 689 self._notice = None
442 self.branch = None 690 self.branch = None
443 self._manifest_server = None 691 self._manifest_server = None
@@ -450,28 +698,24 @@ class XmlManifest(object):
450 b = b[len(R_HEADS):] 698 b = b[len(R_HEADS):]
451 self.branch = b 699 self.branch = b
452 700
701 # The manifestFile was specified by the user which is why we allow include
702 # paths to point anywhere.
453 nodes = [] 703 nodes = []
454 nodes.append(self._ParseManifestXml(self.manifestFile, 704 nodes.append(self._ParseManifestXml(
455 self.manifestProject.worktree)) 705 self.manifestFile, self.manifestProject.worktree,
456 706 restrict_includes=False))
457 if self._load_local_manifests: 707
458 local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME) 708 if self._load_local_manifests and self.local_manifests:
459 if os.path.exists(local):
460 if not self.localManifestWarning:
461 self.localManifestWarning = True
462 print('warning: %s is deprecated; put local manifests '
463 'in `%s` instead' % (LOCAL_MANIFEST_NAME,
464 os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME)),
465 file=sys.stderr)
466 nodes.append(self._ParseManifestXml(local, self.repodir))
467
468 local_dir = os.path.abspath(os.path.join(self.repodir,
469 LOCAL_MANIFESTS_DIR_NAME))
470 try: 709 try:
471 for local_file in sorted(platform_utils.listdir(local_dir)): 710 for local_file in sorted(platform_utils.listdir(self.local_manifests)):
472 if local_file.endswith('.xml'): 711 if local_file.endswith('.xml'):
473 local = os.path.join(local_dir, local_file) 712 local = os.path.join(self.local_manifests, local_file)
474 nodes.append(self._ParseManifestXml(local, self.repodir)) 713 # Since local manifests are entirely managed by the user, allow
714 # them to point anywhere the user wants.
715 nodes.append(self._ParseManifestXml(
716 local, self.repodir,
717 parent_groups=f'{LOCAL_MANIFEST_GROUP_PREFIX}:{local_file[:-4]}',
718 restrict_includes=False))
475 except OSError: 719 except OSError:
476 pass 720 pass
477 721
@@ -489,7 +733,19 @@ class XmlManifest(object):
489 733
490 self._loaded = True 734 self._loaded = True
491 735
492 def _ParseManifestXml(self, path, include_root): 736 def _ParseManifestXml(self, path, include_root, parent_groups='',
737 restrict_includes=True):
738 """Parse a manifest XML and return the computed nodes.
739
740 Args:
741 path: The XML file to read & parse.
742 include_root: The path to interpret include "name"s relative to.
743 parent_groups: The groups to apply to this projects.
744 restrict_includes: Whether to constrain the "name" attribute of includes.
745
746 Returns:
747 List of XML nodes.
748 """
493 try: 749 try:
494 root = xml.dom.minidom.parse(path) 750 root = xml.dom.minidom.parse(path)
495 except (OSError, xml.parsers.expat.ExpatError) as e: 751 except (OSError, xml.parsers.expat.ExpatError) as e:
@@ -508,20 +764,35 @@ class XmlManifest(object):
508 for node in manifest.childNodes: 764 for node in manifest.childNodes:
509 if node.nodeName == 'include': 765 if node.nodeName == 'include':
510 name = self._reqatt(node, 'name') 766 name = self._reqatt(node, 'name')
767 if restrict_includes:
768 msg = self._CheckLocalPath(name)
769 if msg:
770 raise ManifestInvalidPathError(
771 '<include> invalid "name": %s: %s' % (name, msg))
772 include_groups = ''
773 if parent_groups:
774 include_groups = parent_groups
775 if node.hasAttribute('groups'):
776 include_groups = node.getAttribute('groups') + ',' + include_groups
511 fp = os.path.join(include_root, name) 777 fp = os.path.join(include_root, name)
512 if not os.path.isfile(fp): 778 if not os.path.isfile(fp):
513 raise ManifestParseError("include %s doesn't exist or isn't a file" 779 raise ManifestParseError("include [%s/]%s doesn't exist or isn't a file"
514 % (name,)) 780 % (include_root, name))
515 try: 781 try:
516 nodes.extend(self._ParseManifestXml(fp, include_root)) 782 nodes.extend(self._ParseManifestXml(fp, include_root, include_groups))
517 # should isolate this to the exact exception, but that's 783 # should isolate this to the exact exception, but that's
518 # tricky. actual parsing implementation may vary. 784 # tricky. actual parsing implementation may vary.
519 except (KeyboardInterrupt, RuntimeError, SystemExit): 785 except (KeyboardInterrupt, RuntimeError, SystemExit, ManifestParseError):
520 raise 786 raise
521 except Exception as e: 787 except Exception as e:
522 raise ManifestParseError( 788 raise ManifestParseError(
523 "failed parsing included manifest %s: %s" % (name, e)) 789 "failed parsing included manifest %s: %s" % (name, e))
524 else: 790 else:
791 if parent_groups and node.nodeName == 'project':
792 nodeGroups = parent_groups
793 if node.hasAttribute('groups'):
794 nodeGroups = node.getAttribute('groups') + ',' + nodeGroups
795 node.setAttribute('groups', nodeGroups)
525 nodes.append(node) 796 nodes.append(node)
526 return nodes 797 return nodes
527 798
@@ -541,9 +812,10 @@ class XmlManifest(object):
541 for node in itertools.chain(*node_list): 812 for node in itertools.chain(*node_list):
542 if node.nodeName == 'default': 813 if node.nodeName == 'default':
543 new_default = self._ParseDefault(node) 814 new_default = self._ParseDefault(node)
815 emptyDefault = not node.hasAttributes() and not node.hasChildNodes()
544 if self._default is None: 816 if self._default is None:
545 self._default = new_default 817 self._default = new_default
546 elif new_default != self._default: 818 elif not emptyDefault and new_default != self._default:
547 raise ManifestParseError('duplicate default in %s' % 819 raise ManifestParseError('duplicate default in %s' %
548 (self.manifestFile)) 820 (self.manifestFile))
549 821
@@ -582,6 +854,8 @@ class XmlManifest(object):
582 for subproject in project.subprojects: 854 for subproject in project.subprojects:
583 recursively_add_projects(subproject) 855 recursively_add_projects(subproject)
584 856
857 repo_hooks_project = None
858 enabled_repo_hooks = None
585 for node in itertools.chain(*node_list): 859 for node in itertools.chain(*node_list):
586 if node.nodeName == 'project': 860 if node.nodeName == 'project':
587 project = self._ParseProject(node) 861 project = self._ParseProject(node)
@@ -594,61 +868,108 @@ class XmlManifest(object):
594 'project: %s' % name) 868 'project: %s' % name)
595 869
596 path = node.getAttribute('path') 870 path = node.getAttribute('path')
871 dest_path = node.getAttribute('dest-path')
597 groups = node.getAttribute('groups') 872 groups = node.getAttribute('groups')
598 if groups: 873 if groups:
599 groups = self._ParseGroups(groups) 874 groups = self._ParseList(groups)
600 revision = node.getAttribute('revision') 875 revision = node.getAttribute('revision')
876 remote = node.getAttribute('remote')
877 if remote:
878 remote = self._get_remote(node)
601 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)
602 for p in self._projects[name]: 884 for p in self._projects[name]:
603 if path and p.relpath != path: 885 if path and p.relpath != path:
604 continue 886 continue
605 if groups: 887 if groups:
606 p.groups.extend(groups) 888 p.groups.extend(groups)
607 if revision: 889 if revision:
608 p.revisionExpr = revision 890 p.SetRevision(revision)
609 if node.nodeName == 'repo-hooks': 891
610 # Get the name of the project and the (space-separated) list of enabled. 892 if remote:
611 repo_hooks_project = self._reqatt(node, 'in-project') 893 p.remote = remote.ToRemoteSpec(name)
612 enabled_repo_hooks = self._reqatt(node, 'enabled-list').split() 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
613 900
901 if node.nodeName == 'repo-hooks':
614 # Only one project can be the hooks project 902 # Only one project can be the hooks project
615 if self._repo_hooks_project is not None: 903 if repo_hooks_project is not None:
616 raise ManifestParseError( 904 raise ManifestParseError(
617 'duplicate repo-hooks in %s' % 905 'duplicate repo-hooks in %s' %
618 (self.manifestFile)) 906 (self.manifestFile))
619 907
620 # Store a reference to the Project. 908 # Get the name of the project and the (space-separated) list of enabled.
621 try: 909 repo_hooks_project = self._reqatt(node, 'in-project')
622 repo_hooks_projects = self._projects[repo_hooks_project] 910 enabled_repo_hooks = self._ParseList(self._reqatt(node, 'enabled-list'))
623 except KeyError: 911 if node.nodeName == 'superproject':
624 raise ManifestParseError( 912 name = self._reqatt(node, 'name')
625 'project %s not found for repo-hooks' % 913 # There can only be one superproject.
626 (repo_hooks_project)) 914 if self._superproject.get('name'):
627
628 if len(repo_hooks_projects) != 1:
629 raise ManifestParseError( 915 raise ManifestParseError(
630 'internal error parsing repo-hooks in %s' % 916 'duplicate superproject in %s' %
631 (self.manifestFile)) 917 (self.manifestFile))
632 self._repo_hooks_project = repo_hooks_projects[0] 918 self._superproject['name'] = name
919 remote_name = node.getAttribute('remote')
920 if not remote_name:
921 remote = self._default.remote
922 else:
923 remote = self._get_remote(node)
924 if remote is None:
925 raise ManifestParseError("no remote for superproject %s within %s" %
926 (name, self.manifestFile))
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)
633 939
634 # Store the enabled hooks in the Project object.
635 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
636 if node.nodeName == 'remove-project': 940 if node.nodeName == 'remove-project':
637 name = self._reqatt(node, 'name') 941 name = self._reqatt(node, 'name')
638 942
639 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):
640 raise ManifestParseError('remove-project element specifies non-existent ' 953 raise ManifestParseError('remove-project element specifies non-existent '
641 'project: %s' % name) 954 'project: %s' % name)
642 955
643 for p in self._projects[name]: 956 # Store repo hooks project information.
644 del self._paths[p.relpath] 957 if repo_hooks_project:
645 del self._projects[name] 958 # Store a reference to the Project.
646 959 try:
647 # If the manifest removes the hooks project, treat it as if it deleted 960 repo_hooks_projects = self._projects[repo_hooks_project]
648 # the repo-hooks element too. 961 except KeyError:
649 if self._repo_hooks_project and (self._repo_hooks_project.name == name): 962 raise ManifestParseError(
650 self._repo_hooks_project = None 963 'project %s not found for repo-hooks' %
964 (repo_hooks_project))
651 965
966 if len(repo_hooks_projects) != 1:
967 raise ManifestParseError(
968 'internal error parsing repo-hooks in %s' %
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
652 973
653 def _AddMetaProjectMirror(self, m): 974 def _AddMetaProjectMirror(self, m):
654 name = None 975 name = None
@@ -676,15 +997,15 @@ class XmlManifest(object):
676 if name not in self._projects: 997 if name not in self._projects:
677 m.PreSync() 998 m.PreSync()
678 gitdir = os.path.join(self.topdir, '%s.git' % name) 999 gitdir = os.path.join(self.topdir, '%s.git' % name)
679 project = Project(manifest = self, 1000 project = Project(manifest=self,
680 name = name, 1001 name=name,
681 remote = remote.ToRemoteSpec(name), 1002 remote=remote.ToRemoteSpec(name),
682 gitdir = gitdir, 1003 gitdir=gitdir,
683 objdir = gitdir, 1004 objdir=gitdir,
684 worktree = None, 1005 worktree=None,
685 relpath = name or None, 1006 relpath=name or None,
686 revisionExpr = m.revisionExpr, 1007 revisionExpr=m.revisionExpr,
687 revisionId = None) 1008 revisionId=None)
688 self._projects[project.name] = [project] 1009 self._projects[project.name] = [project]
689 self._paths[project.relpath] = project 1010 self._paths[project.relpath] = project
690 1011
@@ -707,7 +1028,14 @@ class XmlManifest(object):
707 if revision == '': 1028 if revision == '':
708 revision = None 1029 revision = None
709 manifestUrl = self.manifestProject.config.GetString('remote.origin.url') 1030 manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
710 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
711 1039
712 def _ParseDefault(self, node): 1040 def _ParseDefault(self, node):
713 """ 1041 """
@@ -722,29 +1050,14 @@ class XmlManifest(object):
722 d.destBranchExpr = node.getAttribute('dest-branch') or None 1050 d.destBranchExpr = node.getAttribute('dest-branch') or None
723 d.upstreamExpr = node.getAttribute('upstream') or None 1051 d.upstreamExpr = node.getAttribute('upstream') or None
724 1052
725 sync_j = node.getAttribute('sync-j') 1053 d.sync_j = XmlInt(node, 'sync-j', 1)
726 if sync_j == '' or sync_j is None: 1054 if d.sync_j <= 0:
727 d.sync_j = 1 1055 raise ManifestParseError('%s: sync-j must be greater than 0, not "%s"' %
728 else: 1056 (self.manifestFile, d.sync_j))
729 d.sync_j = int(sync_j)
730
731 sync_c = node.getAttribute('sync-c')
732 if not sync_c:
733 d.sync_c = False
734 else:
735 d.sync_c = sync_c.lower() in ("yes", "true", "1")
736 1057
737 sync_s = node.getAttribute('sync-s') 1058 d.sync_c = XmlBool(node, 'sync-c', False)
738 if not sync_s: 1059 d.sync_s = XmlBool(node, 'sync-s', False)
739 d.sync_s = False 1060 d.sync_tags = XmlBool(node, 'sync-tags', True)
740 else:
741 d.sync_s = sync_s.lower() in ("yes", "true", "1")
742
743 sync_tags = node.getAttribute('sync-tags')
744 if not sync_tags:
745 d.sync_tags = True
746 else:
747 d.sync_tags = sync_tags.lower() in ("yes", "true", "1")
748 return d 1061 return d
749 1062
750 def _ParseNotice(self, node): 1063 def _ParseNotice(self, node):
@@ -792,11 +1105,15 @@ class XmlManifest(object):
792 def _UnjoinName(self, parent_name, name): 1105 def _UnjoinName(self, parent_name, name):
793 return os.path.relpath(name, parent_name) 1106 return os.path.relpath(name, parent_name)
794 1107
795 def _ParseProject(self, node, parent = None, **extra_proj_attrs): 1108 def _ParseProject(self, node, parent=None, **extra_proj_attrs):
796 """ 1109 """
797 reads a <project> element from the manifest file 1110 reads a <project> element from the manifest file
798 """ 1111 """
799 name = self._reqatt(node, 'name') 1112 name = self._reqatt(node, 'name')
1113 msg = self._CheckLocalPath(name, dir_ok=True)
1114 if msg:
1115 raise ManifestInvalidPathError(
1116 '<project> invalid "name": %s: %s' % (name, msg))
800 if parent: 1117 if parent:
801 name = self._JoinName(parent.name, name) 1118 name = self._JoinName(parent.name, name)
802 1119
@@ -805,55 +1122,34 @@ class XmlManifest(object):
805 remote = self._default.remote 1122 remote = self._default.remote
806 if remote is None: 1123 if remote is None:
807 raise ManifestParseError("no remote for project %s within %s" % 1124 raise ManifestParseError("no remote for project %s within %s" %
808 (name, self.manifestFile)) 1125 (name, self.manifestFile))
809 1126
810 revisionExpr = node.getAttribute('revision') or remote.revision 1127 revisionExpr = node.getAttribute('revision') or remote.revision
811 if not revisionExpr: 1128 if not revisionExpr:
812 revisionExpr = self._default.revisionExpr 1129 revisionExpr = self._default.revisionExpr
813 if not revisionExpr: 1130 if not revisionExpr:
814 raise ManifestParseError("no revision for project %s within %s" % 1131 raise ManifestParseError("no revision for project %s within %s" %
815 (name, self.manifestFile)) 1132 (name, self.manifestFile))
816 1133
817 path = node.getAttribute('path') 1134 path = node.getAttribute('path')
818 if not path: 1135 if not path:
819 path = name 1136 path = name
820 if path.startswith('/'):
821 raise ManifestParseError("project %s path cannot be absolute in %s" %
822 (name, self.manifestFile))
823
824 rebase = node.getAttribute('rebase')
825 if not rebase:
826 rebase = True
827 else: 1137 else:
828 rebase = rebase.lower() in ("yes", "true", "1") 1138 # NB: The "." project is handled specially in Project.Sync_LocalHalf.
829 1139 msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
830 sync_c = node.getAttribute('sync-c') 1140 if msg:
831 if not sync_c: 1141 raise ManifestInvalidPathError(
832 sync_c = False 1142 '<project> invalid "path": %s: %s' % (path, msg))
833 else: 1143
834 sync_c = sync_c.lower() in ("yes", "true", "1") 1144 rebase = XmlBool(node, 'rebase', True)
835 1145 sync_c = XmlBool(node, 'sync-c', False)
836 sync_s = node.getAttribute('sync-s') 1146 sync_s = XmlBool(node, 'sync-s', self._default.sync_s)
837 if not sync_s: 1147 sync_tags = XmlBool(node, 'sync-tags', self._default.sync_tags)
838 sync_s = self._default.sync_s 1148
839 else: 1149 clone_depth = XmlInt(node, 'clone-depth')
840 sync_s = sync_s.lower() in ("yes", "true", "1") 1150 if clone_depth is not None and clone_depth <= 0:
841 1151 raise ManifestParseError('%s: clone-depth must be greater than 0, not "%s"' %
842 sync_tags = node.getAttribute('sync-tags') 1152 (self.manifestFile, clone_depth))
843 if not sync_tags:
844 sync_tags = self._default.sync_tags
845 else:
846 sync_tags = sync_tags.lower() in ("yes", "true", "1")
847
848 clone_depth = node.getAttribute('clone-depth')
849 if clone_depth:
850 try:
851 clone_depth = int(clone_depth)
852 if clone_depth <= 0:
853 raise ValueError()
854 except ValueError:
855 raise ManifestParseError('invalid clone-depth %s in %s' %
856 (clone_depth, self.manifestFile))
857 1153
858 dest_branch = node.getAttribute('dest-branch') or self._default.destBranchExpr 1154 dest_branch = node.getAttribute('dest-branch') or self._default.destBranchExpr
859 1155
@@ -862,11 +1158,13 @@ class XmlManifest(object):
862 groups = '' 1158 groups = ''
863 if node.hasAttribute('groups'): 1159 if node.hasAttribute('groups'):
864 groups = node.getAttribute('groups') 1160 groups = node.getAttribute('groups')
865 groups = self._ParseGroups(groups) 1161 groups = self._ParseList(groups)
866 1162
867 if parent is None: 1163 if parent is None:
868 relpath, worktree, gitdir, objdir = self.GetProjectPaths(name, path) 1164 relpath, worktree, gitdir, objdir, use_git_worktrees = \
1165 self.GetProjectPaths(name, path)
869 else: 1166 else:
1167 use_git_worktrees = False
870 relpath, worktree, gitdir, objdir = \ 1168 relpath, worktree, gitdir, objdir = \
871 self.GetSubprojectPaths(parent, name, path) 1169 self.GetSubprojectPaths(parent, name, path)
872 1170
@@ -874,27 +1172,28 @@ class XmlManifest(object):
874 groups.extend(set(default_groups).difference(groups)) 1172 groups.extend(set(default_groups).difference(groups))
875 1173
876 if self.IsMirror and node.hasAttribute('force-path'): 1174 if self.IsMirror and node.hasAttribute('force-path'):
877 if node.getAttribute('force-path').lower() in ("yes", "true", "1"): 1175 if XmlBool(node, 'force-path', False):
878 gitdir = os.path.join(self.topdir, '%s.git' % path) 1176 gitdir = os.path.join(self.topdir, '%s.git' % path)
879 1177
880 project = Project(manifest = self, 1178 project = Project(manifest=self,
881 name = name, 1179 name=name,
882 remote = remote.ToRemoteSpec(name), 1180 remote=remote.ToRemoteSpec(name),
883 gitdir = gitdir, 1181 gitdir=gitdir,
884 objdir = objdir, 1182 objdir=objdir,
885 worktree = worktree, 1183 worktree=worktree,
886 relpath = relpath, 1184 relpath=relpath,
887 revisionExpr = revisionExpr, 1185 revisionExpr=revisionExpr,
888 revisionId = None, 1186 revisionId=None,
889 rebase = rebase, 1187 rebase=rebase,
890 groups = groups, 1188 groups=groups,
891 sync_c = sync_c, 1189 sync_c=sync_c,
892 sync_s = sync_s, 1190 sync_s=sync_s,
893 sync_tags = sync_tags, 1191 sync_tags=sync_tags,
894 clone_depth = clone_depth, 1192 clone_depth=clone_depth,
895 upstream = upstream, 1193 upstream=upstream,
896 parent = parent, 1194 parent=parent,
897 dest_branch = dest_branch, 1195 dest_branch=dest_branch,
1196 use_git_worktrees=use_git_worktrees,
898 **extra_proj_attrs) 1197 **extra_proj_attrs)
899 1198
900 for n in node.childNodes: 1199 for n in node.childNodes:
@@ -905,11 +1204,16 @@ class XmlManifest(object):
905 if n.nodeName == 'annotation': 1204 if n.nodeName == 'annotation':
906 self._ParseAnnotation(project, n) 1205 self._ParseAnnotation(project, n)
907 if n.nodeName == 'project': 1206 if n.nodeName == 'project':
908 project.subprojects.append(self._ParseProject(n, parent = project)) 1207 project.subprojects.append(self._ParseProject(n, parent=project))
909 1208
910 return project 1209 return project
911 1210
912 def GetProjectPaths(self, name, path): 1211 def GetProjectPaths(self, name, path):
1212 # The manifest entries might have trailing slashes. Normalize them to avoid
1213 # unexpected filesystem behavior since we do string concatenation below.
1214 path = path.rstrip('/')
1215 name = name.rstrip('/')
1216 use_git_worktrees = False
913 relpath = path 1217 relpath = path
914 if self.IsMirror: 1218 if self.IsMirror:
915 worktree = None 1219 worktree = None
@@ -918,8 +1222,15 @@ class XmlManifest(object):
918 else: 1222 else:
919 worktree = os.path.join(self.topdir, path).replace('\\', '/') 1223 worktree = os.path.join(self.topdir, path).replace('\\', '/')
920 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path) 1224 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path)
921 objdir = os.path.join(self.repodir, 'project-objects', '%s.git' % name) 1225 # We allow people to mix git worktrees & non-git worktrees for now.
922 return relpath, worktree, gitdir, objdir 1226 # This allows for in situ migration of repo clients.
1227 if os.path.exists(gitdir) or not self.UseGitWorktrees:
1228 objdir = os.path.join(self.repodir, 'project-objects', '%s.git' % name)
1229 else:
1230 use_git_worktrees = True
1231 gitdir = os.path.join(self.repodir, 'worktrees', '%s.git' % name)
1232 objdir = gitdir
1233 return relpath, worktree, gitdir, objdir, use_git_worktrees
923 1234
924 def GetProjectsWithName(self, name): 1235 def GetProjectsWithName(self, name):
925 return self._projects.get(name, []) 1236 return self._projects.get(name, [])
@@ -934,6 +1245,10 @@ class XmlManifest(object):
934 return os.path.relpath(relpath, parent_relpath) 1245 return os.path.relpath(relpath, parent_relpath)
935 1246
936 def GetSubprojectPaths(self, parent, name, path): 1247 def GetSubprojectPaths(self, parent, name, path):
1248 # The manifest entries might have trailing slashes. Normalize them to avoid
1249 # unexpected filesystem behavior since we do string concatenation below.
1250 path = path.rstrip('/')
1251 name = name.rstrip('/')
937 relpath = self._JoinRelpath(parent.relpath, path) 1252 relpath = self._JoinRelpath(parent.relpath, path)
938 gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path) 1253 gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path)
939 objdir = os.path.join(parent.gitdir, 'subproject-objects', '%s.git' % name) 1254 objdir = os.path.join(parent.gitdir, 'subproject-objects', '%s.git' % name)
@@ -943,23 +1258,151 @@ class XmlManifest(object):
943 worktree = os.path.join(parent.worktree, path).replace('\\', '/') 1258 worktree = os.path.join(parent.worktree, path).replace('\\', '/')
944 return relpath, worktree, gitdir, objdir 1259 return relpath, worktree, gitdir, objdir
945 1260
1261 @staticmethod
1262 def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False):
1263 """Verify |path| is reasonable for use in filesystem paths.
1264
1265 Used with <copyfile> & <linkfile> & <project> elements.
1266
1267 This only validates the |path| in isolation: it does not check against the
1268 current filesystem state. Thus it is suitable as a first-past in a parser.
1269
1270 It enforces a number of constraints:
1271 * No empty paths.
1272 * No "~" in paths.
1273 * No Unicode codepoints that filesystems might elide when normalizing.
1274 * No relative path components like "." or "..".
1275 * No absolute paths.
1276 * No ".git" or ".repo*" path components.
1277
1278 Args:
1279 path: The path name to validate.
1280 dir_ok: Whether |path| may force a directory (e.g. end in a /).
1281 cwd_dot_ok: Whether |path| may be just ".".
1282
1283 Returns:
1284 None if |path| is OK, a failure message otherwise.
1285 """
1286 if not path:
1287 return 'empty paths not allowed'
1288
1289 if '~' in path:
1290 return '~ not allowed (due to 8.3 filenames on Windows filesystems)'
1291
1292 path_codepoints = set(path)
1293
1294 # Some filesystems (like Apple's HFS+) try to normalize Unicode codepoints
1295 # which means there are alternative names for ".git". Reject paths with
1296 # these in it as there shouldn't be any reasonable need for them here.
1297 # The set of codepoints here was cribbed from jgit's implementation:
1298 # https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
1299 BAD_CODEPOINTS = {
1300 u'\u200C', # ZERO WIDTH NON-JOINER
1301 u'\u200D', # ZERO WIDTH JOINER
1302 u'\u200E', # LEFT-TO-RIGHT MARK
1303 u'\u200F', # RIGHT-TO-LEFT MARK
1304 u'\u202A', # LEFT-TO-RIGHT EMBEDDING
1305 u'\u202B', # RIGHT-TO-LEFT EMBEDDING
1306 u'\u202C', # POP DIRECTIONAL FORMATTING
1307 u'\u202D', # LEFT-TO-RIGHT OVERRIDE
1308 u'\u202E', # RIGHT-TO-LEFT OVERRIDE
1309 u'\u206A', # INHIBIT SYMMETRIC SWAPPING
1310 u'\u206B', # ACTIVATE SYMMETRIC SWAPPING
1311 u'\u206C', # INHIBIT ARABIC FORM SHAPING
1312 u'\u206D', # ACTIVATE ARABIC FORM SHAPING
1313 u'\u206E', # NATIONAL DIGIT SHAPES
1314 u'\u206F', # NOMINAL DIGIT SHAPES
1315 u'\uFEFF', # ZERO WIDTH NO-BREAK SPACE
1316 }
1317 if BAD_CODEPOINTS & path_codepoints:
1318 # This message is more expansive than reality, but should be fine.
1319 return 'Unicode combining characters not allowed'
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
1328 # Assume paths might be used on case-insensitive filesystems.
1329 path = path.lower()
1330
1331 # Split up the path by its components. We can't use os.path.sep exclusively
1332 # as some platforms (like Windows) will convert / to \ and that bypasses all
1333 # our constructed logic here. Especially since manifest authors only use
1334 # / in their paths.
1335 resep = re.compile(r'[/%s]' % re.escape(os.path.sep))
1336 # Strip off trailing slashes as those only produce '' elements, and we use
1337 # parts to look for individual bad components.
1338 parts = resep.split(path.rstrip('/'))
1339
1340 # Some people use src="." to create stable links to projects. Lets allow
1341 # that but reject all other uses of "." to keep things simple.
1342 if not cwd_dot_ok or parts != ['.']:
1343 for part in set(parts):
1344 if part in {'.', '..', '.git'} or part.startswith('.repo'):
1345 return 'bad component: %s' % (part,)
1346
1347 if not dir_ok and resep.match(path[-1]):
1348 return 'dirs not allowed'
1349
1350 # NB: The two abspath checks here are to handle platforms with multiple
1351 # filesystem path styles (e.g. Windows).
1352 norm = os.path.normpath(path)
1353 if (norm == '..' or
1354 (len(norm) >= 3 and norm.startswith('..') and resep.match(norm[0])) or
1355 os.path.isabs(norm) or
1356 norm.startswith('/')):
1357 return 'path cannot be outside'
1358
1359 @classmethod
1360 def _ValidateFilePaths(cls, element, src, dest):
1361 """Verify |src| & |dest| are reasonable for <copyfile> & <linkfile>.
1362
1363 We verify the path independent of any filesystem state as we won't have a
1364 checkout available to compare to. i.e. This is for parsing validation
1365 purposes only.
1366
1367 We'll do full/live sanity checking before we do the actual filesystem
1368 modifications in _CopyFile/_LinkFile/etc...
1369 """
1370 # |dest| is the file we write to or symlink we create.
1371 # It is relative to the top of the repo client checkout.
1372 msg = cls._CheckLocalPath(dest)
1373 if msg:
1374 raise ManifestInvalidPathError(
1375 '<%s> invalid "dest": %s: %s' % (element, dest, msg))
1376
1377 # |src| is the file we read from or path we point to for symlinks.
1378 # It is relative to the top of the git project checkout.
1379 is_linkfile = element == 'linkfile'
1380 msg = cls._CheckLocalPath(src, dir_ok=is_linkfile, cwd_dot_ok=is_linkfile)
1381 if msg:
1382 raise ManifestInvalidPathError(
1383 '<%s> invalid "src": %s: %s' % (element, src, msg))
1384
946 def _ParseCopyFile(self, project, node): 1385 def _ParseCopyFile(self, project, node):
947 src = self._reqatt(node, 'src') 1386 src = self._reqatt(node, 'src')
948 dest = self._reqatt(node, 'dest') 1387 dest = self._reqatt(node, 'dest')
949 if not self.IsMirror: 1388 if not self.IsMirror:
950 # src is project relative; 1389 # src is project relative;
951 # dest is relative to the top of the tree 1390 # dest is relative to the top of the tree.
952 project.AddCopyFile(src, dest, os.path.join(self.topdir, dest)) 1391 # We only validate paths if we actually plan to process them.
1392 self._ValidateFilePaths('copyfile', src, dest)
1393 project.AddCopyFile(src, dest, self.topdir)
953 1394
954 def _ParseLinkFile(self, project, node): 1395 def _ParseLinkFile(self, project, node):
955 src = self._reqatt(node, 'src') 1396 src = self._reqatt(node, 'src')
956 dest = self._reqatt(node, 'dest') 1397 dest = self._reqatt(node, 'dest')
957 if not self.IsMirror: 1398 if not self.IsMirror:
958 # src is project relative; 1399 # src is project relative;
959 # dest is relative to the top of the tree 1400 # dest is relative to the top of the tree.
960 project.AddLinkFile(src, dest, os.path.join(self.topdir, dest)) 1401 # We only validate paths if we actually plan to process them.
1402 self._ValidateFilePaths('linkfile', src, dest)
1403 project.AddLinkFile(src, dest, self.topdir)
961 1404
962 def _ParseAnnotation(self, project, node): 1405 def _ParseAnnotation(self, element, node):
963 name = self._reqatt(node, 'name') 1406 name = self._reqatt(node, 'name')
964 value = self._reqatt(node, 'value') 1407 value = self._reqatt(node, 'value')
965 try: 1408 try:
@@ -968,8 +1411,8 @@ class XmlManifest(object):
968 keep = "true" 1411 keep = "true"
969 if keep != "true" and keep != "false": 1412 if keep != "true" and keep != "false":
970 raise ManifestParseError('optional "keep" attribute must be ' 1413 raise ManifestParseError('optional "keep" attribute must be '
971 '"true" or "false"') 1414 '"true" or "false"')
972 project.AddAnnotation(name, value, keep) 1415 element.AddAnnotation(name, value, keep)
973 1416
974 def _get_remote(self, node): 1417 def _get_remote(self, node):
975 name = node.getAttribute('remote') 1418 name = node.getAttribute('remote')
@@ -979,7 +1422,7 @@ class XmlManifest(object):
979 v = self._remotes.get(name) 1422 v = self._remotes.get(name)
980 if not v: 1423 if not v:
981 raise ManifestParseError("remote %s not defined in %s" % 1424 raise ManifestParseError("remote %s not defined in %s" %
982 (name, self.manifestFile)) 1425 (name, self.manifestFile))
983 return v 1426 return v
984 1427
985 def _reqatt(self, node, attname): 1428 def _reqatt(self, node, attname):
@@ -989,7 +1432,7 @@ class XmlManifest(object):
989 v = node.getAttribute(attname) 1432 v = node.getAttribute(attname)
990 if not v: 1433 if not v:
991 raise ManifestParseError("no %s in <%s> within %s" % 1434 raise ManifestParseError("no %s in <%s> within %s" %
992 (attname, node.nodeName, self.manifestFile)) 1435 (attname, node.nodeName, self.manifestFile))
993 return v 1436 return v
994 1437
995 def projectsDiff(self, manifest): 1438 def projectsDiff(self, manifest):
@@ -1007,7 +1450,7 @@ class XmlManifest(object):
1007 diff = {'added': [], 'removed': [], 'changed': [], 'unreachable': []} 1450 diff = {'added': [], 'removed': [], 'changed': [], 'unreachable': []}
1008 1451
1009 for proj in fromKeys: 1452 for proj in fromKeys:
1010 if not proj in toKeys: 1453 if proj not in toKeys:
1011 diff['removed'].append(fromProjects[proj]) 1454 diff['removed'].append(fromProjects[proj])
1012 else: 1455 else:
1013 fromProj = fromProjects[proj] 1456 fromProj = fromProjects[proj]
@@ -1029,19 +1472,11 @@ class XmlManifest(object):
1029 1472
1030 1473
1031class GitcManifest(XmlManifest): 1474class GitcManifest(XmlManifest):
1475 """Parser for GitC (git-in-the-cloud) manifests."""
1032 1476
1033 def __init__(self, repodir, gitc_client_name): 1477 def _ParseProject(self, node, parent=None):
1034 """Initialize the GitcManifest object."""
1035 super(GitcManifest, self).__init__(repodir)
1036 self.isGitcClient = True
1037 self.gitc_client_name = gitc_client_name
1038 self.gitc_client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
1039 gitc_client_name)
1040 self.manifestFile = os.path.join(self.gitc_client_dir, '.manifest')
1041
1042 def _ParseProject(self, node, parent = None):
1043 """Override _ParseProject and add support for GITC specific attributes.""" 1478 """Override _ParseProject and add support for GITC specific attributes."""
1044 return super(GitcManifest, self)._ParseProject( 1479 return super()._ParseProject(
1045 node, parent=parent, old_revision=node.getAttribute('old-revision')) 1480 node, parent=parent, old_revision=node.getAttribute('old-revision'))
1046 1481
1047 def _output_manifest_project_extras(self, p, e): 1482 def _output_manifest_project_extras(self, p, e):
@@ -1049,3 +1484,36 @@ class GitcManifest(XmlManifest):
1049 if p.old_revision: 1484 if p.old_revision:
1050 e.setAttribute('old-revision', str(p.old_revision)) 1485 e.setAttribute('old-revision', str(p.old_revision))
1051 1486
1487
1488class RepoClient(XmlManifest):
1489 """Manages a repo client checkout."""
1490
1491 def __init__(self, repodir, manifest_file=None):
1492 self.isGitcClient = False
1493
1494 if os.path.exists(os.path.join(repodir, LOCAL_MANIFEST_NAME)):
1495 print('error: %s is not supported; put local manifests in `%s` instead' %
1496 (LOCAL_MANIFEST_NAME, os.path.join(repodir, LOCAL_MANIFESTS_DIR_NAME)),
1497 file=sys.stderr)
1498 sys.exit(1)
1499
1500 if manifest_file is None:
1501 manifest_file = os.path.join(repodir, MANIFEST_FILE_NAME)
1502 local_manifests = os.path.abspath(os.path.join(repodir, LOCAL_MANIFESTS_DIR_NAME))
1503 super().__init__(repodir, manifest_file, local_manifests)
1504
1505 # TODO: Completely separate manifest logic out of the client.
1506 self.manifest = self
1507
1508
1509class GitcClient(RepoClient, GitcManifest):
1510 """Manages a GitC client checkout."""
1511
1512 def __init__(self, repodir, gitc_client_name):
1513 """Initialize the GitcManifest object."""
1514 self.gitc_client_name = gitc_client_name
1515 self.gitc_client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
1516 gitc_client_name)
1517
1518 super().__init__(repodir, os.path.join(self.gitc_client_dir, '.manifest'))
1519 self.isGitcClient = True