summaryrefslogtreecommitdiffstats
path: root/manifest_xml.py
diff options
context:
space:
mode:
authorGavin Mak <gavinmak@google.com>2023-03-11 06:46:20 +0000
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-03-22 17:46:28 +0000
commitea2e330e43c182dc16b0111ebc69ee5a71ee4ce1 (patch)
treedc33ba0e56825b3e007d0589891756724725a465 /manifest_xml.py
parent1604cf255f8c1786a23388db6d5277ac7949a24a (diff)
downloadgit-repo-ea2e330e43c182dc16b0111ebc69ee5a71ee4ce1.tar.gz
Format codebase with black and check formatting in CQ
Apply rules set by https://gerrit-review.googlesource.com/c/git-repo/+/362954/ across the codebase and fix any lingering errors caught by flake8. Also check black formatting in run_tests (and CQ). Bug: b/267675342 Change-Id: I972d77649dac351150dcfeb1cd1ad0ea2efc1956 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/363474 Reviewed-by: Mike Frysinger <vapier@google.com> Tested-by: Gavin Mak <gavinmak@google.com> Commit-Queue: Gavin Mak <gavinmak@google.com>
Diffstat (limited to 'manifest_xml.py')
-rw-r--r--manifest_xml.py4107
1 files changed, 2178 insertions, 1929 deletions
diff --git a/manifest_xml.py b/manifest_xml.py
index 5b83f368..9603906f 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -26,415 +26,452 @@ from git_config import GitConfig
26from git_refs import R_HEADS, HEAD 26from git_refs import R_HEADS, HEAD
27from git_superproject import Superproject 27from git_superproject import Superproject
28import platform_utils 28import platform_utils
29from project import (Annotation, RemoteSpec, Project, RepoProject, 29from project import (
30 ManifestProject) 30 Annotation,
31from error import (ManifestParseError, ManifestInvalidPathError, 31 RemoteSpec,
32 ManifestInvalidRevisionError) 32 Project,
33 RepoProject,
34 ManifestProject,
35)
36from error import (
37 ManifestParseError,
38 ManifestInvalidPathError,
39 ManifestInvalidRevisionError,
40)
33from wrapper import Wrapper 41from wrapper import Wrapper
34 42
35MANIFEST_FILE_NAME = 'manifest.xml' 43MANIFEST_FILE_NAME = "manifest.xml"
36LOCAL_MANIFEST_NAME = 'local_manifest.xml' 44LOCAL_MANIFEST_NAME = "local_manifest.xml"
37LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' 45LOCAL_MANIFESTS_DIR_NAME = "local_manifests"
38SUBMANIFEST_DIR = 'submanifests' 46SUBMANIFEST_DIR = "submanifests"
39# Limit submanifests to an arbitrary depth for loop detection. 47# Limit submanifests to an arbitrary depth for loop detection.
40MAX_SUBMANIFEST_DEPTH = 8 48MAX_SUBMANIFEST_DEPTH = 8
41# Add all projects from sub manifest into a group. 49# Add all projects from sub manifest into a group.
42SUBMANIFEST_GROUP_PREFIX = 'submanifest:' 50SUBMANIFEST_GROUP_PREFIX = "submanifest:"
43 51
44# Add all projects from local manifest into a group. 52# Add all projects from local manifest into a group.
45LOCAL_MANIFEST_GROUP_PREFIX = 'local:' 53LOCAL_MANIFEST_GROUP_PREFIX = "local:"
46 54
47# ContactInfo has the self-registered bug url, supplied by the manifest authors. 55# ContactInfo has the self-registered bug url, supplied by the manifest authors.
48ContactInfo = collections.namedtuple('ContactInfo', 'bugurl') 56ContactInfo = collections.namedtuple("ContactInfo", "bugurl")
49 57
50# urljoin gets confused if the scheme is not known. 58# urljoin gets confused if the scheme is not known.
51urllib.parse.uses_relative.extend([ 59urllib.parse.uses_relative.extend(
52 'ssh', 60 ["ssh", "git", "persistent-https", "sso", "rpc"]
53 'git', 61)
54 'persistent-https', 62urllib.parse.uses_netloc.extend(
55 'sso', 63 ["ssh", "git", "persistent-https", "sso", "rpc"]
56 'rpc']) 64)
57urllib.parse.uses_netloc.extend([
58 'ssh',
59 'git',
60 'persistent-https',
61 'sso',
62 'rpc'])
63 65
64 66
65def XmlBool(node, attr, default=None): 67def XmlBool(node, attr, default=None):
66 """Determine boolean value of |node|'s |attr|. 68 """Determine boolean value of |node|'s |attr|.
67 69
68 Invalid values will issue a non-fatal warning. 70 Invalid values will issue a non-fatal warning.
69 71
70 Args: 72 Args:
71 node: XML node whose attributes we access. 73 node: XML node whose attributes we access.
72 attr: The attribute to access. 74 attr: The attribute to access.
73 default: If the attribute is not set (value is empty), then use this. 75 default: If the attribute is not set (value is empty), then use this.
74 76
75 Returns: 77 Returns:
76 True if the attribute is a valid string representing true. 78 True if the attribute is a valid string representing true.
77 False if the attribute is a valid string representing false. 79 False if the attribute is a valid string representing false.
78 |default| otherwise. 80 |default| otherwise.
79 """ 81 """
80 value = node.getAttribute(attr) 82 value = node.getAttribute(attr)
81 s = value.lower() 83 s = value.lower()
82 if s == '': 84 if s == "":
83 return default 85 return default
84 elif s in {'yes', 'true', '1'}: 86 elif s in {"yes", "true", "1"}:
85 return True 87 return True
86 elif s in {'no', 'false', '0'}: 88 elif s in {"no", "false", "0"}:
87 return False 89 return False
88 else: 90 else:
89 print('warning: manifest: %s="%s": ignoring invalid XML boolean' % 91 print(
90 (attr, value), file=sys.stderr) 92 'warning: manifest: %s="%s": ignoring invalid XML boolean'
91 return default 93 % (attr, value),
94 file=sys.stderr,
95 )
96 return default
92 97
93 98
94def XmlInt(node, attr, default=None): 99def XmlInt(node, attr, default=None):
95 """Determine integer value of |node|'s |attr|. 100 """Determine integer value of |node|'s |attr|.
96 101
97 Args: 102 Args:
98 node: XML node whose attributes we access. 103 node: XML node whose attributes we access.
99 attr: The attribute to access. 104 attr: The attribute to access.
100 default: If the attribute is not set (value is empty), then use this. 105 default: If the attribute is not set (value is empty), then use this.
101 106
102 Returns: 107 Returns:
103 The number if the attribute is a valid number. 108 The number if the attribute is a valid number.
104 109
105 Raises: 110 Raises:
106 ManifestParseError: The number is invalid. 111 ManifestParseError: The number is invalid.
107 """ 112 """
108 value = node.getAttribute(attr) 113 value = node.getAttribute(attr)
109 if not value: 114 if not value:
110 return default 115 return default
111 116
112 try: 117 try:
113 return int(value) 118 return int(value)
114 except ValueError: 119 except ValueError:
115 raise ManifestParseError('manifest: invalid %s="%s" integer' % 120 raise ManifestParseError(
116 (attr, value)) 121 'manifest: invalid %s="%s" integer' % (attr, value)
122 )
117 123
118 124
119class _Default(object): 125class _Default(object):
120 """Project defaults within the manifest.""" 126 """Project defaults within the manifest."""
121 127
122 revisionExpr = None 128 revisionExpr = None
123 destBranchExpr = None 129 destBranchExpr = None
124 upstreamExpr = None 130 upstreamExpr = None
125 remote = None 131 remote = None
126 sync_j = None 132 sync_j = None
127 sync_c = False 133 sync_c = False
128 sync_s = False 134 sync_s = False
129 sync_tags = True 135 sync_tags = True
130 136
131 def __eq__(self, other): 137 def __eq__(self, other):
132 if not isinstance(other, _Default): 138 if not isinstance(other, _Default):
133 return False 139 return False
134 return self.__dict__ == other.__dict__ 140 return self.__dict__ == other.__dict__
135 141
136 def __ne__(self, other): 142 def __ne__(self, other):
137 if not isinstance(other, _Default): 143 if not isinstance(other, _Default):
138 return True 144 return True
139 return self.__dict__ != other.__dict__ 145 return self.__dict__ != other.__dict__
140 146
141 147
142class _XmlRemote(object): 148class _XmlRemote(object):
143 def __init__(self, 149 def __init__(
144 name, 150 self,
145 alias=None, 151 name,
146 fetch=None, 152 alias=None,
147 pushUrl=None, 153 fetch=None,
148 manifestUrl=None, 154 pushUrl=None,
149 review=None, 155 manifestUrl=None,
150 revision=None): 156 review=None,
151 self.name = name 157 revision=None,
152 self.fetchUrl = fetch 158 ):
153 self.pushUrl = pushUrl 159 self.name = name
154 self.manifestUrl = manifestUrl 160 self.fetchUrl = fetch
155 self.remoteAlias = alias 161 self.pushUrl = pushUrl
156 self.reviewUrl = review 162 self.manifestUrl = manifestUrl
157 self.revision = revision 163 self.remoteAlias = alias
158 self.resolvedFetchUrl = self._resolveFetchUrl() 164 self.reviewUrl = review
159 self.annotations = [] 165 self.revision = revision
160 166 self.resolvedFetchUrl = self._resolveFetchUrl()
161 def __eq__(self, other): 167 self.annotations = []
162 if not isinstance(other, _XmlRemote): 168
163 return False 169 def __eq__(self, other):
164 return (sorted(self.annotations) == sorted(other.annotations) and 170 if not isinstance(other, _XmlRemote):
165 self.name == other.name and self.fetchUrl == other.fetchUrl and 171 return False
166 self.pushUrl == other.pushUrl and self.remoteAlias == other.remoteAlias 172 return (
167 and self.reviewUrl == other.reviewUrl and self.revision == other.revision) 173 sorted(self.annotations) == sorted(other.annotations)
168 174 and self.name == other.name
169 def __ne__(self, other): 175 and self.fetchUrl == other.fetchUrl
170 return not self.__eq__(other) 176 and self.pushUrl == other.pushUrl
171 177 and self.remoteAlias == other.remoteAlias
172 def _resolveFetchUrl(self): 178 and self.reviewUrl == other.reviewUrl
173 if self.fetchUrl is None: 179 and self.revision == other.revision
174 return '' 180 )
175 url = self.fetchUrl.rstrip('/') 181
176 manifestUrl = self.manifestUrl.rstrip('/') 182 def __ne__(self, other):
177 # urljoin will gets confused over quite a few things. The ones we care 183 return not self.__eq__(other)
178 # about here are: 184
179 # * no scheme in the base url, like <hostname:port> 185 def _resolveFetchUrl(self):
180 # We handle no scheme by replacing it with an obscure protocol, gopher 186 if self.fetchUrl is None:
181 # and then replacing it with the original when we are done. 187 return ""
182 188 url = self.fetchUrl.rstrip("/")
183 if manifestUrl.find(':') != manifestUrl.find('/') - 1: 189 manifestUrl = self.manifestUrl.rstrip("/")
184 url = urllib.parse.urljoin('gopher://' + manifestUrl, url) 190 # urljoin will gets confused over quite a few things. The ones we care
185 url = re.sub(r'^gopher://', '', url) 191 # about here are:
186 else: 192 # * no scheme in the base url, like <hostname:port>
187 url = urllib.parse.urljoin(manifestUrl, url) 193 # We handle no scheme by replacing it with an obscure protocol, gopher
188 return url 194 # and then replacing it with the original when we are done.
189 195
190 def ToRemoteSpec(self, projectName): 196 if manifestUrl.find(":") != manifestUrl.find("/") - 1:
191 fetchUrl = self.resolvedFetchUrl.rstrip('/') 197 url = urllib.parse.urljoin("gopher://" + manifestUrl, url)
192 url = fetchUrl + '/' + projectName 198 url = re.sub(r"^gopher://", "", url)
193 remoteName = self.name 199 else:
194 if self.remoteAlias: 200 url = urllib.parse.urljoin(manifestUrl, url)
195 remoteName = self.remoteAlias 201 return url
196 return RemoteSpec(remoteName, 202
197 url=url, 203 def ToRemoteSpec(self, projectName):
198 pushUrl=self.pushUrl, 204 fetchUrl = self.resolvedFetchUrl.rstrip("/")
199 review=self.reviewUrl, 205 url = fetchUrl + "/" + projectName
200 orig_name=self.name, 206 remoteName = self.name
201 fetchUrl=self.fetchUrl) 207 if self.remoteAlias:
202 208 remoteName = self.remoteAlias
203 def AddAnnotation(self, name, value, keep): 209 return RemoteSpec(
204 self.annotations.append(Annotation(name, value, keep)) 210 remoteName,
211 url=url,
212 pushUrl=self.pushUrl,
213 review=self.reviewUrl,
214 orig_name=self.name,
215 fetchUrl=self.fetchUrl,
216 )
217
218 def AddAnnotation(self, name, value, keep):
219 self.annotations.append(Annotation(name, value, keep))
205 220
206 221
207class _XmlSubmanifest: 222class _XmlSubmanifest:
208 """Manage the <submanifest> element specified in the manifest. 223 """Manage the <submanifest> element specified in the manifest.
209 224
210 Attributes: 225 Attributes:
211 name: a string, the name for this submanifest. 226 name: a string, the name for this submanifest.
212 remote: a string, the remote.name for this submanifest. 227 remote: a string, the remote.name for this submanifest.
213 project: a string, the name of the manifest project. 228 project: a string, the name of the manifest project.
214 revision: a string, the commitish. 229 revision: a string, the commitish.
215 manifestName: a string, the submanifest file name. 230 manifestName: a string, the submanifest file name.
216 groups: a list of strings, the groups to add to all projects in the submanifest. 231 groups: a list of strings, the groups to add to all projects in the
217 default_groups: a list of strings, the default groups to sync. 232 submanifest.
218 path: a string, the relative path for the submanifest checkout. 233 default_groups: a list of strings, the default groups to sync.
219 parent: an XmlManifest, the parent manifest. 234 path: a string, the relative path for the submanifest checkout.
220 annotations: (derived) a list of annotations. 235 parent: an XmlManifest, the parent manifest.
221 present: (derived) a boolean, whether the sub manifest file is present. 236 annotations: (derived) a list of annotations.
222 """ 237 present: (derived) a boolean, whether the sub manifest file is present.
223 def __init__(self, 238 """
224 name,
225 remote=None,
226 project=None,
227 revision=None,
228 manifestName=None,
229 groups=None,
230 default_groups=None,
231 path=None,
232 parent=None):
233 self.name = name
234 self.remote = remote
235 self.project = project
236 self.revision = revision
237 self.manifestName = manifestName
238 self.groups = groups
239 self.default_groups = default_groups
240 self.path = path
241 self.parent = parent
242 self.annotations = []
243 outer_client = parent._outer_client or parent
244 if self.remote and not self.project:
245 raise ManifestParseError(
246 f'Submanifest {name}: must specify project when remote is given.')
247 # Construct the absolute path to the manifest file using the parent's
248 # method, so that we can correctly create our repo_client.
249 manifestFile = parent.SubmanifestInfoDir(
250 os.path.join(parent.path_prefix, self.relpath),
251 os.path.join('manifests', manifestName or 'default.xml'))
252 linkFile = parent.SubmanifestInfoDir(
253 os.path.join(parent.path_prefix, self.relpath), MANIFEST_FILE_NAME)
254 rc = self.repo_client = RepoClient(
255 parent.repodir, linkFile, parent_groups=','.join(groups) or '',
256 submanifest_path=self.relpath, outer_client=outer_client,
257 default_groups=default_groups)
258
259 self.present = os.path.exists(manifestFile)
260
261 def __eq__(self, other):
262 if not isinstance(other, _XmlSubmanifest):
263 return False
264 return (
265 self.name == other.name and
266 self.remote == other.remote and
267 self.project == other.project and
268 self.revision == other.revision and
269 self.manifestName == other.manifestName and
270 self.groups == other.groups and
271 self.default_groups == other.default_groups and
272 self.path == other.path and
273 sorted(self.annotations) == sorted(other.annotations))
274
275 def __ne__(self, other):
276 return not self.__eq__(other)
277
278 def ToSubmanifestSpec(self):
279 """Return a SubmanifestSpec object, populating attributes"""
280 mp = self.parent.manifestProject
281 remote = self.parent.remotes[self.remote or self.parent.default.remote.name]
282 # If a project was given, generate the url from the remote and project.
283 # If not, use this manifestProject's url.
284 if self.project:
285 manifestUrl = remote.ToRemoteSpec(self.project).url
286 else:
287 manifestUrl = mp.GetRemote().url
288 manifestName = self.manifestName or 'default.xml'
289 revision = self.revision or self.name
290 path = self.path or revision.split('/')[-1]
291 groups = self.groups or []
292 default_groups = self.default_groups or []
293 239
294 return SubmanifestSpec(self.name, manifestUrl, manifestName, revision, path, 240 def __init__(
295 groups) 241 self,
242 name,
243 remote=None,
244 project=None,
245 revision=None,
246 manifestName=None,
247 groups=None,
248 default_groups=None,
249 path=None,
250 parent=None,
251 ):
252 self.name = name
253 self.remote = remote
254 self.project = project
255 self.revision = revision
256 self.manifestName = manifestName
257 self.groups = groups
258 self.default_groups = default_groups
259 self.path = path
260 self.parent = parent
261 self.annotations = []
262 outer_client = parent._outer_client or parent
263 if self.remote and not self.project:
264 raise ManifestParseError(
265 f"Submanifest {name}: must specify project when remote is "
266 "given."
267 )
268 # Construct the absolute path to the manifest file using the parent's
269 # method, so that we can correctly create our repo_client.
270 manifestFile = parent.SubmanifestInfoDir(
271 os.path.join(parent.path_prefix, self.relpath),
272 os.path.join("manifests", manifestName or "default.xml"),
273 )
274 linkFile = parent.SubmanifestInfoDir(
275 os.path.join(parent.path_prefix, self.relpath), MANIFEST_FILE_NAME
276 )
277 self.repo_client = RepoClient(
278 parent.repodir,
279 linkFile,
280 parent_groups=",".join(groups) or "",
281 submanifest_path=self.relpath,
282 outer_client=outer_client,
283 default_groups=default_groups,
284 )
285
286 self.present = os.path.exists(manifestFile)
287
288 def __eq__(self, other):
289 if not isinstance(other, _XmlSubmanifest):
290 return False
291 return (
292 self.name == other.name
293 and self.remote == other.remote
294 and self.project == other.project
295 and self.revision == other.revision
296 and self.manifestName == other.manifestName
297 and self.groups == other.groups
298 and self.default_groups == other.default_groups
299 and self.path == other.path
300 and sorted(self.annotations) == sorted(other.annotations)
301 )
302
303 def __ne__(self, other):
304 return not self.__eq__(other)
305
306 def ToSubmanifestSpec(self):
307 """Return a SubmanifestSpec object, populating attributes"""
308 mp = self.parent.manifestProject
309 remote = self.parent.remotes[
310 self.remote or self.parent.default.remote.name
311 ]
312 # If a project was given, generate the url from the remote and project.
313 # If not, use this manifestProject's url.
314 if self.project:
315 manifestUrl = remote.ToRemoteSpec(self.project).url
316 else:
317 manifestUrl = mp.GetRemote().url
318 manifestName = self.manifestName or "default.xml"
319 revision = self.revision or self.name
320 path = self.path or revision.split("/")[-1]
321 groups = self.groups or []
296 322
297 @property 323 return SubmanifestSpec(
298 def relpath(self): 324 self.name, manifestUrl, manifestName, revision, path, groups
299 """The path of this submanifest relative to the parent manifest.""" 325 )
300 revision = self.revision or self.name
301 return self.path or revision.split('/')[-1]
302 326
303 def GetGroupsStr(self): 327 @property
304 """Returns the `groups` given for this submanifest.""" 328 def relpath(self):
305 if self.groups: 329 """The path of this submanifest relative to the parent manifest."""
306 return ','.join(self.groups) 330 revision = self.revision or self.name
307 return '' 331 return self.path or revision.split("/")[-1]
308 332
309 def GetDefaultGroupsStr(self): 333 def GetGroupsStr(self):
310 """Returns the `default-groups` given for this submanifest.""" 334 """Returns the `groups` given for this submanifest."""
311 return ','.join(self.default_groups or []) 335 if self.groups:
336 return ",".join(self.groups)
337 return ""
312 338
313 def AddAnnotation(self, name, value, keep): 339 def GetDefaultGroupsStr(self):
314 """Add annotations to the submanifest.""" 340 """Returns the `default-groups` given for this submanifest."""
315 self.annotations.append(Annotation(name, value, keep)) 341 return ",".join(self.default_groups or [])
316 342
343 def AddAnnotation(self, name, value, keep):
344 """Add annotations to the submanifest."""
345 self.annotations.append(Annotation(name, value, keep))
317 346
318class SubmanifestSpec:
319 """The submanifest element, with all fields expanded."""
320
321 def __init__(self,
322 name,
323 manifestUrl,
324 manifestName,
325 revision,
326 path,
327 groups):
328 self.name = name
329 self.manifestUrl = manifestUrl
330 self.manifestName = manifestName
331 self.revision = revision
332 self.path = path
333 self.groups = groups or []
334 347
348class SubmanifestSpec:
349 """The submanifest element, with all fields expanded."""
335 350
336class XmlManifest(object): 351 def __init__(self, name, manifestUrl, manifestName, revision, path, groups):
337 """manages the repo configuration file""" 352 self.name = name
353 self.manifestUrl = manifestUrl
354 self.manifestName = manifestName
355 self.revision = revision
356 self.path = path
357 self.groups = groups or []
338 358
339 def __init__(self, repodir, manifest_file, local_manifests=None,
340 outer_client=None, parent_groups='', submanifest_path='',
341 default_groups=None):
342 """Initialize.
343 359
344 Args: 360class XmlManifest(object):
345 repodir: Path to the .repo/ dir for holding all internal checkout state. 361 """manages the repo configuration file"""
346 It must be in the top directory of the repo client checkout. 362
347 manifest_file: Full path to the manifest file to parse. This will usually 363 def __init__(
348 be |repodir|/|MANIFEST_FILE_NAME|. 364 self,
349 local_manifests: Full path to the directory of local override manifests. 365 repodir,
350 This will usually be |repodir|/|LOCAL_MANIFESTS_DIR_NAME|. 366 manifest_file,
351 outer_client: RepoClient of the outer manifest. 367 local_manifests=None,
352 parent_groups: a string, the groups to apply to this projects. 368 outer_client=None,
353 submanifest_path: The submanifest root relative to the repo root. 369 parent_groups="",
354 default_groups: a string, the default manifest groups to use. 370 submanifest_path="",
355 """ 371 default_groups=None,
356 # TODO(vapier): Move this out of this class. 372 ):
357 self.globalConfig = GitConfig.ForUser() 373 """Initialize.
358 374
359 self.repodir = os.path.abspath(repodir) 375 Args:
360 self._CheckLocalPath(submanifest_path) 376 repodir: Path to the .repo/ dir for holding all internal checkout
361 self.topdir = os.path.dirname(self.repodir) 377 state. It must be in the top directory of the repo client
362 if submanifest_path: 378 checkout.
363 # This avoids a trailing os.path.sep when submanifest_path is empty. 379 manifest_file: Full path to the manifest file to parse. This will
364 self.topdir = os.path.join(self.topdir, submanifest_path) 380 usually be |repodir|/|MANIFEST_FILE_NAME|.
365 if manifest_file != os.path.abspath(manifest_file): 381 local_manifests: Full path to the directory of local override
366 raise ManifestParseError('manifest_file must be abspath') 382 manifests. This will usually be
367 self.manifestFile = manifest_file 383 |repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
368 if not outer_client or outer_client == self: 384 outer_client: RepoClient of the outer manifest.
369 # manifestFileOverrides only exists in the outer_client's manifest, since 385 parent_groups: a string, the groups to apply to this projects.
370 # that is the only instance left when Unload() is called on the outer 386 submanifest_path: The submanifest root relative to the repo root.
371 # manifest. 387 default_groups: a string, the default manifest groups to use.
372 self.manifestFileOverrides = {} 388 """
373 self.local_manifests = local_manifests 389 # TODO(vapier): Move this out of this class.
374 self._load_local_manifests = True 390 self.globalConfig = GitConfig.ForUser()
375 self.parent_groups = parent_groups 391
376 self.default_groups = default_groups 392 self.repodir = os.path.abspath(repodir)
377 393 self._CheckLocalPath(submanifest_path)
378 if outer_client and self.isGitcClient: 394 self.topdir = os.path.dirname(self.repodir)
379 raise ManifestParseError('Multi-manifest is incompatible with `gitc-init`') 395 if submanifest_path:
380 396 # This avoids a trailing os.path.sep when submanifest_path is empty.
381 if submanifest_path and not outer_client: 397 self.topdir = os.path.join(self.topdir, submanifest_path)
382 # If passing a submanifest_path, there must be an outer_client. 398 if manifest_file != os.path.abspath(manifest_file):
383 raise ManifestParseError(f'Bad call to {self.__class__.__name__}') 399 raise ManifestParseError("manifest_file must be abspath")
384 400 self.manifestFile = manifest_file
385 # If self._outer_client is None, this is not a checkout that supports 401 if not outer_client or outer_client == self:
386 # multi-tree. 402 # manifestFileOverrides only exists in the outer_client's manifest,
387 self._outer_client = outer_client or self 403 # since that is the only instance left when Unload() is called on
388 404 # the outer manifest.
389 self.repoProject = RepoProject(self, 'repo', 405 self.manifestFileOverrides = {}
390 gitdir=os.path.join(repodir, 'repo/.git'), 406 self.local_manifests = local_manifests
391 worktree=os.path.join(repodir, 'repo')) 407 self._load_local_manifests = True
392 408 self.parent_groups = parent_groups
393 mp = self.SubmanifestProject(self.path_prefix) 409 self.default_groups = default_groups
394 self.manifestProject = mp 410
395 411 if outer_client and self.isGitcClient:
396 # This is a bit hacky, but we're in a chicken & egg situation: all the 412 raise ManifestParseError(
397 # normal repo settings live in the manifestProject which we just setup 413 "Multi-manifest is incompatible with `gitc-init`"
398 # above, so we couldn't easily query before that. We assume Project() 414 )
399 # init doesn't care if this changes afterwards. 415
400 if os.path.exists(mp.gitdir) and mp.use_worktree: 416 if submanifest_path and not outer_client:
401 mp.use_git_worktrees = True 417 # If passing a submanifest_path, there must be an outer_client.
402 418 raise ManifestParseError(f"Bad call to {self.__class__.__name__}")
403 self.Unload() 419
404 420 # If self._outer_client is None, this is not a checkout that supports
405 def Override(self, name, load_local_manifests=True): 421 # multi-tree.
406 """Use a different manifest, just for the current instantiation. 422 self._outer_client = outer_client or self
407 """ 423
408 path = None 424 self.repoProject = RepoProject(
409 425 self,
410 # Look for a manifest by path in the filesystem (including the cwd). 426 "repo",
411 if not load_local_manifests: 427 gitdir=os.path.join(repodir, "repo/.git"),
412 local_path = os.path.abspath(name) 428 worktree=os.path.join(repodir, "repo"),
413 if os.path.isfile(local_path): 429 )
414 path = local_path 430
415 431 mp = self.SubmanifestProject(self.path_prefix)
416 # Look for manifests by name from the manifests repo. 432 self.manifestProject = mp
417 if path is None: 433
418 path = os.path.join(self.manifestProject.worktree, name) 434 # This is a bit hacky, but we're in a chicken & egg situation: all the
419 if not os.path.isfile(path): 435 # normal repo settings live in the manifestProject which we just setup
420 raise ManifestParseError('manifest %s not found' % name) 436 # above, so we couldn't easily query before that. We assume Project()
421 437 # init doesn't care if this changes afterwards.
422 self._load_local_manifests = load_local_manifests 438 if os.path.exists(mp.gitdir) and mp.use_worktree:
423 self._outer_client.manifestFileOverrides[self.path_prefix] = path 439 mp.use_git_worktrees = True
424 self.Unload() 440
425 self._Load() 441 self.Unload()
426 442
427 def Link(self, name): 443 def Override(self, name, load_local_manifests=True):
428 """Update the repo metadata to use a different manifest. 444 """Use a different manifest, just for the current instantiation."""
429 """ 445 path = None
430 self.Override(name) 446
431 447 # Look for a manifest by path in the filesystem (including the cwd).
432 # Old versions of repo would generate symlinks we need to clean up. 448 if not load_local_manifests:
433 platform_utils.remove(self.manifestFile, missing_ok=True) 449 local_path = os.path.abspath(name)
434 # This file is interpreted as if it existed inside the manifest repo. 450 if os.path.isfile(local_path):
435 # That allows us to use <include> with the relative file name. 451 path = local_path
436 with open(self.manifestFile, 'w') as fp: 452
437 fp.write("""<?xml version="1.0" encoding="UTF-8"?> 453 # Look for manifests by name from the manifests repo.
454 if path is None:
455 path = os.path.join(self.manifestProject.worktree, name)
456 if not os.path.isfile(path):
457 raise ManifestParseError("manifest %s not found" % name)
458
459 self._load_local_manifests = load_local_manifests
460 self._outer_client.manifestFileOverrides[self.path_prefix] = path
461 self.Unload()
462 self._Load()
463
464 def Link(self, name):
465 """Update the repo metadata to use a different manifest."""
466 self.Override(name)
467
468 # Old versions of repo would generate symlinks we need to clean up.
469 platform_utils.remove(self.manifestFile, missing_ok=True)
470 # This file is interpreted as if it existed inside the manifest repo.
471 # That allows us to use <include> with the relative file name.
472 with open(self.manifestFile, "w") as fp:
473 fp.write(
474 """<?xml version="1.0" encoding="UTF-8"?>
438<!-- 475<!--
439DO NOT EDIT THIS FILE! It is generated by repo and changes will be discarded. 476DO NOT EDIT THIS FILE! It is generated by repo and changes will be discarded.
440If you want to use a different manifest, use `repo init -m <file>` instead. 477If you want to use a different manifest, use `repo init -m <file>` instead.
@@ -448,1591 +485,1803 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
448<manifest> 485<manifest>
449 <include name="%s" /> 486 <include name="%s" />
450</manifest> 487</manifest>
451""" % (name,)) 488"""
452 489 % (name,)
453 def _RemoteToXml(self, r, doc, root): 490 )
454 e = doc.createElement('remote') 491
455 root.appendChild(e) 492 def _RemoteToXml(self, r, doc, root):
456 e.setAttribute('name', r.name) 493 e = doc.createElement("remote")
457 e.setAttribute('fetch', r.fetchUrl) 494 root.appendChild(e)
458 if r.pushUrl is not None: 495 e.setAttribute("name", r.name)
459 e.setAttribute('pushurl', r.pushUrl) 496 e.setAttribute("fetch", r.fetchUrl)
460 if r.remoteAlias is not None: 497 if r.pushUrl is not None:
461 e.setAttribute('alias', r.remoteAlias) 498 e.setAttribute("pushurl", r.pushUrl)
462 if r.reviewUrl is not None: 499 if r.remoteAlias is not None:
463 e.setAttribute('review', r.reviewUrl) 500 e.setAttribute("alias", r.remoteAlias)
464 if r.revision is not None: 501 if r.reviewUrl is not None:
465 e.setAttribute('revision', r.revision) 502 e.setAttribute("review", r.reviewUrl)
466 503 if r.revision is not None:
467 for a in r.annotations: 504 e.setAttribute("revision", r.revision)
468 if a.keep == 'true': 505
469 ae = doc.createElement('annotation') 506 for a in r.annotations:
470 ae.setAttribute('name', a.name) 507 if a.keep == "true":
471 ae.setAttribute('value', a.value) 508 ae = doc.createElement("annotation")
472 e.appendChild(ae) 509 ae.setAttribute("name", a.name)
473 510 ae.setAttribute("value", a.value)
474 def _SubmanifestToXml(self, r, doc, root): 511 e.appendChild(ae)
475 """Generate XML <submanifest/> node.""" 512
476 e = doc.createElement('submanifest') 513 def _SubmanifestToXml(self, r, doc, root):
477 root.appendChild(e) 514 """Generate XML <submanifest/> node."""
478 e.setAttribute('name', r.name) 515 e = doc.createElement("submanifest")
479 if r.remote is not None: 516 root.appendChild(e)
480 e.setAttribute('remote', r.remote) 517 e.setAttribute("name", r.name)
481 if r.project is not None: 518 if r.remote is not None:
482 e.setAttribute('project', r.project) 519 e.setAttribute("remote", r.remote)
483 if r.manifestName is not None: 520 if r.project is not None:
484 e.setAttribute('manifest-name', r.manifestName) 521 e.setAttribute("project", r.project)
485 if r.revision is not None: 522 if r.manifestName is not None:
486 e.setAttribute('revision', r.revision) 523 e.setAttribute("manifest-name", r.manifestName)
487 if r.path is not None: 524 if r.revision is not None:
488 e.setAttribute('path', r.path) 525 e.setAttribute("revision", r.revision)
489 if r.groups: 526 if r.path is not None:
490 e.setAttribute('groups', r.GetGroupsStr()) 527 e.setAttribute("path", r.path)
491 if r.default_groups: 528 if r.groups:
492 e.setAttribute('default-groups', r.GetDefaultGroupsStr()) 529 e.setAttribute("groups", r.GetGroupsStr())
493 530 if r.default_groups:
494 for a in r.annotations: 531 e.setAttribute("default-groups", r.GetDefaultGroupsStr())
495 if a.keep == 'true': 532
496 ae = doc.createElement('annotation') 533 for a in r.annotations:
497 ae.setAttribute('name', a.name) 534 if a.keep == "true":
498 ae.setAttribute('value', a.value) 535 ae = doc.createElement("annotation")
499 e.appendChild(ae) 536 ae.setAttribute("name", a.name)
500 537 ae.setAttribute("value", a.value)
501 def _ParseList(self, field): 538 e.appendChild(ae)
502 """Parse fields that contain flattened lists. 539
503 540 def _ParseList(self, field):
504 These are whitespace & comma separated. Empty elements will be discarded. 541 """Parse fields that contain flattened lists.
505 """ 542
506 return [x for x in re.split(r'[,\s]+', field) if x] 543 These are whitespace & comma separated. Empty elements will be
507 544 discarded.
508 def ToXml(self, peg_rev=False, peg_rev_upstream=True, 545 """
509 peg_rev_dest_branch=True, groups=None, omit_local=False): 546 return [x for x in re.split(r"[,\s]+", field) if x]
510 """Return the current manifest XML.""" 547
511 mp = self.manifestProject 548 def ToXml(
512 549 self,
513 if groups is None: 550 peg_rev=False,
514 groups = mp.manifest_groups 551 peg_rev_upstream=True,
515 if groups: 552 peg_rev_dest_branch=True,
516 groups = self._ParseList(groups) 553 groups=None,
517 554 omit_local=False,
518 doc = xml.dom.minidom.Document() 555 ):
519 root = doc.createElement('manifest') 556 """Return the current manifest XML."""
520 if self.is_submanifest: 557 mp = self.manifestProject
521 root.setAttribute('path', self.path_prefix) 558
522 doc.appendChild(root) 559 if groups is None:
523 560 groups = mp.manifest_groups
524 # Save out the notice. There's a little bit of work here to give it the 561 if groups:
525 # right whitespace, which assumes that the notice is automatically indented 562 groups = self._ParseList(groups)
526 # by 4 by minidom. 563
527 if self.notice: 564 doc = xml.dom.minidom.Document()
528 notice_element = root.appendChild(doc.createElement('notice')) 565 root = doc.createElement("manifest")
529 notice_lines = self.notice.splitlines() 566 if self.is_submanifest:
530 indented_notice = ('\n'.join(" " * 4 + line for line in notice_lines))[4:] 567 root.setAttribute("path", self.path_prefix)
531 notice_element.appendChild(doc.createTextNode(indented_notice)) 568 doc.appendChild(root)
532 569
533 d = self.default 570 # Save out the notice. There's a little bit of work here to give it the
534 571 # right whitespace, which assumes that the notice is automatically
535 for r in sorted(self.remotes): 572 # indented by 4 by minidom.
536 self._RemoteToXml(self.remotes[r], doc, root) 573 if self.notice:
537 if self.remotes: 574 notice_element = root.appendChild(doc.createElement("notice"))
538 root.appendChild(doc.createTextNode('')) 575 notice_lines = self.notice.splitlines()
539 576 indented_notice = (
540 have_default = False 577 "\n".join(" " * 4 + line for line in notice_lines)
541 e = doc.createElement('default') 578 )[4:]
542 if d.remote: 579 notice_element.appendChild(doc.createTextNode(indented_notice))
543 have_default = True 580
544 e.setAttribute('remote', d.remote.name) 581 d = self.default
545 if d.revisionExpr: 582
546 have_default = True 583 for r in sorted(self.remotes):
547 e.setAttribute('revision', d.revisionExpr) 584 self._RemoteToXml(self.remotes[r], doc, root)
548 if d.destBranchExpr: 585 if self.remotes:
549 have_default = True 586 root.appendChild(doc.createTextNode(""))
550 e.setAttribute('dest-branch', d.destBranchExpr) 587
551 if d.upstreamExpr: 588 have_default = False
552 have_default = True 589 e = doc.createElement("default")
553 e.setAttribute('upstream', d.upstreamExpr) 590 if d.remote:
554 if d.sync_j is not None: 591 have_default = True
555 have_default = True 592 e.setAttribute("remote", d.remote.name)
556 e.setAttribute('sync-j', '%d' % d.sync_j) 593 if d.revisionExpr:
557 if d.sync_c: 594 have_default = True
558 have_default = True 595 e.setAttribute("revision", d.revisionExpr)
559 e.setAttribute('sync-c', 'true') 596 if d.destBranchExpr:
560 if d.sync_s: 597 have_default = True
561 have_default = True 598 e.setAttribute("dest-branch", d.destBranchExpr)
562 e.setAttribute('sync-s', 'true') 599 if d.upstreamExpr:
563 if not d.sync_tags: 600 have_default = True
564 have_default = True 601 e.setAttribute("upstream", d.upstreamExpr)
565 e.setAttribute('sync-tags', 'false') 602 if d.sync_j is not None:
566 if have_default: 603 have_default = True
567 root.appendChild(e) 604 e.setAttribute("sync-j", "%d" % d.sync_j)
568 root.appendChild(doc.createTextNode('')) 605 if d.sync_c:
569 606 have_default = True
570 if self._manifest_server: 607 e.setAttribute("sync-c", "true")
571 e = doc.createElement('manifest-server') 608 if d.sync_s:
572 e.setAttribute('url', self._manifest_server) 609 have_default = True
573 root.appendChild(e) 610 e.setAttribute("sync-s", "true")
574 root.appendChild(doc.createTextNode('')) 611 if not d.sync_tags:
575 612 have_default = True
576 for r in sorted(self.submanifests): 613 e.setAttribute("sync-tags", "false")
577 self._SubmanifestToXml(self.submanifests[r], doc, root) 614 if have_default:
578 if self.submanifests: 615 root.appendChild(e)
579 root.appendChild(doc.createTextNode('')) 616 root.appendChild(doc.createTextNode(""))
580 617
581 def output_projects(parent, parent_node, projects): 618 if self._manifest_server:
582 for project_name in projects: 619 e = doc.createElement("manifest-server")
583 for project in self._projects[project_name]: 620 e.setAttribute("url", self._manifest_server)
584 output_project(parent, parent_node, project) 621 root.appendChild(e)
585 622 root.appendChild(doc.createTextNode(""))
586 def output_project(parent, parent_node, p): 623
587 if not p.MatchesGroups(groups): 624 for r in sorted(self.submanifests):
588 return 625 self._SubmanifestToXml(self.submanifests[r], doc, root)
589 626 if self.submanifests:
590 if omit_local and self.IsFromLocalManifest(p): 627 root.appendChild(doc.createTextNode(""))
591 return 628
592 629 def output_projects(parent, parent_node, projects):
593 name = p.name 630 for project_name in projects:
594 relpath = p.relpath 631 for project in self._projects[project_name]:
595 if parent: 632 output_project(parent, parent_node, project)
596 name = self._UnjoinName(parent.name, name) 633
597 relpath = self._UnjoinRelpath(parent.relpath, relpath) 634 def output_project(parent, parent_node, p):
598 635 if not p.MatchesGroups(groups):
599 e = doc.createElement('project') 636 return
600 parent_node.appendChild(e) 637
601 e.setAttribute('name', name) 638 if omit_local and self.IsFromLocalManifest(p):
602 if relpath != name: 639 return
603 e.setAttribute('path', relpath) 640
604 remoteName = None 641 name = p.name
605 if d.remote: 642 relpath = p.relpath
606 remoteName = d.remote.name 643 if parent:
607 if not d.remote or p.remote.orig_name != remoteName: 644 name = self._UnjoinName(parent.name, name)
608 remoteName = p.remote.orig_name 645 relpath = self._UnjoinRelpath(parent.relpath, relpath)
609 e.setAttribute('remote', remoteName) 646
610 if peg_rev: 647 e = doc.createElement("project")
611 if self.IsMirror: 648 parent_node.appendChild(e)
612 value = p.bare_git.rev_parse(p.revisionExpr + '^0') 649 e.setAttribute("name", name)
613 else: 650 if relpath != name:
614 value = p.work_git.rev_parse(HEAD + '^0') 651 e.setAttribute("path", relpath)
615 e.setAttribute('revision', value) 652 remoteName = None
616 if peg_rev_upstream: 653 if d.remote:
617 if p.upstream: 654 remoteName = d.remote.name
618 e.setAttribute('upstream', p.upstream) 655 if not d.remote or p.remote.orig_name != remoteName:
619 elif value != p.revisionExpr: 656 remoteName = p.remote.orig_name
620 # Only save the origin if the origin is not a sha1, and the default 657 e.setAttribute("remote", remoteName)
621 # isn't our value 658 if peg_rev:
622 e.setAttribute('upstream', p.revisionExpr) 659 if self.IsMirror:
623 660 value = p.bare_git.rev_parse(p.revisionExpr + "^0")
624 if peg_rev_dest_branch: 661 else:
625 if p.dest_branch: 662 value = p.work_git.rev_parse(HEAD + "^0")
626 e.setAttribute('dest-branch', p.dest_branch) 663 e.setAttribute("revision", value)
627 elif value != p.revisionExpr: 664 if peg_rev_upstream:
628 e.setAttribute('dest-branch', p.revisionExpr) 665 if p.upstream:
629 666 e.setAttribute("upstream", p.upstream)
630 else: 667 elif value != p.revisionExpr:
631 revision = self.remotes[p.remote.orig_name].revision or d.revisionExpr 668 # Only save the origin if the origin is not a sha1, and
632 if not revision or revision != p.revisionExpr: 669 # the default isn't our value
633 e.setAttribute('revision', p.revisionExpr) 670 e.setAttribute("upstream", p.revisionExpr)
634 elif p.revisionId: 671
635 e.setAttribute('revision', p.revisionId) 672 if peg_rev_dest_branch:
636 if (p.upstream and (p.upstream != p.revisionExpr or 673 if p.dest_branch:
637 p.upstream != d.upstreamExpr)): 674 e.setAttribute("dest-branch", p.dest_branch)
638 e.setAttribute('upstream', p.upstream) 675 elif value != p.revisionExpr:
639 676 e.setAttribute("dest-branch", p.revisionExpr)
640 if p.dest_branch and p.dest_branch != d.destBranchExpr: 677
641 e.setAttribute('dest-branch', p.dest_branch) 678 else:
642 679 revision = (
643 for c in p.copyfiles: 680 self.remotes[p.remote.orig_name].revision or d.revisionExpr
644 ce = doc.createElement('copyfile') 681 )
645 ce.setAttribute('src', c.src) 682 if not revision or revision != p.revisionExpr:
646 ce.setAttribute('dest', c.dest) 683 e.setAttribute("revision", p.revisionExpr)
647 e.appendChild(ce) 684 elif p.revisionId:
648 685 e.setAttribute("revision", p.revisionId)
649 for l in p.linkfiles: 686 if p.upstream and (
650 le = doc.createElement('linkfile') 687 p.upstream != p.revisionExpr or p.upstream != d.upstreamExpr
651 le.setAttribute('src', l.src) 688 ):
652 le.setAttribute('dest', l.dest) 689 e.setAttribute("upstream", p.upstream)
653 e.appendChild(le) 690
654 691 if p.dest_branch and p.dest_branch != d.destBranchExpr:
655 default_groups = ['all', 'name:%s' % p.name, 'path:%s' % p.relpath] 692 e.setAttribute("dest-branch", p.dest_branch)
656 egroups = [g for g in p.groups if g not in default_groups] 693
657 if egroups: 694 for c in p.copyfiles:
658 e.setAttribute('groups', ','.join(egroups)) 695 ce = doc.createElement("copyfile")
659 696 ce.setAttribute("src", c.src)
660 for a in p.annotations: 697 ce.setAttribute("dest", c.dest)
661 if a.keep == "true": 698 e.appendChild(ce)
662 ae = doc.createElement('annotation') 699
663 ae.setAttribute('name', a.name) 700 for lf in p.linkfiles:
664 ae.setAttribute('value', a.value) 701 le = doc.createElement("linkfile")
665 e.appendChild(ae) 702 le.setAttribute("src", lf.src)
666 703 le.setAttribute("dest", lf.dest)
667 if p.sync_c: 704 e.appendChild(le)
668 e.setAttribute('sync-c', 'true') 705
669 706 default_groups = ["all", "name:%s" % p.name, "path:%s" % p.relpath]
670 if p.sync_s: 707 egroups = [g for g in p.groups if g not in default_groups]
671 e.setAttribute('sync-s', 'true') 708 if egroups:
672 709 e.setAttribute("groups", ",".join(egroups))
673 if not p.sync_tags: 710
674 e.setAttribute('sync-tags', 'false') 711 for a in p.annotations:
675 712 if a.keep == "true":
676 if p.clone_depth: 713 ae = doc.createElement("annotation")
677 e.setAttribute('clone-depth', str(p.clone_depth)) 714 ae.setAttribute("name", a.name)
678 715 ae.setAttribute("value", a.value)
679 self._output_manifest_project_extras(p, e) 716 e.appendChild(ae)
680 717
681 if p.subprojects: 718 if p.sync_c:
682 subprojects = set(subp.name for subp in p.subprojects) 719 e.setAttribute("sync-c", "true")
683 output_projects(p, e, list(sorted(subprojects))) 720
684 721 if p.sync_s:
685 projects = set(p.name for p in self._paths.values() if not p.parent) 722 e.setAttribute("sync-s", "true")
686 output_projects(None, root, list(sorted(projects))) 723
687 724 if not p.sync_tags:
688 if self._repo_hooks_project: 725 e.setAttribute("sync-tags", "false")
689 root.appendChild(doc.createTextNode('')) 726
690 e = doc.createElement('repo-hooks') 727 if p.clone_depth:
691 e.setAttribute('in-project', self._repo_hooks_project.name) 728 e.setAttribute("clone-depth", str(p.clone_depth))
692 e.setAttribute('enabled-list', 729
693 ' '.join(self._repo_hooks_project.enabled_repo_hooks)) 730 self._output_manifest_project_extras(p, e)
694 root.appendChild(e) 731
695 732 if p.subprojects:
696 if self._superproject: 733 subprojects = set(subp.name for subp in p.subprojects)
697 root.appendChild(doc.createTextNode('')) 734 output_projects(p, e, list(sorted(subprojects)))
698 e = doc.createElement('superproject') 735
699 e.setAttribute('name', self._superproject.name) 736 projects = set(p.name for p in self._paths.values() if not p.parent)
700 remoteName = None 737 output_projects(None, root, list(sorted(projects)))
701 if d.remote: 738
702 remoteName = d.remote.name 739 if self._repo_hooks_project:
703 remote = self._superproject.remote 740 root.appendChild(doc.createTextNode(""))
704 if not d.remote or remote.orig_name != remoteName: 741 e = doc.createElement("repo-hooks")
705 remoteName = remote.orig_name 742 e.setAttribute("in-project", self._repo_hooks_project.name)
706 e.setAttribute('remote', remoteName) 743 e.setAttribute(
707 revision = remote.revision or d.revisionExpr 744 "enabled-list",
708 if not revision or revision != self._superproject.revision: 745 " ".join(self._repo_hooks_project.enabled_repo_hooks),
709 e.setAttribute('revision', self._superproject.revision) 746 )
710 root.appendChild(e) 747 root.appendChild(e)
711
712 if self._contactinfo.bugurl != Wrapper().BUG_URL:
713 root.appendChild(doc.createTextNode(''))
714 e = doc.createElement('contactinfo')
715 e.setAttribute('bugurl', self._contactinfo.bugurl)
716 root.appendChild(e)
717
718 return doc
719
720 def ToDict(self, **kwargs):
721 """Return the current manifest as a dictionary."""
722 # Elements that may only appear once.
723 SINGLE_ELEMENTS = {
724 'notice',
725 'default',
726 'manifest-server',
727 'repo-hooks',
728 'superproject',
729 'contactinfo',
730 }
731 # Elements that may be repeated.
732 MULTI_ELEMENTS = {
733 'remote',
734 'remove-project',
735 'project',
736 'extend-project',
737 'include',
738 'submanifest',
739 # These are children of 'project' nodes.
740 'annotation',
741 'project',
742 'copyfile',
743 'linkfile',
744 }
745
746 doc = self.ToXml(**kwargs)
747 ret = {}
748
749 def append_children(ret, node):
750 for child in node.childNodes:
751 if child.nodeType == xml.dom.Node.ELEMENT_NODE:
752 attrs = child.attributes
753 element = dict((attrs.item(i).localName, attrs.item(i).value)
754 for i in range(attrs.length))
755 if child.nodeName in SINGLE_ELEMENTS:
756 ret[child.nodeName] = element
757 elif child.nodeName in MULTI_ELEMENTS:
758 ret.setdefault(child.nodeName, []).append(element)
759 else:
760 raise ManifestParseError('Unhandled element "%s"' % (child.nodeName,))
761
762 append_children(element, child)
763
764 append_children(ret, doc.firstChild)
765
766 return ret
767
768 def Save(self, fd, **kwargs):
769 """Write the current manifest out to the given file descriptor."""
770 doc = self.ToXml(**kwargs)
771 doc.writexml(fd, '', ' ', '\n', 'UTF-8')
772
773 def _output_manifest_project_extras(self, p, e):
774 """Manifests can modify e if they support extra project attributes."""
775
776 @property
777 def is_multimanifest(self):
778 """Whether this is a multimanifest checkout.
779
780 This is safe to use as long as the outermost manifest XML has been parsed.
781 """
782 return bool(self._outer_client._submanifests)
783
784 @property
785 def is_submanifest(self):
786 """Whether this manifest is a submanifest.
787
788 This is safe to use as long as the outermost manifest XML has been parsed.
789 """
790 return self._outer_client and self._outer_client != self
791
792 @property
793 def outer_client(self):
794 """The instance of the outermost manifest client."""
795 self._Load()
796 return self._outer_client
797
798 @property
799 def all_manifests(self):
800 """Generator yielding all (sub)manifests, in depth-first order."""
801 self._Load()
802 outer = self._outer_client
803 yield outer
804 for tree in outer.all_children:
805 yield tree
806
807 @property
808 def all_children(self):
809 """Generator yielding all (present) child submanifests."""
810 self._Load()
811 for child in self._submanifests.values():
812 if child.repo_client:
813 yield child.repo_client
814 for tree in child.repo_client.all_children:
815 yield tree
816
817 @property
818 def path_prefix(self):
819 """The path of this submanifest, relative to the outermost manifest."""
820 if not self._outer_client or self == self._outer_client:
821 return ''
822 return os.path.relpath(self.topdir, self._outer_client.topdir)
823
824 @property
825 def all_paths(self):
826 """All project paths for all (sub)manifests.
827
828 See also `paths`.
829
830 Returns:
831 A dictionary of {path: Project()}. `path` is relative to the outer
832 manifest.
833 """
834 ret = {}
835 for tree in self.all_manifests:
836 prefix = tree.path_prefix
837 ret.update({os.path.join(prefix, k): v for k, v in tree.paths.items()})
838 return ret
839
840 @property
841 def all_projects(self):
842 """All projects for all (sub)manifests. See `projects`."""
843 return list(itertools.chain.from_iterable(x._paths.values() for x in self.all_manifests))
844
845 @property
846 def paths(self):
847 """Return all paths for this manifest.
848
849 Returns:
850 A dictionary of {path: Project()}. `path` is relative to this manifest.
851 """
852 self._Load()
853 return self._paths
854
855 @property
856 def projects(self):
857 """Return a list of all Projects in this manifest."""
858 self._Load()
859 return list(self._paths.values())
860
861 @property
862 def remotes(self):
863 """Return a list of remotes for this manifest."""
864 self._Load()
865 return self._remotes
866
867 @property
868 def default(self):
869 """Return default values for this manifest."""
870 self._Load()
871 return self._default
872
873 @property
874 def submanifests(self):
875 """All submanifests in this manifest."""
876 self._Load()
877 return self._submanifests
878
879 @property
880 def repo_hooks_project(self):
881 self._Load()
882 return self._repo_hooks_project
883
884 @property
885 def superproject(self):
886 self._Load()
887 return self._superproject
888
889 @property
890 def contactinfo(self):
891 self._Load()
892 return self._contactinfo
893
894 @property
895 def notice(self):
896 self._Load()
897 return self._notice
898
899 @property
900 def manifest_server(self):
901 self._Load()
902 return self._manifest_server
903
904 @property
905 def CloneBundle(self):
906 clone_bundle = self.manifestProject.clone_bundle
907 if clone_bundle is None:
908 return False if self.manifestProject.partial_clone else True
909 else:
910 return clone_bundle
911
912 @property
913 def CloneFilter(self):
914 if self.manifestProject.partial_clone:
915 return self.manifestProject.clone_filter
916 return None
917
918 @property
919 def PartialCloneExclude(self):
920 exclude = self.manifest.manifestProject.partial_clone_exclude or ''
921 return set(x.strip() for x in exclude.split(','))
922
923 def SetManifestOverride(self, path):
924 """Override manifestFile. The caller must call Unload()"""
925 self._outer_client.manifest.manifestFileOverrides[self.path_prefix] = path
926
927 @property
928 def UseLocalManifests(self):
929 return self._load_local_manifests
930
931 def SetUseLocalManifests(self, value):
932 self._load_local_manifests = value
933
934 @property
935 def HasLocalManifests(self):
936 return self._load_local_manifests and self.local_manifests
937
938 def IsFromLocalManifest(self, project):
939 """Is the project from a local manifest?"""
940 return any(x.startswith(LOCAL_MANIFEST_GROUP_PREFIX)
941 for x in project.groups)
942
943 @property
944 def IsMirror(self):
945 return self.manifestProject.mirror
946
947 @property
948 def UseGitWorktrees(self):
949 return self.manifestProject.use_worktree
950
951 @property
952 def IsArchive(self):
953 return self.manifestProject.archive
954
955 @property
956 def HasSubmodules(self):
957 return self.manifestProject.submodules
958
959 @property
960 def EnableGitLfs(self):
961 return self.manifestProject.git_lfs
962
963 def FindManifestByPath(self, path):
964 """Returns the manifest containing path."""
965 path = os.path.abspath(path)
966 manifest = self._outer_client or self
967 old = None
968 while manifest._submanifests and manifest != old:
969 old = manifest
970 for name in manifest._submanifests:
971 tree = manifest._submanifests[name]
972 if path.startswith(tree.repo_client.manifest.topdir):
973 manifest = tree.repo_client
974 break
975 return manifest
976
977 @property
978 def subdir(self):
979 """Returns the path for per-submanifest objects for this manifest."""
980 return self.SubmanifestInfoDir(self.path_prefix)
981
982 def SubmanifestInfoDir(self, submanifest_path, object_path=''):
983 """Return the path to submanifest-specific info for a submanifest.
984
985 Return the full path of the directory in which to put per-manifest objects.
986
987 Args:
988 submanifest_path: a string, the path of the submanifest, relative to the
989 outermost topdir. If empty, then repodir is returned.
990 object_path: a string, relative path to append to the submanifest info
991 directory path.
992 """
993 if submanifest_path:
994 return os.path.join(self.repodir, SUBMANIFEST_DIR, submanifest_path,
995 object_path)
996 else:
997 return os.path.join(self.repodir, object_path)
998
999 def SubmanifestProject(self, submanifest_path):
1000 """Return a manifestProject for a submanifest."""
1001 subdir = self.SubmanifestInfoDir(submanifest_path)
1002 mp = ManifestProject(self, 'manifests',
1003 gitdir=os.path.join(subdir, 'manifests.git'),
1004 worktree=os.path.join(subdir, 'manifests'))
1005 return mp
1006
1007 def GetDefaultGroupsStr(self, with_platform=True):
1008 """Returns the default group string to use.
1009
1010 Args:
1011 with_platform: a boolean, whether to include the group for the
1012 underlying platform.
1013 """
1014 groups = ','.join(self.default_groups or ['default'])
1015 if with_platform:
1016 groups += f',platform-{platform.system().lower()}'
1017 return groups
1018
1019 def GetGroupsStr(self):
1020 """Returns the manifest group string that should be synced."""
1021 return self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
1022
1023 def Unload(self):
1024 """Unload the manifest.
1025
1026 If the manifest files have been changed since Load() was called, this will
1027 cause the new/updated manifest to be used.
1028
1029 """
1030 self._loaded = False
1031 self._projects = {}
1032 self._paths = {}
1033 self._remotes = {}
1034 self._default = None
1035 self._submanifests = {}
1036 self._repo_hooks_project = None
1037 self._superproject = None
1038 self._contactinfo = ContactInfo(Wrapper().BUG_URL)
1039 self._notice = None
1040 self.branch = None
1041 self._manifest_server = None
1042
1043 def Load(self):
1044 """Read the manifest into memory."""
1045 # Do not expose internal arguments.
1046 self._Load()
1047
1048 def _Load(self, initial_client=None, submanifest_depth=0):
1049 if submanifest_depth > MAX_SUBMANIFEST_DEPTH:
1050 raise ManifestParseError('maximum submanifest depth %d exceeded.' %
1051 MAX_SUBMANIFEST_DEPTH)
1052 if not self._loaded:
1053 if self._outer_client and self._outer_client != self:
1054 # This will load all clients.
1055 self._outer_client._Load(initial_client=self)
1056
1057 savedManifestFile = self.manifestFile
1058 override = self._outer_client.manifestFileOverrides.get(self.path_prefix)
1059 if override:
1060 self.manifestFile = override
1061
1062 try:
1063 m = self.manifestProject
1064 b = m.GetBranch(m.CurrentBranch).merge
1065 if b is not None and b.startswith(R_HEADS):
1066 b = b[len(R_HEADS):]
1067 self.branch = b
1068
1069 parent_groups = self.parent_groups
1070 if self.path_prefix:
1071 parent_groups = f'{SUBMANIFEST_GROUP_PREFIX}:path:{self.path_prefix},{parent_groups}'
1072
1073 # The manifestFile was specified by the user which is why we allow include
1074 # paths to point anywhere.
1075 nodes = []
1076 nodes.append(self._ParseManifestXml(
1077 self.manifestFile, self.manifestProject.worktree,
1078 parent_groups=parent_groups, restrict_includes=False))
1079
1080 if self._load_local_manifests and self.local_manifests:
1081 try:
1082 for local_file in sorted(platform_utils.listdir(self.local_manifests)):
1083 if local_file.endswith('.xml'):
1084 local = os.path.join(self.local_manifests, local_file)
1085 # Since local manifests are entirely managed by the user, allow
1086 # them to point anywhere the user wants.
1087 local_group = f'{LOCAL_MANIFEST_GROUP_PREFIX}:{local_file[:-4]}'
1088 nodes.append(self._ParseManifestXml(
1089 local, self.subdir,
1090 parent_groups=f'{local_group},{parent_groups}',
1091 restrict_includes=False))
1092 except OSError:
1093 pass
1094 748
749 if self._superproject:
750 root.appendChild(doc.createTextNode(""))
751 e = doc.createElement("superproject")
752 e.setAttribute("name", self._superproject.name)
753 remoteName = None
754 if d.remote:
755 remoteName = d.remote.name
756 remote = self._superproject.remote
757 if not d.remote or remote.orig_name != remoteName:
758 remoteName = remote.orig_name
759 e.setAttribute("remote", remoteName)
760 revision = remote.revision or d.revisionExpr
761 if not revision or revision != self._superproject.revision:
762 e.setAttribute("revision", self._superproject.revision)
763 root.appendChild(e)
764
765 if self._contactinfo.bugurl != Wrapper().BUG_URL:
766 root.appendChild(doc.createTextNode(""))
767 e = doc.createElement("contactinfo")
768 e.setAttribute("bugurl", self._contactinfo.bugurl)
769 root.appendChild(e)
770
771 return doc
772
773 def ToDict(self, **kwargs):
774 """Return the current manifest as a dictionary."""
775 # Elements that may only appear once.
776 SINGLE_ELEMENTS = {
777 "notice",
778 "default",
779 "manifest-server",
780 "repo-hooks",
781 "superproject",
782 "contactinfo",
783 }
784 # Elements that may be repeated.
785 MULTI_ELEMENTS = {
786 "remote",
787 "remove-project",
788 "project",
789 "extend-project",
790 "include",
791 "submanifest",
792 # These are children of 'project' nodes.
793 "annotation",
794 "project",
795 "copyfile",
796 "linkfile",
797 }
798
799 doc = self.ToXml(**kwargs)
800 ret = {}
801
802 def append_children(ret, node):
803 for child in node.childNodes:
804 if child.nodeType == xml.dom.Node.ELEMENT_NODE:
805 attrs = child.attributes
806 element = dict(
807 (attrs.item(i).localName, attrs.item(i).value)
808 for i in range(attrs.length)
809 )
810 if child.nodeName in SINGLE_ELEMENTS:
811 ret[child.nodeName] = element
812 elif child.nodeName in MULTI_ELEMENTS:
813 ret.setdefault(child.nodeName, []).append(element)
814 else:
815 raise ManifestParseError(
816 'Unhandled element "%s"' % (child.nodeName,)
817 )
818
819 append_children(element, child)
820
821 append_children(ret, doc.firstChild)
822
823 return ret
824
825 def Save(self, fd, **kwargs):
826 """Write the current manifest out to the given file descriptor."""
827 doc = self.ToXml(**kwargs)
828 doc.writexml(fd, "", " ", "\n", "UTF-8")
829
830 def _output_manifest_project_extras(self, p, e):
831 """Manifests can modify e if they support extra project attributes."""
832
833 @property
834 def is_multimanifest(self):
835 """Whether this is a multimanifest checkout.
836
837 This is safe to use as long as the outermost manifest XML has been
838 parsed.
839 """
840 return bool(self._outer_client._submanifests)
841
842 @property
843 def is_submanifest(self):
844 """Whether this manifest is a submanifest.
845
846 This is safe to use as long as the outermost manifest XML has been
847 parsed.
848 """
849 return self._outer_client and self._outer_client != self
850
851 @property
852 def outer_client(self):
853 """The instance of the outermost manifest client."""
854 self._Load()
855 return self._outer_client
856
857 @property
858 def all_manifests(self):
859 """Generator yielding all (sub)manifests, in depth-first order."""
860 self._Load()
861 outer = self._outer_client
862 yield outer
863 for tree in outer.all_children:
864 yield tree
865
866 @property
867 def all_children(self):
868 """Generator yielding all (present) child submanifests."""
869 self._Load()
870 for child in self._submanifests.values():
871 if child.repo_client:
872 yield child.repo_client
873 for tree in child.repo_client.all_children:
874 yield tree
875
876 @property
877 def path_prefix(self):
878 """The path of this submanifest, relative to the outermost manifest."""
879 if not self._outer_client or self == self._outer_client:
880 return ""
881 return os.path.relpath(self.topdir, self._outer_client.topdir)
882
883 @property
884 def all_paths(self):
885 """All project paths for all (sub)manifests.
886
887 See also `paths`.
888
889 Returns:
890 A dictionary of {path: Project()}. `path` is relative to the outer
891 manifest.
892 """
893 ret = {}
894 for tree in self.all_manifests:
895 prefix = tree.path_prefix
896 ret.update(
897 {os.path.join(prefix, k): v for k, v in tree.paths.items()}
898 )
899 return ret
900
901 @property
902 def all_projects(self):
903 """All projects for all (sub)manifests. See `projects`."""
904 return list(
905 itertools.chain.from_iterable(
906 x._paths.values() for x in self.all_manifests
907 )
908 )
909
910 @property
911 def paths(self):
912 """Return all paths for this manifest.
913
914 Returns:
915 A dictionary of {path: Project()}. `path` is relative to this
916 manifest.
917 """
918 self._Load()
919 return self._paths
920
921 @property
922 def projects(self):
923 """Return a list of all Projects in this manifest."""
924 self._Load()
925 return list(self._paths.values())
926
927 @property
928 def remotes(self):
929 """Return a list of remotes for this manifest."""
930 self._Load()
931 return self._remotes
932
933 @property
934 def default(self):
935 """Return default values for this manifest."""
936 self._Load()
937 return self._default
938
939 @property
940 def submanifests(self):
941 """All submanifests in this manifest."""
942 self._Load()
943 return self._submanifests
944
945 @property
946 def repo_hooks_project(self):
947 self._Load()
948 return self._repo_hooks_project
949
950 @property
951 def superproject(self):
952 self._Load()
953 return self._superproject
954
955 @property
956 def contactinfo(self):
957 self._Load()
958 return self._contactinfo
959
960 @property
961 def notice(self):
962 self._Load()
963 return self._notice
964
965 @property
966 def manifest_server(self):
967 self._Load()
968 return self._manifest_server
969
970 @property
971 def CloneBundle(self):
972 clone_bundle = self.manifestProject.clone_bundle
973 if clone_bundle is None:
974 return False if self.manifestProject.partial_clone else True
975 else:
976 return clone_bundle
977
978 @property
979 def CloneFilter(self):
980 if self.manifestProject.partial_clone:
981 return self.manifestProject.clone_filter
982 return None
983
984 @property
985 def PartialCloneExclude(self):
986 exclude = self.manifest.manifestProject.partial_clone_exclude or ""
987 return set(x.strip() for x in exclude.split(","))
988
989 def SetManifestOverride(self, path):
990 """Override manifestFile. The caller must call Unload()"""
991 self._outer_client.manifest.manifestFileOverrides[
992 self.path_prefix
993 ] = path
994
995 @property
996 def UseLocalManifests(self):
997 return self._load_local_manifests
998
999 def SetUseLocalManifests(self, value):
1000 self._load_local_manifests = value
1001
1002 @property
1003 def HasLocalManifests(self):
1004 return self._load_local_manifests and self.local_manifests
1005
1006 def IsFromLocalManifest(self, project):
1007 """Is the project from a local manifest?"""
1008 return any(
1009 x.startswith(LOCAL_MANIFEST_GROUP_PREFIX) for x in project.groups
1010 )
1011
1012 @property
1013 def IsMirror(self):
1014 return self.manifestProject.mirror
1015
1016 @property
1017 def UseGitWorktrees(self):
1018 return self.manifestProject.use_worktree
1019
1020 @property
1021 def IsArchive(self):
1022 return self.manifestProject.archive
1023
1024 @property
1025 def HasSubmodules(self):
1026 return self.manifestProject.submodules
1027
1028 @property
1029 def EnableGitLfs(self):
1030 return self.manifestProject.git_lfs
1031
1032 def FindManifestByPath(self, path):
1033 """Returns the manifest containing path."""
1034 path = os.path.abspath(path)
1035 manifest = self._outer_client or self
1036 old = None
1037 while manifest._submanifests and manifest != old:
1038 old = manifest
1039 for name in manifest._submanifests:
1040 tree = manifest._submanifests[name]
1041 if path.startswith(tree.repo_client.manifest.topdir):
1042 manifest = tree.repo_client
1043 break
1044 return manifest
1045
1046 @property
1047 def subdir(self):
1048 """Returns the path for per-submanifest objects for this manifest."""
1049 return self.SubmanifestInfoDir(self.path_prefix)
1050
1051 def SubmanifestInfoDir(self, submanifest_path, object_path=""):
1052 """Return the path to submanifest-specific info for a submanifest.
1053
1054 Return the full path of the directory in which to put per-manifest
1055 objects.
1056
1057 Args:
1058 submanifest_path: a string, the path of the submanifest, relative to
1059 the outermost topdir. If empty, then repodir is returned.
1060 object_path: a string, relative path to append to the submanifest
1061 info directory path.
1062 """
1063 if submanifest_path:
1064 return os.path.join(
1065 self.repodir, SUBMANIFEST_DIR, submanifest_path, object_path
1066 )
1067 else:
1068 return os.path.join(self.repodir, object_path)
1069
1070 def SubmanifestProject(self, submanifest_path):
1071 """Return a manifestProject for a submanifest."""
1072 subdir = self.SubmanifestInfoDir(submanifest_path)
1073 mp = ManifestProject(
1074 self,
1075 "manifests",
1076 gitdir=os.path.join(subdir, "manifests.git"),
1077 worktree=os.path.join(subdir, "manifests"),
1078 )
1079 return mp
1080
1081 def GetDefaultGroupsStr(self, with_platform=True):
1082 """Returns the default group string to use.
1083
1084 Args:
1085 with_platform: a boolean, whether to include the group for the
1086 underlying platform.
1087 """
1088 groups = ",".join(self.default_groups or ["default"])
1089 if with_platform:
1090 groups += f",platform-{platform.system().lower()}"
1091 return groups
1092
1093 def GetGroupsStr(self):
1094 """Returns the manifest group string that should be synced."""
1095 return (
1096 self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
1097 )
1098
1099 def Unload(self):
1100 """Unload the manifest.
1101
1102 If the manifest files have been changed since Load() was called, this
1103 will cause the new/updated manifest to be used.
1104
1105 """
1106 self._loaded = False
1107 self._projects = {}
1108 self._paths = {}
1109 self._remotes = {}
1110 self._default = None
1111 self._submanifests = {}
1112 self._repo_hooks_project = None
1113 self._superproject = None
1114 self._contactinfo = ContactInfo(Wrapper().BUG_URL)
1115 self._notice = None
1116 self.branch = None
1117 self._manifest_server = None
1118
1119 def Load(self):
1120 """Read the manifest into memory."""
1121 # Do not expose internal arguments.
1122 self._Load()
1123
1124 def _Load(self, initial_client=None, submanifest_depth=0):
1125 if submanifest_depth > MAX_SUBMANIFEST_DEPTH:
1126 raise ManifestParseError(
1127 "maximum submanifest depth %d exceeded." % MAX_SUBMANIFEST_DEPTH
1128 )
1129 if not self._loaded:
1130 if self._outer_client and self._outer_client != self:
1131 # This will load all clients.
1132 self._outer_client._Load(initial_client=self)
1133
1134 savedManifestFile = self.manifestFile
1135 override = self._outer_client.manifestFileOverrides.get(
1136 self.path_prefix
1137 )
1138 if override:
1139 self.manifestFile = override
1140
1141 try:
1142 m = self.manifestProject
1143 b = m.GetBranch(m.CurrentBranch).merge
1144 if b is not None and b.startswith(R_HEADS):
1145 b = b[len(R_HEADS) :]
1146 self.branch = b
1147
1148 parent_groups = self.parent_groups
1149 if self.path_prefix:
1150 parent_groups = (
1151 f"{SUBMANIFEST_GROUP_PREFIX}:path:"
1152 f"{self.path_prefix},{parent_groups}"
1153 )
1154
1155 # The manifestFile was specified by the user which is why we
1156 # allow include paths to point anywhere.
1157 nodes = []
1158 nodes.append(
1159 self._ParseManifestXml(
1160 self.manifestFile,
1161 self.manifestProject.worktree,
1162 parent_groups=parent_groups,
1163 restrict_includes=False,
1164 )
1165 )
1166
1167 if self._load_local_manifests and self.local_manifests:
1168 try:
1169 for local_file in sorted(
1170 platform_utils.listdir(self.local_manifests)
1171 ):
1172 if local_file.endswith(".xml"):
1173 local = os.path.join(
1174 self.local_manifests, local_file
1175 )
1176 # Since local manifests are entirely managed by
1177 # the user, allow them to point anywhere the
1178 # user wants.
1179 local_group = (
1180 f"{LOCAL_MANIFEST_GROUP_PREFIX}:"
1181 f"{local_file[:-4]}"
1182 )
1183 nodes.append(
1184 self._ParseManifestXml(
1185 local,
1186 self.subdir,
1187 parent_groups=(
1188 f"{local_group},{parent_groups}"
1189 ),
1190 restrict_includes=False,
1191 )
1192 )
1193 except OSError:
1194 pass
1195
1196 try:
1197 self._ParseManifest(nodes)
1198 except ManifestParseError as e:
1199 # There was a problem parsing, unload ourselves in case they
1200 # catch this error and try again later, we will show the
1201 # correct error
1202 self.Unload()
1203 raise e
1204
1205 if self.IsMirror:
1206 self._AddMetaProjectMirror(self.repoProject)
1207 self._AddMetaProjectMirror(self.manifestProject)
1208
1209 self._loaded = True
1210 finally:
1211 if override:
1212 self.manifestFile = savedManifestFile
1213
1214 # Now that we have loaded this manifest, load any submanifests as
1215 # well. We need to do this after self._loaded is set to avoid
1216 # looping.
1217 for name in self._submanifests:
1218 tree = self._submanifests[name]
1219 tree.ToSubmanifestSpec()
1220 present = os.path.exists(
1221 os.path.join(self.subdir, MANIFEST_FILE_NAME)
1222 )
1223 if present and tree.present and not tree.repo_client:
1224 if initial_client and initial_client.topdir == self.topdir:
1225 tree.repo_client = self
1226 tree.present = present
1227 elif not os.path.exists(self.subdir):
1228 tree.present = False
1229 if present and tree.present:
1230 tree.repo_client._Load(
1231 initial_client=initial_client,
1232 submanifest_depth=submanifest_depth + 1,
1233 )
1234
1235 def _ParseManifestXml(
1236 self, path, include_root, parent_groups="", restrict_includes=True
1237 ):
1238 """Parse a manifest XML and return the computed nodes.
1239
1240 Args:
1241 path: The XML file to read & parse.
1242 include_root: The path to interpret include "name"s relative to.
1243 parent_groups: The groups to apply to this projects.
1244 restrict_includes: Whether to constrain the "name" attribute of
1245 includes.
1246
1247 Returns:
1248 List of XML nodes.
1249 """
1095 try: 1250 try:
1096 self._ParseManifest(nodes) 1251 root = xml.dom.minidom.parse(path)
1097 except ManifestParseError as e: 1252 except (OSError, xml.parsers.expat.ExpatError) as e:
1098 # There was a problem parsing, unload ourselves in case they catch 1253 raise ManifestParseError(
1099 # this error and try again later, we will show the correct error 1254 "error parsing manifest %s: %s" % (path, e)
1100 self.Unload() 1255 )
1101 raise e 1256
1102 1257 if not root or not root.childNodes:
1103 if self.IsMirror: 1258 raise ManifestParseError("no root node in %s" % (path,))
1104 self._AddMetaProjectMirror(self.repoProject) 1259
1105 self._AddMetaProjectMirror(self.manifestProject) 1260 for manifest in root.childNodes:
1106 1261 if manifest.nodeName == "manifest":
1107 self._loaded = True 1262 break
1108 finally: 1263 else:
1109 if override: 1264 raise ManifestParseError("no <manifest> in %s" % (path,))
1110 self.manifestFile = savedManifestFile
1111
1112 # Now that we have loaded this manifest, load any submanifests as well.
1113 # We need to do this after self._loaded is set to avoid looping.
1114 for name in self._submanifests:
1115 tree = self._submanifests[name]
1116 spec = tree.ToSubmanifestSpec()
1117 present = os.path.exists(os.path.join(self.subdir, MANIFEST_FILE_NAME))
1118 if present and tree.present and not tree.repo_client:
1119 if initial_client and initial_client.topdir == self.topdir:
1120 tree.repo_client = self
1121 tree.present = present
1122 elif not os.path.exists(self.subdir):
1123 tree.present = False
1124 if present and tree.present:
1125 tree.repo_client._Load(initial_client=initial_client,
1126 submanifest_depth=submanifest_depth + 1)
1127
1128 def _ParseManifestXml(self, path, include_root, parent_groups='',
1129 restrict_includes=True):
1130 """Parse a manifest XML and return the computed nodes.
1131
1132 Args:
1133 path: The XML file to read & parse.
1134 include_root: The path to interpret include "name"s relative to.
1135 parent_groups: The groups to apply to this projects.
1136 restrict_includes: Whether to constrain the "name" attribute of includes.
1137
1138 Returns:
1139 List of XML nodes.
1140 """
1141 try:
1142 root = xml.dom.minidom.parse(path)
1143 except (OSError, xml.parsers.expat.ExpatError) as e:
1144 raise ManifestParseError("error parsing manifest %s: %s" % (path, e))
1145 1265
1146 if not root or not root.childNodes: 1266 nodes = []
1147 raise ManifestParseError("no root node in %s" % (path,)) 1267 for node in manifest.childNodes:
1268 if node.nodeName == "include":
1269 name = self._reqatt(node, "name")
1270 if restrict_includes:
1271 msg = self._CheckLocalPath(name)
1272 if msg:
1273 raise ManifestInvalidPathError(
1274 '<include> invalid "name": %s: %s' % (name, msg)
1275 )
1276 include_groups = ""
1277 if parent_groups:
1278 include_groups = parent_groups
1279 if node.hasAttribute("groups"):
1280 include_groups = (
1281 node.getAttribute("groups") + "," + include_groups
1282 )
1283 fp = os.path.join(include_root, name)
1284 if not os.path.isfile(fp):
1285 raise ManifestParseError(
1286 "include [%s/]%s doesn't exist or isn't a file"
1287 % (include_root, name)
1288 )
1289 try:
1290 nodes.extend(
1291 self._ParseManifestXml(fp, include_root, include_groups)
1292 )
1293 # should isolate this to the exact exception, but that's
1294 # tricky. actual parsing implementation may vary.
1295 except (
1296 KeyboardInterrupt,
1297 RuntimeError,
1298 SystemExit,
1299 ManifestParseError,
1300 ):
1301 raise
1302 except Exception as e:
1303 raise ManifestParseError(
1304 "failed parsing included manifest %s: %s" % (name, e)
1305 )
1306 else:
1307 if parent_groups and node.nodeName == "project":
1308 nodeGroups = parent_groups
1309 if node.hasAttribute("groups"):
1310 nodeGroups = (
1311 node.getAttribute("groups") + "," + nodeGroups
1312 )
1313 node.setAttribute("groups", nodeGroups)
1314 nodes.append(node)
1315 return nodes
1316
1317 def _ParseManifest(self, node_list):
1318 for node in itertools.chain(*node_list):
1319 if node.nodeName == "remote":
1320 remote = self._ParseRemote(node)
1321 if remote:
1322 if remote.name in self._remotes:
1323 if remote != self._remotes[remote.name]:
1324 raise ManifestParseError(
1325 "remote %s already exists with different "
1326 "attributes" % (remote.name)
1327 )
1328 else:
1329 self._remotes[remote.name] = remote
1330
1331 for node in itertools.chain(*node_list):
1332 if node.nodeName == "default":
1333 new_default = self._ParseDefault(node)
1334 emptyDefault = (
1335 not node.hasAttributes() and not node.hasChildNodes()
1336 )
1337 if self._default is None:
1338 self._default = new_default
1339 elif not emptyDefault and new_default != self._default:
1340 raise ManifestParseError(
1341 "duplicate default in %s" % (self.manifestFile)
1342 )
1148 1343
1149 for manifest in root.childNodes:
1150 if manifest.nodeName == 'manifest':
1151 break
1152 else:
1153 raise ManifestParseError("no <manifest> in %s" % (path,))
1154
1155 nodes = []
1156 for node in manifest.childNodes:
1157 if node.nodeName == 'include':
1158 name = self._reqatt(node, 'name')
1159 if restrict_includes:
1160 msg = self._CheckLocalPath(name)
1161 if msg:
1162 raise ManifestInvalidPathError(
1163 '<include> invalid "name": %s: %s' % (name, msg))
1164 include_groups = ''
1165 if parent_groups:
1166 include_groups = parent_groups
1167 if node.hasAttribute('groups'):
1168 include_groups = node.getAttribute('groups') + ',' + include_groups
1169 fp = os.path.join(include_root, name)
1170 if not os.path.isfile(fp):
1171 raise ManifestParseError("include [%s/]%s doesn't exist or isn't a file"
1172 % (include_root, name))
1173 try:
1174 nodes.extend(self._ParseManifestXml(fp, include_root, include_groups))
1175 # should isolate this to the exact exception, but that's
1176 # tricky. actual parsing implementation may vary.
1177 except (KeyboardInterrupt, RuntimeError, SystemExit, ManifestParseError):
1178 raise
1179 except Exception as e:
1180 raise ManifestParseError(
1181 "failed parsing included manifest %s: %s" % (name, e))
1182 else:
1183 if parent_groups and node.nodeName == 'project':
1184 nodeGroups = parent_groups
1185 if node.hasAttribute('groups'):
1186 nodeGroups = node.getAttribute('groups') + ',' + nodeGroups
1187 node.setAttribute('groups', nodeGroups)
1188 nodes.append(node)
1189 return nodes
1190
1191 def _ParseManifest(self, node_list):
1192 for node in itertools.chain(*node_list):
1193 if node.nodeName == 'remote':
1194 remote = self._ParseRemote(node)
1195 if remote:
1196 if remote.name in self._remotes:
1197 if remote != self._remotes[remote.name]:
1198 raise ManifestParseError(
1199 'remote %s already exists with different attributes' %
1200 (remote.name))
1201 else:
1202 self._remotes[remote.name] = remote
1203
1204 for node in itertools.chain(*node_list):
1205 if node.nodeName == 'default':
1206 new_default = self._ParseDefault(node)
1207 emptyDefault = not node.hasAttributes() and not node.hasChildNodes()
1208 if self._default is None: 1344 if self._default is None:
1209 self._default = new_default 1345 self._default = _Default()
1210 elif not emptyDefault and new_default != self._default: 1346
1211 raise ManifestParseError('duplicate default in %s' % 1347 submanifest_paths = set()
1212 (self.manifestFile)) 1348 for node in itertools.chain(*node_list):
1213 1349 if node.nodeName == "submanifest":
1214 if self._default is None: 1350 submanifest = self._ParseSubmanifest(node)
1215 self._default = _Default() 1351 if submanifest:
1216 1352 if submanifest.name in self._submanifests:
1217 submanifest_paths = set() 1353 if submanifest != self._submanifests[submanifest.name]:
1218 for node in itertools.chain(*node_list): 1354 raise ManifestParseError(
1219 if node.nodeName == 'submanifest': 1355 "submanifest %s already exists with different "
1220 submanifest = self._ParseSubmanifest(node) 1356 "attributes" % (submanifest.name)
1221 if submanifest: 1357 )
1222 if submanifest.name in self._submanifests: 1358 else:
1223 if submanifest != self._submanifests[submanifest.name]: 1359 self._submanifests[submanifest.name] = submanifest
1224 raise ManifestParseError( 1360 submanifest_paths.add(submanifest.relpath)
1225 'submanifest %s already exists with different attributes' % 1361
1226 (submanifest.name)) 1362 for node in itertools.chain(*node_list):
1227 else: 1363 if node.nodeName == "notice":
1228 self._submanifests[submanifest.name] = submanifest 1364 if self._notice is not None:
1229 submanifest_paths.add(submanifest.relpath) 1365 raise ManifestParseError(
1230 1366 "duplicate notice in %s" % (self.manifestFile)
1231 for node in itertools.chain(*node_list): 1367 )
1232 if node.nodeName == 'notice': 1368 self._notice = self._ParseNotice(node)
1233 if self._notice is not None: 1369
1234 raise ManifestParseError( 1370 for node in itertools.chain(*node_list):
1235 'duplicate notice in %s' % 1371 if node.nodeName == "manifest-server":
1236 (self.manifestFile)) 1372 url = self._reqatt(node, "url")
1237 self._notice = self._ParseNotice(node) 1373 if self._manifest_server is not None:
1238 1374 raise ManifestParseError(
1239 for node in itertools.chain(*node_list): 1375 "duplicate manifest-server in %s" % (self.manifestFile)
1240 if node.nodeName == 'manifest-server': 1376 )
1241 url = self._reqatt(node, 'url') 1377 self._manifest_server = url
1242 if self._manifest_server is not None: 1378
1243 raise ManifestParseError( 1379 def recursively_add_projects(project):
1244 'duplicate manifest-server in %s' % 1380 projects = self._projects.setdefault(project.name, [])
1245 (self.manifestFile)) 1381 if project.relpath is None:
1246 self._manifest_server = url 1382 raise ManifestParseError(
1247 1383 "missing path for %s in %s"
1248 def recursively_add_projects(project): 1384 % (project.name, self.manifestFile)
1249 projects = self._projects.setdefault(project.name, []) 1385 )
1250 if project.relpath is None: 1386 if project.relpath in self._paths:
1251 raise ManifestParseError( 1387 raise ManifestParseError(
1252 'missing path for %s in %s' % 1388 "duplicate path %s in %s"
1253 (project.name, self.manifestFile)) 1389 % (project.relpath, self.manifestFile)
1254 if project.relpath in self._paths: 1390 )
1255 raise ManifestParseError( 1391 for tree in submanifest_paths:
1256 'duplicate path %s in %s' % 1392 if project.relpath.startswith(tree):
1257 (project.relpath, self.manifestFile)) 1393 raise ManifestParseError(
1258 for tree in submanifest_paths: 1394 "project %s conflicts with submanifest path %s"
1259 if project.relpath.startswith(tree): 1395 % (project.relpath, tree)
1260 raise ManifestParseError( 1396 )
1261 'project %s conflicts with submanifest path %s' % 1397 self._paths[project.relpath] = project
1262 (project.relpath, tree)) 1398 projects.append(project)
1263 self._paths[project.relpath] = project 1399 for subproject in project.subprojects:
1264 projects.append(project) 1400 recursively_add_projects(subproject)
1265 for subproject in project.subprojects: 1401
1266 recursively_add_projects(subproject) 1402 repo_hooks_project = None
1267 1403 enabled_repo_hooks = None
1268 repo_hooks_project = None 1404 for node in itertools.chain(*node_list):
1269 enabled_repo_hooks = None 1405 if node.nodeName == "project":
1270 for node in itertools.chain(*node_list): 1406 project = self._ParseProject(node)
1271 if node.nodeName == 'project': 1407 recursively_add_projects(project)
1272 project = self._ParseProject(node) 1408 if node.nodeName == "extend-project":
1273 recursively_add_projects(project) 1409 name = self._reqatt(node, "name")
1274 if node.nodeName == 'extend-project': 1410
1275 name = self._reqatt(node, 'name') 1411 if name not in self._projects:
1412 raise ManifestParseError(
1413 "extend-project element specifies non-existent "
1414 "project: %s" % name
1415 )
1416
1417 path = node.getAttribute("path")
1418 dest_path = node.getAttribute("dest-path")
1419 groups = node.getAttribute("groups")
1420 if groups:
1421 groups = self._ParseList(groups)
1422 revision = node.getAttribute("revision")
1423 remote_name = node.getAttribute("remote")
1424 if not remote_name:
1425 remote = self._default.remote
1426 else:
1427 remote = self._get_remote(node)
1428 dest_branch = node.getAttribute("dest-branch")
1429 upstream = node.getAttribute("upstream")
1430
1431 named_projects = self._projects[name]
1432 if dest_path and not path and len(named_projects) > 1:
1433 raise ManifestParseError(
1434 "extend-project cannot use dest-path when "
1435 "matching multiple projects: %s" % name
1436 )
1437 for p in self._projects[name]:
1438 if path and p.relpath != path:
1439 continue
1440 if groups:
1441 p.groups.extend(groups)
1442 if revision:
1443 p.SetRevision(revision)
1444
1445 if remote_name:
1446 p.remote = remote.ToRemoteSpec(name)
1447 if dest_branch:
1448 p.dest_branch = dest_branch
1449 if upstream:
1450 p.upstream = upstream
1451
1452 if dest_path:
1453 del self._paths[p.relpath]
1454 (
1455 relpath,
1456 worktree,
1457 gitdir,
1458 objdir,
1459 _,
1460 ) = self.GetProjectPaths(name, dest_path, remote.name)
1461 p.UpdatePaths(relpath, worktree, gitdir, objdir)
1462 self._paths[p.relpath] = p
1463
1464 if node.nodeName == "repo-hooks":
1465 # Only one project can be the hooks project
1466 if repo_hooks_project is not None:
1467 raise ManifestParseError(
1468 "duplicate repo-hooks in %s" % (self.manifestFile)
1469 )
1470
1471 # Get the name of the project and the (space-separated) list of
1472 # enabled.
1473 repo_hooks_project = self._reqatt(node, "in-project")
1474 enabled_repo_hooks = self._ParseList(
1475 self._reqatt(node, "enabled-list")
1476 )
1477 if node.nodeName == "superproject":
1478 name = self._reqatt(node, "name")
1479 # There can only be one superproject.
1480 if self._superproject:
1481 raise ManifestParseError(
1482 "duplicate superproject in %s" % (self.manifestFile)
1483 )
1484 remote_name = node.getAttribute("remote")
1485 if not remote_name:
1486 remote = self._default.remote
1487 else:
1488 remote = self._get_remote(node)
1489 if remote is None:
1490 raise ManifestParseError(
1491 "no remote for superproject %s within %s"
1492 % (name, self.manifestFile)
1493 )
1494 revision = node.getAttribute("revision") or remote.revision
1495 if not revision:
1496 revision = self._default.revisionExpr
1497 if not revision:
1498 raise ManifestParseError(
1499 "no revision for superproject %s within %s"
1500 % (name, self.manifestFile)
1501 )
1502 self._superproject = Superproject(
1503 self,
1504 name=name,
1505 remote=remote.ToRemoteSpec(name),
1506 revision=revision,
1507 )
1508 if node.nodeName == "contactinfo":
1509 bugurl = self._reqatt(node, "bugurl")
1510 # This element can be repeated, later entries will clobber
1511 # earlier ones.
1512 self._contactinfo = ContactInfo(bugurl)
1513
1514 if node.nodeName == "remove-project":
1515 name = self._reqatt(node, "name")
1516
1517 if name in self._projects:
1518 for p in self._projects[name]:
1519 del self._paths[p.relpath]
1520 del self._projects[name]
1521
1522 # If the manifest removes the hooks project, treat it as if
1523 # it deleted
1524 # the repo-hooks element too.
1525 if repo_hooks_project == name:
1526 repo_hooks_project = None
1527 elif not XmlBool(node, "optional", False):
1528 raise ManifestParseError(
1529 "remove-project element specifies non-existent "
1530 "project: %s" % name
1531 )
1532
1533 # Store repo hooks project information.
1534 if repo_hooks_project:
1535 # Store a reference to the Project.
1536 try:
1537 repo_hooks_projects = self._projects[repo_hooks_project]
1538 except KeyError:
1539 raise ManifestParseError(
1540 "project %s not found for repo-hooks" % (repo_hooks_project)
1541 )
1542
1543 if len(repo_hooks_projects) != 1:
1544 raise ManifestParseError(
1545 "internal error parsing repo-hooks in %s"
1546 % (self.manifestFile)
1547 )
1548 self._repo_hooks_project = repo_hooks_projects[0]
1549 # Store the enabled hooks in the Project object.
1550 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
1551
1552 def _AddMetaProjectMirror(self, m):
1553 name = None
1554 m_url = m.GetRemote().url
1555 if m_url.endswith("/.git"):
1556 raise ManifestParseError("refusing to mirror %s" % m_url)
1557
1558 if self._default and self._default.remote:
1559 url = self._default.remote.resolvedFetchUrl
1560 if not url.endswith("/"):
1561 url += "/"
1562 if m_url.startswith(url):
1563 remote = self._default.remote
1564 name = m_url[len(url) :]
1565
1566 if name is None:
1567 s = m_url.rindex("/") + 1
1568 manifestUrl = self.manifestProject.config.GetString(
1569 "remote.origin.url"
1570 )
1571 remote = _XmlRemote(
1572 "origin", fetch=m_url[:s], manifestUrl=manifestUrl
1573 )
1574 name = m_url[s:]
1575
1576 if name.endswith(".git"):
1577 name = name[:-4]
1276 1578
1277 if name not in self._projects: 1579 if name not in self._projects:
1278 raise ManifestParseError('extend-project element specifies non-existent ' 1580 m.PreSync()
1279 'project: %s' % name) 1581 gitdir = os.path.join(self.topdir, "%s.git" % name)
1582 project = Project(
1583 manifest=self,
1584 name=name,
1585 remote=remote.ToRemoteSpec(name),
1586 gitdir=gitdir,
1587 objdir=gitdir,
1588 worktree=None,
1589 relpath=name or None,
1590 revisionExpr=m.revisionExpr,
1591 revisionId=None,
1592 )
1593 self._projects[project.name] = [project]
1594 self._paths[project.relpath] = project
1595
1596 def _ParseRemote(self, node):
1597 """
1598 reads a <remote> element from the manifest file
1599 """
1600 name = self._reqatt(node, "name")
1601 alias = node.getAttribute("alias")
1602 if alias == "":
1603 alias = None
1604 fetch = self._reqatt(node, "fetch")
1605 pushUrl = node.getAttribute("pushurl")
1606 if pushUrl == "":
1607 pushUrl = None
1608 review = node.getAttribute("review")
1609 if review == "":
1610 review = None
1611 revision = node.getAttribute("revision")
1612 if revision == "":
1613 revision = None
1614 manifestUrl = self.manifestProject.config.GetString("remote.origin.url")
1615
1616 remote = _XmlRemote(
1617 name, alias, fetch, pushUrl, manifestUrl, review, revision
1618 )
1619
1620 for n in node.childNodes:
1621 if n.nodeName == "annotation":
1622 self._ParseAnnotation(remote, n)
1623
1624 return remote
1625
1626 def _ParseDefault(self, node):
1627 """
1628 reads a <default> element from the manifest file
1629 """
1630 d = _Default()
1631 d.remote = self._get_remote(node)
1632 d.revisionExpr = node.getAttribute("revision")
1633 if d.revisionExpr == "":
1634 d.revisionExpr = None
1635
1636 d.destBranchExpr = node.getAttribute("dest-branch") or None
1637 d.upstreamExpr = node.getAttribute("upstream") or None
1638
1639 d.sync_j = XmlInt(node, "sync-j", None)
1640 if d.sync_j is not None and d.sync_j <= 0:
1641 raise ManifestParseError(
1642 '%s: sync-j must be greater than 0, not "%s"'
1643 % (self.manifestFile, d.sync_j)
1644 )
1645
1646 d.sync_c = XmlBool(node, "sync-c", False)
1647 d.sync_s = XmlBool(node, "sync-s", False)
1648 d.sync_tags = XmlBool(node, "sync-tags", True)
1649 return d
1650
1651 def _ParseNotice(self, node):
1652 """
1653 reads a <notice> element from the manifest file
1654
1655 The <notice> element is distinct from other tags in the XML in that the
1656 data is conveyed between the start and end tag (it's not an
1657 empty-element tag).
1658
1659 The white space (carriage returns, indentation) for the notice element
1660 is relevant and is parsed in a way that is based on how python
1661 docstrings work. In fact, the code is remarkably similar to here:
1662 http://www.python.org/dev/peps/pep-0257/
1663 """
1664 # Get the data out of the node...
1665 notice = node.childNodes[0].data
1666
1667 # Figure out minimum indentation, skipping the first line (the same line
1668 # as the <notice> tag)...
1669 minIndent = sys.maxsize
1670 lines = notice.splitlines()
1671 for line in lines[1:]:
1672 lstrippedLine = line.lstrip()
1673 if lstrippedLine:
1674 indent = len(line) - len(lstrippedLine)
1675 minIndent = min(indent, minIndent)
1676
1677 # Strip leading / trailing blank lines and also indentation.
1678 cleanLines = [lines[0].strip()]
1679 for line in lines[1:]:
1680 cleanLines.append(line[minIndent:].rstrip())
1681
1682 # Clear completely blank lines from front and back...
1683 while cleanLines and not cleanLines[0]:
1684 del cleanLines[0]
1685 while cleanLines and not cleanLines[-1]:
1686 del cleanLines[-1]
1687
1688 return "\n".join(cleanLines)
1689
1690 def _ParseSubmanifest(self, node):
1691 """Reads a <submanifest> element from the manifest file."""
1692 name = self._reqatt(node, "name")
1693 remote = node.getAttribute("remote")
1694 if remote == "":
1695 remote = None
1696 project = node.getAttribute("project")
1697 if project == "":
1698 project = None
1699 revision = node.getAttribute("revision")
1700 if revision == "":
1701 revision = None
1702 manifestName = node.getAttribute("manifest-name")
1703 if manifestName == "":
1704 manifestName = None
1705 groups = ""
1706 if node.hasAttribute("groups"):
1707 groups = node.getAttribute("groups")
1708 groups = self._ParseList(groups)
1709 default_groups = self._ParseList(node.getAttribute("default-groups"))
1710 path = node.getAttribute("path")
1711 if path == "":
1712 path = None
1713 if revision:
1714 msg = self._CheckLocalPath(revision.split("/")[-1])
1715 if msg:
1716 raise ManifestInvalidPathError(
1717 '<submanifest> invalid "revision": %s: %s'
1718 % (revision, msg)
1719 )
1720 else:
1721 msg = self._CheckLocalPath(name)
1722 if msg:
1723 raise ManifestInvalidPathError(
1724 '<submanifest> invalid "name": %s: %s' % (name, msg)
1725 )
1726 else:
1727 msg = self._CheckLocalPath(path)
1728 if msg:
1729 raise ManifestInvalidPathError(
1730 '<submanifest> invalid "path": %s: %s' % (path, msg)
1731 )
1732
1733 submanifest = _XmlSubmanifest(
1734 name,
1735 remote,
1736 project,
1737 revision,
1738 manifestName,
1739 groups,
1740 default_groups,
1741 path,
1742 self,
1743 )
1744
1745 for n in node.childNodes:
1746 if n.nodeName == "annotation":
1747 self._ParseAnnotation(submanifest, n)
1748
1749 return submanifest
1750
1751 def _JoinName(self, parent_name, name):
1752 return os.path.join(parent_name, name)
1753
1754 def _UnjoinName(self, parent_name, name):
1755 return os.path.relpath(name, parent_name)
1756
1757 def _ParseProject(self, node, parent=None, **extra_proj_attrs):
1758 """
1759 reads a <project> element from the manifest file
1760 """
1761 name = self._reqatt(node, "name")
1762 msg = self._CheckLocalPath(name, dir_ok=True)
1763 if msg:
1764 raise ManifestInvalidPathError(
1765 '<project> invalid "name": %s: %s' % (name, msg)
1766 )
1767 if parent:
1768 name = self._JoinName(parent.name, name)
1280 1769
1281 path = node.getAttribute('path') 1770 remote = self._get_remote(node)
1282 dest_path = node.getAttribute('dest-path') 1771 if remote is None:
1283 groups = node.getAttribute('groups') 1772 remote = self._default.remote
1284 if groups: 1773 if remote is None:
1285 groups = self._ParseList(groups) 1774 raise ManifestParseError(
1286 revision = node.getAttribute('revision') 1775 "no remote for project %s within %s" % (name, self.manifestFile)
1287 remote_name = node.getAttribute('remote') 1776 )
1288 if not remote_name: 1777
1289 remote = self._default.remote 1778 revisionExpr = node.getAttribute("revision") or remote.revision
1779 if not revisionExpr:
1780 revisionExpr = self._default.revisionExpr
1781 if not revisionExpr:
1782 raise ManifestParseError(
1783 "no revision for project %s within %s"
1784 % (name, self.manifestFile)
1785 )
1786
1787 path = node.getAttribute("path")
1788 if not path:
1789 path = name
1290 else: 1790 else:
1291 remote = self._get_remote(node) 1791 # NB: The "." project is handled specially in
1292 dest_branch = node.getAttribute('dest-branch') 1792 # Project.Sync_LocalHalf.
1293 upstream = node.getAttribute('upstream') 1793 msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
1294 1794 if msg:
1295 named_projects = self._projects[name] 1795 raise ManifestInvalidPathError(
1296 if dest_path and not path and len(named_projects) > 1: 1796 '<project> invalid "path": %s: %s' % (path, msg)
1297 raise ManifestParseError('extend-project cannot use dest-path when ' 1797 )
1298 'matching multiple projects: %s' % name) 1798
1299 for p in self._projects[name]: 1799 rebase = XmlBool(node, "rebase", True)
1300 if path and p.relpath != path: 1800 sync_c = XmlBool(node, "sync-c", False)
1301 continue 1801 sync_s = XmlBool(node, "sync-s", self._default.sync_s)
1302 if groups: 1802 sync_tags = XmlBool(node, "sync-tags", self._default.sync_tags)
1303 p.groups.extend(groups) 1803
1304 if revision: 1804 clone_depth = XmlInt(node, "clone-depth")
1305 p.SetRevision(revision) 1805 if clone_depth is not None and clone_depth <= 0:
1306 1806 raise ManifestParseError(
1307 if remote_name: 1807 '%s: clone-depth must be greater than 0, not "%s"'
1308 p.remote = remote.ToRemoteSpec(name) 1808 % (self.manifestFile, clone_depth)
1309 if dest_branch: 1809 )
1310 p.dest_branch = dest_branch 1810
1311 if upstream: 1811 dest_branch = (
1312 p.upstream = upstream 1812 node.getAttribute("dest-branch") or self._default.destBranchExpr
1313 1813 )
1314 if dest_path: 1814
1315 del self._paths[p.relpath] 1815 upstream = node.getAttribute("upstream") or self._default.upstreamExpr
1316 relpath, worktree, gitdir, objdir, _ = self.GetProjectPaths( 1816
1317 name, dest_path, remote.name) 1817 groups = ""
1318 p.UpdatePaths(relpath, worktree, gitdir, objdir) 1818 if node.hasAttribute("groups"):
1319 self._paths[p.relpath] = p 1819 groups = node.getAttribute("groups")
1320 1820 groups = self._ParseList(groups)
1321 if node.nodeName == 'repo-hooks': 1821
1322 # Only one project can be the hooks project 1822 if parent is None:
1323 if repo_hooks_project is not None: 1823 (
1324 raise ManifestParseError( 1824 relpath,
1325 'duplicate repo-hooks in %s' % 1825 worktree,
1326 (self.manifestFile)) 1826 gitdir,
1327 1827 objdir,
1328 # Get the name of the project and the (space-separated) list of enabled. 1828 use_git_worktrees,
1329 repo_hooks_project = self._reqatt(node, 'in-project') 1829 ) = self.GetProjectPaths(name, path, remote.name)
1330 enabled_repo_hooks = self._ParseList(self._reqatt(node, 'enabled-list'))
1331 if node.nodeName == 'superproject':
1332 name = self._reqatt(node, 'name')
1333 # There can only be one superproject.
1334 if self._superproject:
1335 raise ManifestParseError(
1336 'duplicate superproject in %s' %
1337 (self.manifestFile))
1338 remote_name = node.getAttribute('remote')
1339 if not remote_name:
1340 remote = self._default.remote
1341 else: 1830 else:
1342 remote = self._get_remote(node) 1831 use_git_worktrees = False
1343 if remote is None: 1832 relpath, worktree, gitdir, objdir = self.GetSubprojectPaths(
1344 raise ManifestParseError("no remote for superproject %s within %s" % 1833 parent, name, path
1345 (name, self.manifestFile)) 1834 )
1346 revision = node.getAttribute('revision') or remote.revision 1835
1347 if not revision: 1836 default_groups = ["all", "name:%s" % name, "path:%s" % relpath]
1348 revision = self._default.revisionExpr 1837 groups.extend(set(default_groups).difference(groups))
1349 if not revision: 1838
1350 raise ManifestParseError('no revision for superproject %s within %s' % 1839 if self.IsMirror and node.hasAttribute("force-path"):
1351 (name, self.manifestFile)) 1840 if XmlBool(node, "force-path", False):
1352 self._superproject = Superproject(self, 1841 gitdir = os.path.join(self.topdir, "%s.git" % path)
1353 name=name, 1842
1354 remote=remote.ToRemoteSpec(name), 1843 project = Project(
1355 revision=revision) 1844 manifest=self,
1356 if node.nodeName == 'contactinfo': 1845 name=name,
1357 bugurl = self._reqatt(node, 'bugurl') 1846 remote=remote.ToRemoteSpec(name),
1358 # This element can be repeated, later entries will clobber earlier ones. 1847 gitdir=gitdir,
1359 self._contactinfo = ContactInfo(bugurl) 1848 objdir=objdir,
1360 1849 worktree=worktree,
1361 if node.nodeName == 'remove-project': 1850 relpath=relpath,
1362 name = self._reqatt(node, 'name') 1851 revisionExpr=revisionExpr,
1363 1852 revisionId=None,
1364 if name in self._projects: 1853 rebase=rebase,
1365 for p in self._projects[name]: 1854 groups=groups,
1366 del self._paths[p.relpath] 1855 sync_c=sync_c,
1367 del self._projects[name] 1856 sync_s=sync_s,
1368 1857 sync_tags=sync_tags,
1369 # If the manifest removes the hooks project, treat it as if it deleted 1858 clone_depth=clone_depth,
1370 # the repo-hooks element too. 1859 upstream=upstream,
1371 if repo_hooks_project == name: 1860 parent=parent,
1372 repo_hooks_project = None 1861 dest_branch=dest_branch,
1373 elif not XmlBool(node, 'optional', False): 1862 use_git_worktrees=use_git_worktrees,
1374 raise ManifestParseError('remove-project element specifies non-existent ' 1863 **extra_proj_attrs,
1375 'project: %s' % name) 1864 )
1376 1865
1377 # Store repo hooks project information. 1866 for n in node.childNodes:
1378 if repo_hooks_project: 1867 if n.nodeName == "copyfile":
1379 # Store a reference to the Project. 1868 self._ParseCopyFile(project, n)
1380 try: 1869 if n.nodeName == "linkfile":
1381 repo_hooks_projects = self._projects[repo_hooks_project] 1870 self._ParseLinkFile(project, n)
1382 except KeyError: 1871 if n.nodeName == "annotation":
1383 raise ManifestParseError( 1872 self._ParseAnnotation(project, n)
1384 'project %s not found for repo-hooks' % 1873 if n.nodeName == "project":
1385 (repo_hooks_project)) 1874 project.subprojects.append(
1386 1875 self._ParseProject(n, parent=project)
1387 if len(repo_hooks_projects) != 1: 1876 )
1388 raise ManifestParseError( 1877
1389 'internal error parsing repo-hooks in %s' % 1878 return project
1390 (self.manifestFile)) 1879
1391 self._repo_hooks_project = repo_hooks_projects[0] 1880 def GetProjectPaths(self, name, path, remote):
1392 # Store the enabled hooks in the Project object. 1881 """Return the paths for a project.
1393 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks 1882
1394 1883 Args:
1395 def _AddMetaProjectMirror(self, m): 1884 name: a string, the name of the project.
1396 name = None 1885 path: a string, the path of the project.
1397 m_url = m.GetRemote().url 1886 remote: a string, the remote.name of the project.
1398 if m_url.endswith('/.git'): 1887
1399 raise ManifestParseError('refusing to mirror %s' % m_url) 1888 Returns:
1400 1889 A tuple of (relpath, worktree, gitdir, objdir, use_git_worktrees)
1401 if self._default and self._default.remote: 1890 for the project with |name| and |path|.
1402 url = self._default.remote.resolvedFetchUrl 1891 """
1403 if not url.endswith('/'): 1892 # The manifest entries might have trailing slashes. Normalize them to
1404 url += '/' 1893 # avoid unexpected filesystem behavior since we do string concatenation
1405 if m_url.startswith(url): 1894 # below.
1406 remote = self._default.remote 1895 path = path.rstrip("/")
1407 name = m_url[len(url):] 1896 name = name.rstrip("/")
1408 1897 remote = remote.rstrip("/")
1409 if name is None: 1898 use_git_worktrees = False
1410 s = m_url.rindex('/') + 1 1899 use_remote_name = self.is_multimanifest
1411 manifestUrl = self.manifestProject.config.GetString('remote.origin.url') 1900 relpath = path
1412 remote = _XmlRemote('origin', fetch=m_url[:s], manifestUrl=manifestUrl) 1901 if self.IsMirror:
1413 name = m_url[s:] 1902 worktree = None
1414 1903 gitdir = os.path.join(self.topdir, "%s.git" % name)
1415 if name.endswith('.git'): 1904 objdir = gitdir
1416 name = name[:-4] 1905 else:
1417 1906 if use_remote_name:
1418 if name not in self._projects: 1907 namepath = os.path.join(remote, f"{name}.git")
1419 m.PreSync() 1908 else:
1420 gitdir = os.path.join(self.topdir, '%s.git' % name) 1909 namepath = f"{name}.git"
1421 project = Project(manifest=self, 1910 worktree = os.path.join(self.topdir, path).replace("\\", "/")
1422 name=name, 1911 gitdir = os.path.join(self.subdir, "projects", "%s.git" % path)
1423 remote=remote.ToRemoteSpec(name), 1912 # We allow people to mix git worktrees & non-git worktrees for now.
1424 gitdir=gitdir, 1913 # This allows for in situ migration of repo clients.
1425 objdir=gitdir, 1914 if os.path.exists(gitdir) or not self.UseGitWorktrees:
1426 worktree=None, 1915 objdir = os.path.join(self.repodir, "project-objects", namepath)
1427 relpath=name or None, 1916 else:
1428 revisionExpr=m.revisionExpr, 1917 use_git_worktrees = True
1429 revisionId=None) 1918 gitdir = os.path.join(self.repodir, "worktrees", namepath)
1430 self._projects[project.name] = [project] 1919 objdir = gitdir
1431 self._paths[project.relpath] = project 1920 return relpath, worktree, gitdir, objdir, use_git_worktrees
1432 1921
1433 def _ParseRemote(self, node): 1922 def GetProjectsWithName(self, name, all_manifests=False):
1434 """ 1923 """All projects with |name|.
1435 reads a <remote> element from the manifest file 1924
1436 """ 1925 Args:
1437 name = self._reqatt(node, 'name') 1926 name: a string, the name of the project.
1438 alias = node.getAttribute('alias') 1927 all_manifests: a boolean, if True, then all manifests are searched.
1439 if alias == '': 1928 If False, then only this manifest is searched.
1440 alias = None 1929
1441 fetch = self._reqatt(node, 'fetch') 1930 Returns:
1442 pushUrl = node.getAttribute('pushurl') 1931 A list of Project instances with name |name|.
1443 if pushUrl == '': 1932 """
1444 pushUrl = None 1933 if all_manifests:
1445 review = node.getAttribute('review') 1934 return list(
1446 if review == '': 1935 itertools.chain.from_iterable(
1447 review = None 1936 x._projects.get(name, []) for x in self.all_manifests
1448 revision = node.getAttribute('revision') 1937 )
1449 if revision == '': 1938 )
1450 revision = None 1939 return self._projects.get(name, [])
1451 manifestUrl = self.manifestProject.config.GetString('remote.origin.url') 1940
1452 1941 def GetSubprojectName(self, parent, submodule_path):
1453 remote = _XmlRemote(name, alias, fetch, pushUrl, manifestUrl, review, revision) 1942 return os.path.join(parent.name, submodule_path)
1454 1943
1455 for n in node.childNodes: 1944 def _JoinRelpath(self, parent_relpath, relpath):
1456 if n.nodeName == 'annotation': 1945 return os.path.join(parent_relpath, relpath)
1457 self._ParseAnnotation(remote, n) 1946
1458 1947 def _UnjoinRelpath(self, parent_relpath, relpath):
1459 return remote 1948 return os.path.relpath(relpath, parent_relpath)
1460 1949
1461 def _ParseDefault(self, node): 1950 def GetSubprojectPaths(self, parent, name, path):
1462 """ 1951 # The manifest entries might have trailing slashes. Normalize them to
1463 reads a <default> element from the manifest file 1952 # avoid unexpected filesystem behavior since we do string concatenation
1464 """ 1953 # below.
1465 d = _Default() 1954 path = path.rstrip("/")
1466 d.remote = self._get_remote(node) 1955 name = name.rstrip("/")
1467 d.revisionExpr = node.getAttribute('revision') 1956 relpath = self._JoinRelpath(parent.relpath, path)
1468 if d.revisionExpr == '': 1957 gitdir = os.path.join(parent.gitdir, "subprojects", "%s.git" % path)
1469 d.revisionExpr = None 1958 objdir = os.path.join(
1470 1959 parent.gitdir, "subproject-objects", "%s.git" % name
1471 d.destBranchExpr = node.getAttribute('dest-branch') or None 1960 )
1472 d.upstreamExpr = node.getAttribute('upstream') or None 1961 if self.IsMirror:
1473 1962 worktree = None
1474 d.sync_j = XmlInt(node, 'sync-j', None) 1963 else:
1475 if d.sync_j is not None and d.sync_j <= 0: 1964 worktree = os.path.join(parent.worktree, path).replace("\\", "/")
1476 raise ManifestParseError('%s: sync-j must be greater than 0, not "%s"' % 1965 return relpath, worktree, gitdir, objdir
1477 (self.manifestFile, d.sync_j)) 1966
1478 1967 @staticmethod
1479 d.sync_c = XmlBool(node, 'sync-c', False) 1968 def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False):
1480 d.sync_s = XmlBool(node, 'sync-s', False) 1969 """Verify |path| is reasonable for use in filesystem paths.
1481 d.sync_tags = XmlBool(node, 'sync-tags', True) 1970
1482 return d 1971 Used with <copyfile> & <linkfile> & <project> elements.
1483 1972
1484 def _ParseNotice(self, node): 1973 This only validates the |path| in isolation: it does not check against
1485 """ 1974 the current filesystem state. Thus it is suitable as a first-past in a
1486 reads a <notice> element from the manifest file 1975 parser.
1487 1976
1488 The <notice> element is distinct from other tags in the XML in that the 1977 It enforces a number of constraints:
1489 data is conveyed between the start and end tag (it's not an empty-element 1978 * No empty paths.
1490 tag). 1979 * No "~" in paths.
1491 1980 * No Unicode codepoints that filesystems might elide when normalizing.
1492 The white space (carriage returns, indentation) for the notice element is 1981 * No relative path components like "." or "..".
1493 relevant and is parsed in a way that is based on how python docstrings work. 1982 * No absolute paths.
1494 In fact, the code is remarkably similar to here: 1983 * No ".git" or ".repo*" path components.
1495 http://www.python.org/dev/peps/pep-0257/ 1984
1496 """ 1985 Args:
1497 # Get the data out of the node... 1986 path: The path name to validate.
1498 notice = node.childNodes[0].data 1987 dir_ok: Whether |path| may force a directory (e.g. end in a /).
1499 1988 cwd_dot_ok: Whether |path| may be just ".".
1500 # Figure out minimum indentation, skipping the first line (the same line 1989
1501 # as the <notice> tag)... 1990 Returns:
1502 minIndent = sys.maxsize 1991 None if |path| is OK, a failure message otherwise.
1503 lines = notice.splitlines() 1992 """
1504 for line in lines[1:]: 1993 if not path:
1505 lstrippedLine = line.lstrip() 1994 return "empty paths not allowed"
1506 if lstrippedLine: 1995
1507 indent = len(line) - len(lstrippedLine) 1996 if "~" in path:
1508 minIndent = min(indent, minIndent) 1997 return "~ not allowed (due to 8.3 filenames on Windows filesystems)"
1509 1998
1510 # Strip leading / trailing blank lines and also indentation. 1999 path_codepoints = set(path)
1511 cleanLines = [lines[0].strip()] 2000
1512 for line in lines[1:]: 2001 # Some filesystems (like Apple's HFS+) try to normalize Unicode
1513 cleanLines.append(line[minIndent:].rstrip()) 2002 # codepoints which means there are alternative names for ".git". Reject
1514 2003 # paths with these in it as there shouldn't be any reasonable need for
1515 # Clear completely blank lines from front and back... 2004 # them here. The set of codepoints here was cribbed from jgit's
1516 while cleanLines and not cleanLines[0]: 2005 # implementation:
1517 del cleanLines[0] 2006 # https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
1518 while cleanLines and not cleanLines[-1]: 2007 BAD_CODEPOINTS = {
1519 del cleanLines[-1] 2008 "\u200C", # ZERO WIDTH NON-JOINER
1520 2009 "\u200D", # ZERO WIDTH JOINER
1521 return '\n'.join(cleanLines) 2010 "\u200E", # LEFT-TO-RIGHT MARK
1522 2011 "\u200F", # RIGHT-TO-LEFT MARK
1523 def _ParseSubmanifest(self, node): 2012 "\u202A", # LEFT-TO-RIGHT EMBEDDING
1524 """Reads a <submanifest> element from the manifest file.""" 2013 "\u202B", # RIGHT-TO-LEFT EMBEDDING
1525 name = self._reqatt(node, 'name') 2014 "\u202C", # POP DIRECTIONAL FORMATTING
1526 remote = node.getAttribute('remote') 2015 "\u202D", # LEFT-TO-RIGHT OVERRIDE
1527 if remote == '': 2016 "\u202E", # RIGHT-TO-LEFT OVERRIDE
1528 remote = None 2017 "\u206A", # INHIBIT SYMMETRIC SWAPPING
1529 project = node.getAttribute('project') 2018 "\u206B", # ACTIVATE SYMMETRIC SWAPPING
1530 if project == '': 2019 "\u206C", # INHIBIT ARABIC FORM SHAPING
1531 project = None 2020 "\u206D", # ACTIVATE ARABIC FORM SHAPING
1532 revision = node.getAttribute('revision') 2021 "\u206E", # NATIONAL DIGIT SHAPES
1533 if revision == '': 2022 "\u206F", # NOMINAL DIGIT SHAPES
1534 revision = None 2023 "\uFEFF", # ZERO WIDTH NO-BREAK SPACE
1535 manifestName = node.getAttribute('manifest-name') 2024 }
1536 if manifestName == '': 2025 if BAD_CODEPOINTS & path_codepoints:
1537 manifestName = None 2026 # This message is more expansive than reality, but should be fine.
1538 groups = '' 2027 return "Unicode combining characters not allowed"
1539 if node.hasAttribute('groups'): 2028
1540 groups = node.getAttribute('groups') 2029 # Reject newlines as there shouldn't be any legitmate use for them,
1541 groups = self._ParseList(groups) 2030 # they'll be confusing to users, and they can easily break tools that
1542 default_groups = self._ParseList(node.getAttribute('default-groups')) 2031 # expect to be able to iterate over newline delimited lists. This even
1543 path = node.getAttribute('path') 2032 # applies to our own code like .repo/project.list.
1544 if path == '': 2033 if {"\r", "\n"} & path_codepoints:
1545 path = None 2034 return "Newlines not allowed"
1546 if revision: 2035
1547 msg = self._CheckLocalPath(revision.split('/')[-1]) 2036 # Assume paths might be used on case-insensitive filesystems.
2037 path = path.lower()
2038
2039 # Split up the path by its components. We can't use os.path.sep
2040 # exclusively as some platforms (like Windows) will convert / to \ and
2041 # that bypasses all our constructed logic here. Especially since
2042 # manifest authors only use / in their paths.
2043 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
2044 # Strip off trailing slashes as those only produce '' elements, and we
2045 # use parts to look for individual bad components.
2046 parts = resep.split(path.rstrip("/"))
2047
2048 # Some people use src="." to create stable links to projects. Lets
2049 # allow that but reject all other uses of "." to keep things simple.
2050 if not cwd_dot_ok or parts != ["."]:
2051 for part in set(parts):
2052 if part in {".", "..", ".git"} or part.startswith(".repo"):
2053 return "bad component: %s" % (part,)
2054
2055 if not dir_ok and resep.match(path[-1]):
2056 return "dirs not allowed"
2057
2058 # NB: The two abspath checks here are to handle platforms with multiple
2059 # filesystem path styles (e.g. Windows).
2060 norm = os.path.normpath(path)
2061 if (
2062 norm == ".."
2063 or (
2064 len(norm) >= 3
2065 and norm.startswith("..")
2066 and resep.match(norm[0])
2067 )
2068 or os.path.isabs(norm)
2069 or norm.startswith("/")
2070 ):
2071 return "path cannot be outside"
2072
2073 @classmethod
2074 def _ValidateFilePaths(cls, element, src, dest):
2075 """Verify |src| & |dest| are reasonable for <copyfile> & <linkfile>.
2076
2077 We verify the path independent of any filesystem state as we won't have
2078 a checkout available to compare to. i.e. This is for parsing validation
2079 purposes only.
2080
2081 We'll do full/live sanity checking before we do the actual filesystem
2082 modifications in _CopyFile/_LinkFile/etc...
2083 """
2084 # |dest| is the file we write to or symlink we create.
2085 # It is relative to the top of the repo client checkout.
2086 msg = cls._CheckLocalPath(dest)
1548 if msg: 2087 if msg:
1549 raise ManifestInvalidPathError( 2088 raise ManifestInvalidPathError(
1550 '<submanifest> invalid "revision": %s: %s' % (revision, msg)) 2089 '<%s> invalid "dest": %s: %s' % (element, dest, msg)
1551 else: 2090 )
1552 msg = self._CheckLocalPath(name) 2091
2092 # |src| is the file we read from or path we point to for symlinks.
2093 # It is relative to the top of the git project checkout.
2094 is_linkfile = element == "linkfile"
2095 msg = cls._CheckLocalPath(
2096 src, dir_ok=is_linkfile, cwd_dot_ok=is_linkfile
2097 )
1553 if msg: 2098 if msg:
1554 raise ManifestInvalidPathError( 2099 raise ManifestInvalidPathError(
1555 '<submanifest> invalid "name": %s: %s' % (name, msg)) 2100 '<%s> invalid "src": %s: %s' % (element, src, msg)
1556 else: 2101 )
1557 msg = self._CheckLocalPath(path) 2102
1558 if msg: 2103 def _ParseCopyFile(self, project, node):
1559 raise ManifestInvalidPathError( 2104 src = self._reqatt(node, "src")
1560 '<submanifest> invalid "path": %s: %s' % (path, msg)) 2105 dest = self._reqatt(node, "dest")
1561 2106 if not self.IsMirror:
1562 submanifest = _XmlSubmanifest(name, remote, project, revision, manifestName, 2107 # src is project relative;
1563 groups, default_groups, path, self) 2108 # dest is relative to the top of the tree.
1564 2109 # We only validate paths if we actually plan to process them.
1565 for n in node.childNodes: 2110 self._ValidateFilePaths("copyfile", src, dest)
1566 if n.nodeName == 'annotation': 2111 project.AddCopyFile(src, dest, self.topdir)
1567 self._ParseAnnotation(submanifest, n) 2112
1568 2113 def _ParseLinkFile(self, project, node):
1569 return submanifest 2114 src = self._reqatt(node, "src")
1570 2115 dest = self._reqatt(node, "dest")
1571 def _JoinName(self, parent_name, name): 2116 if not self.IsMirror:
1572 return os.path.join(parent_name, name) 2117 # src is project relative;
1573 2118 # dest is relative to the top of the tree.
1574 def _UnjoinName(self, parent_name, name): 2119 # We only validate paths if we actually plan to process them.
1575 return os.path.relpath(name, parent_name) 2120 self._ValidateFilePaths("linkfile", src, dest)
1576 2121 project.AddLinkFile(src, dest, self.topdir)
1577 def _ParseProject(self, node, parent=None, **extra_proj_attrs): 2122
1578 """ 2123 def _ParseAnnotation(self, element, node):
1579 reads a <project> element from the manifest file 2124 name = self._reqatt(node, "name")
1580 """ 2125 value = self._reqatt(node, "value")
1581 name = self._reqatt(node, 'name')
1582 msg = self._CheckLocalPath(name, dir_ok=True)
1583 if msg:
1584 raise ManifestInvalidPathError(
1585 '<project> invalid "name": %s: %s' % (name, msg))
1586 if parent:
1587 name = self._JoinName(parent.name, name)
1588
1589 remote = self._get_remote(node)
1590 if remote is None:
1591 remote = self._default.remote
1592 if remote is None:
1593 raise ManifestParseError("no remote for project %s within %s" %
1594 (name, self.manifestFile))
1595
1596 revisionExpr = node.getAttribute('revision') or remote.revision
1597 if not revisionExpr:
1598 revisionExpr = self._default.revisionExpr
1599 if not revisionExpr:
1600 raise ManifestParseError("no revision for project %s within %s" %
1601 (name, self.manifestFile))
1602
1603 path = node.getAttribute('path')
1604 if not path:
1605 path = name
1606 else:
1607 # NB: The "." project is handled specially in Project.Sync_LocalHalf.
1608 msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
1609 if msg:
1610 raise ManifestInvalidPathError(
1611 '<project> invalid "path": %s: %s' % (path, msg))
1612
1613 rebase = XmlBool(node, 'rebase', True)
1614 sync_c = XmlBool(node, 'sync-c', False)
1615 sync_s = XmlBool(node, 'sync-s', self._default.sync_s)
1616 sync_tags = XmlBool(node, 'sync-tags', self._default.sync_tags)
1617
1618 clone_depth = XmlInt(node, 'clone-depth')
1619 if clone_depth is not None and clone_depth <= 0:
1620 raise ManifestParseError('%s: clone-depth must be greater than 0, not "%s"' %
1621 (self.manifestFile, clone_depth))
1622
1623 dest_branch = node.getAttribute('dest-branch') or self._default.destBranchExpr
1624
1625 upstream = node.getAttribute('upstream') or self._default.upstreamExpr
1626
1627 groups = ''
1628 if node.hasAttribute('groups'):
1629 groups = node.getAttribute('groups')
1630 groups = self._ParseList(groups)
1631
1632 if parent is None:
1633 relpath, worktree, gitdir, objdir, use_git_worktrees = \
1634 self.GetProjectPaths(name, path, remote.name)
1635 else:
1636 use_git_worktrees = False
1637 relpath, worktree, gitdir, objdir = \
1638 self.GetSubprojectPaths(parent, name, path)
1639
1640 default_groups = ['all', 'name:%s' % name, 'path:%s' % relpath]
1641 groups.extend(set(default_groups).difference(groups))
1642
1643 if self.IsMirror and node.hasAttribute('force-path'):
1644 if XmlBool(node, 'force-path', False):
1645 gitdir = os.path.join(self.topdir, '%s.git' % path)
1646
1647 project = Project(manifest=self,
1648 name=name,
1649 remote=remote.ToRemoteSpec(name),
1650 gitdir=gitdir,
1651 objdir=objdir,
1652 worktree=worktree,
1653 relpath=relpath,
1654 revisionExpr=revisionExpr,
1655 revisionId=None,
1656 rebase=rebase,
1657 groups=groups,
1658 sync_c=sync_c,
1659 sync_s=sync_s,
1660 sync_tags=sync_tags,
1661 clone_depth=clone_depth,
1662 upstream=upstream,
1663 parent=parent,
1664 dest_branch=dest_branch,
1665 use_git_worktrees=use_git_worktrees,
1666 **extra_proj_attrs)
1667
1668 for n in node.childNodes:
1669 if n.nodeName == 'copyfile':
1670 self._ParseCopyFile(project, n)
1671 if n.nodeName == 'linkfile':
1672 self._ParseLinkFile(project, n)
1673 if n.nodeName == 'annotation':
1674 self._ParseAnnotation(project, n)
1675 if n.nodeName == 'project':
1676 project.subprojects.append(self._ParseProject(n, parent=project))
1677
1678 return project
1679
1680 def GetProjectPaths(self, name, path, remote):
1681 """Return the paths for a project.
1682
1683 Args:
1684 name: a string, the name of the project.
1685 path: a string, the path of the project.
1686 remote: a string, the remote.name of the project.
1687
1688 Returns:
1689 A tuple of (relpath, worktree, gitdir, objdir, use_git_worktrees) for the
1690 project with |name| and |path|.
1691 """
1692 # The manifest entries might have trailing slashes. Normalize them to avoid
1693 # unexpected filesystem behavior since we do string concatenation below.
1694 path = path.rstrip('/')
1695 name = name.rstrip('/')
1696 remote = remote.rstrip('/')
1697 use_git_worktrees = False
1698 use_remote_name = self.is_multimanifest
1699 relpath = path
1700 if self.IsMirror:
1701 worktree = None
1702 gitdir = os.path.join(self.topdir, '%s.git' % name)
1703 objdir = gitdir
1704 else:
1705 if use_remote_name:
1706 namepath = os.path.join(remote, f'{name}.git')
1707 else:
1708 namepath = f'{name}.git'
1709 worktree = os.path.join(self.topdir, path).replace('\\', '/')
1710 gitdir = os.path.join(self.subdir, 'projects', '%s.git' % path)
1711 # We allow people to mix git worktrees & non-git worktrees for now.
1712 # This allows for in situ migration of repo clients.
1713 if os.path.exists(gitdir) or not self.UseGitWorktrees:
1714 objdir = os.path.join(self.repodir, 'project-objects', namepath)
1715 else:
1716 use_git_worktrees = True
1717 gitdir = os.path.join(self.repodir, 'worktrees', namepath)
1718 objdir = gitdir
1719 return relpath, worktree, gitdir, objdir, use_git_worktrees
1720
1721 def GetProjectsWithName(self, name, all_manifests=False):
1722 """All projects with |name|.
1723
1724 Args:
1725 name: a string, the name of the project.
1726 all_manifests: a boolean, if True, then all manifests are searched. If
1727 False, then only this manifest is searched.
1728
1729 Returns:
1730 A list of Project instances with name |name|.
1731 """
1732 if all_manifests:
1733 return list(itertools.chain.from_iterable(
1734 x._projects.get(name, []) for x in self.all_manifests))
1735 return self._projects.get(name, [])
1736
1737 def GetSubprojectName(self, parent, submodule_path):
1738 return os.path.join(parent.name, submodule_path)
1739
1740 def _JoinRelpath(self, parent_relpath, relpath):
1741 return os.path.join(parent_relpath, relpath)
1742
1743 def _UnjoinRelpath(self, parent_relpath, relpath):
1744 return os.path.relpath(relpath, parent_relpath)
1745
1746 def GetSubprojectPaths(self, parent, name, path):
1747 # The manifest entries might have trailing slashes. Normalize them to avoid
1748 # unexpected filesystem behavior since we do string concatenation below.
1749 path = path.rstrip('/')
1750 name = name.rstrip('/')
1751 relpath = self._JoinRelpath(parent.relpath, path)
1752 gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path)
1753 objdir = os.path.join(parent.gitdir, 'subproject-objects', '%s.git' % name)
1754 if self.IsMirror:
1755 worktree = None
1756 else:
1757 worktree = os.path.join(parent.worktree, path).replace('\\', '/')
1758 return relpath, worktree, gitdir, objdir
1759
1760 @staticmethod
1761 def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False):
1762 """Verify |path| is reasonable for use in filesystem paths.
1763
1764 Used with <copyfile> & <linkfile> & <project> elements.
1765
1766 This only validates the |path| in isolation: it does not check against the
1767 current filesystem state. Thus it is suitable as a first-past in a parser.
1768
1769 It enforces a number of constraints:
1770 * No empty paths.
1771 * No "~" in paths.
1772 * No Unicode codepoints that filesystems might elide when normalizing.
1773 * No relative path components like "." or "..".
1774 * No absolute paths.
1775 * No ".git" or ".repo*" path components.
1776
1777 Args:
1778 path: The path name to validate.
1779 dir_ok: Whether |path| may force a directory (e.g. end in a /).
1780 cwd_dot_ok: Whether |path| may be just ".".
1781
1782 Returns:
1783 None if |path| is OK, a failure message otherwise.
1784 """
1785 if not path:
1786 return 'empty paths not allowed'
1787
1788 if '~' in path:
1789 return '~ not allowed (due to 8.3 filenames on Windows filesystems)'
1790
1791 path_codepoints = set(path)
1792
1793 # Some filesystems (like Apple's HFS+) try to normalize Unicode codepoints
1794 # which means there are alternative names for ".git". Reject paths with
1795 # these in it as there shouldn't be any reasonable need for them here.
1796 # The set of codepoints here was cribbed from jgit's implementation:
1797 # https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
1798 BAD_CODEPOINTS = {
1799 u'\u200C', # ZERO WIDTH NON-JOINER
1800 u'\u200D', # ZERO WIDTH JOINER
1801 u'\u200E', # LEFT-TO-RIGHT MARK
1802 u'\u200F', # RIGHT-TO-LEFT MARK
1803 u'\u202A', # LEFT-TO-RIGHT EMBEDDING
1804 u'\u202B', # RIGHT-TO-LEFT EMBEDDING
1805 u'\u202C', # POP DIRECTIONAL FORMATTING
1806 u'\u202D', # LEFT-TO-RIGHT OVERRIDE
1807 u'\u202E', # RIGHT-TO-LEFT OVERRIDE
1808 u'\u206A', # INHIBIT SYMMETRIC SWAPPING
1809 u'\u206B', # ACTIVATE SYMMETRIC SWAPPING
1810 u'\u206C', # INHIBIT ARABIC FORM SHAPING
1811 u'\u206D', # ACTIVATE ARABIC FORM SHAPING
1812 u'\u206E', # NATIONAL DIGIT SHAPES
1813 u'\u206F', # NOMINAL DIGIT SHAPES
1814 u'\uFEFF', # ZERO WIDTH NO-BREAK SPACE
1815 }
1816 if BAD_CODEPOINTS & path_codepoints:
1817 # This message is more expansive than reality, but should be fine.
1818 return 'Unicode combining characters not allowed'
1819
1820 # Reject newlines as there shouldn't be any legitmate use for them, they'll
1821 # be confusing to users, and they can easily break tools that expect to be
1822 # able to iterate over newline delimited lists. This even applies to our
1823 # own code like .repo/project.list.
1824 if {'\r', '\n'} & path_codepoints:
1825 return 'Newlines not allowed'
1826
1827 # Assume paths might be used on case-insensitive filesystems.
1828 path = path.lower()
1829
1830 # Split up the path by its components. We can't use os.path.sep exclusively
1831 # as some platforms (like Windows) will convert / to \ and that bypasses all
1832 # our constructed logic here. Especially since manifest authors only use
1833 # / in their paths.
1834 resep = re.compile(r'[/%s]' % re.escape(os.path.sep))
1835 # Strip off trailing slashes as those only produce '' elements, and we use
1836 # parts to look for individual bad components.
1837 parts = resep.split(path.rstrip('/'))
1838
1839 # Some people use src="." to create stable links to projects. Lets allow
1840 # that but reject all other uses of "." to keep things simple.
1841 if not cwd_dot_ok or parts != ['.']:
1842 for part in set(parts):
1843 if part in {'.', '..', '.git'} or part.startswith('.repo'):
1844 return 'bad component: %s' % (part,)
1845
1846 if not dir_ok and resep.match(path[-1]):
1847 return 'dirs not allowed'
1848
1849 # NB: The two abspath checks here are to handle platforms with multiple
1850 # filesystem path styles (e.g. Windows).
1851 norm = os.path.normpath(path)
1852 if (norm == '..' or
1853 (len(norm) >= 3 and norm.startswith('..') and resep.match(norm[0])) or
1854 os.path.isabs(norm) or
1855 norm.startswith('/')):
1856 return 'path cannot be outside'
1857
1858 @classmethod
1859 def _ValidateFilePaths(cls, element, src, dest):
1860 """Verify |src| & |dest| are reasonable for <copyfile> & <linkfile>.
1861
1862 We verify the path independent of any filesystem state as we won't have a
1863 checkout available to compare to. i.e. This is for parsing validation
1864 purposes only.
1865
1866 We'll do full/live sanity checking before we do the actual filesystem
1867 modifications in _CopyFile/_LinkFile/etc...
1868 """
1869 # |dest| is the file we write to or symlink we create.
1870 # It is relative to the top of the repo client checkout.
1871 msg = cls._CheckLocalPath(dest)
1872 if msg:
1873 raise ManifestInvalidPathError(
1874 '<%s> invalid "dest": %s: %s' % (element, dest, msg))
1875
1876 # |src| is the file we read from or path we point to for symlinks.
1877 # It is relative to the top of the git project checkout.
1878 is_linkfile = element == 'linkfile'
1879 msg = cls._CheckLocalPath(src, dir_ok=is_linkfile, cwd_dot_ok=is_linkfile)
1880 if msg:
1881 raise ManifestInvalidPathError(
1882 '<%s> invalid "src": %s: %s' % (element, src, msg))
1883
1884 def _ParseCopyFile(self, project, node):
1885 src = self._reqatt(node, 'src')
1886 dest = self._reqatt(node, 'dest')
1887 if not self.IsMirror:
1888 # src is project relative;
1889 # dest is relative to the top of the tree.
1890 # We only validate paths if we actually plan to process them.
1891 self._ValidateFilePaths('copyfile', src, dest)
1892 project.AddCopyFile(src, dest, self.topdir)
1893
1894 def _ParseLinkFile(self, project, node):
1895 src = self._reqatt(node, 'src')
1896 dest = self._reqatt(node, 'dest')
1897 if not self.IsMirror:
1898 # src is project relative;
1899 # dest is relative to the top of the tree.
1900 # We only validate paths if we actually plan to process them.
1901 self._ValidateFilePaths('linkfile', src, dest)
1902 project.AddLinkFile(src, dest, self.topdir)
1903
1904 def _ParseAnnotation(self, element, node):
1905 name = self._reqatt(node, 'name')
1906 value = self._reqatt(node, 'value')
1907 try:
1908 keep = self._reqatt(node, 'keep').lower()
1909 except ManifestParseError:
1910 keep = "true"
1911 if keep != "true" and keep != "false":
1912 raise ManifestParseError('optional "keep" attribute must be '
1913 '"true" or "false"')
1914 element.AddAnnotation(name, value, keep)
1915
1916 def _get_remote(self, node):
1917 name = node.getAttribute('remote')
1918 if not name:
1919 return None
1920
1921 v = self._remotes.get(name)
1922 if not v:
1923 raise ManifestParseError("remote %s not defined in %s" %
1924 (name, self.manifestFile))
1925 return v
1926
1927 def _reqatt(self, node, attname):
1928 """
1929 reads a required attribute from the node.
1930 """
1931 v = node.getAttribute(attname)
1932 if not v:
1933 raise ManifestParseError("no %s in <%s> within %s" %
1934 (attname, node.nodeName, self.manifestFile))
1935 return v
1936
1937 def projectsDiff(self, manifest):
1938 """return the projects differences between two manifests.
1939
1940 The diff will be from self to given manifest.
1941
1942 """
1943 fromProjects = self.paths
1944 toProjects = manifest.paths
1945
1946 fromKeys = sorted(fromProjects.keys())
1947 toKeys = sorted(toProjects.keys())
1948
1949 diff = {'added': [], 'removed': [], 'missing': [], 'changed': [], 'unreachable': []}
1950
1951 for proj in fromKeys:
1952 if proj not in toKeys:
1953 diff['removed'].append(fromProjects[proj])
1954 elif not fromProjects[proj].Exists:
1955 diff['missing'].append(toProjects[proj])
1956 toKeys.remove(proj)
1957 else:
1958 fromProj = fromProjects[proj]
1959 toProj = toProjects[proj]
1960 try: 2126 try:
1961 fromRevId = fromProj.GetCommitRevisionId() 2127 keep = self._reqatt(node, "keep").lower()
1962 toRevId = toProj.GetCommitRevisionId() 2128 except ManifestParseError:
1963 except ManifestInvalidRevisionError: 2129 keep = "true"
1964 diff['unreachable'].append((fromProj, toProj)) 2130 if keep != "true" and keep != "false":
1965 else: 2131 raise ManifestParseError(
1966 if fromRevId != toRevId: 2132 'optional "keep" attribute must be ' '"true" or "false"'
1967 diff['changed'].append((fromProj, toProj)) 2133 )
1968 toKeys.remove(proj) 2134 element.AddAnnotation(name, value, keep)
1969 2135
1970 for proj in toKeys: 2136 def _get_remote(self, node):
1971 diff['added'].append(toProjects[proj]) 2137 name = node.getAttribute("remote")
1972 2138 if not name:
1973 return diff 2139 return None
2140
2141 v = self._remotes.get(name)
2142 if not v:
2143 raise ManifestParseError(
2144 "remote %s not defined in %s" % (name, self.manifestFile)
2145 )
2146 return v
2147
2148 def _reqatt(self, node, attname):
2149 """
2150 reads a required attribute from the node.
2151 """
2152 v = node.getAttribute(attname)
2153 if not v:
2154 raise ManifestParseError(
2155 "no %s in <%s> within %s"
2156 % (attname, node.nodeName, self.manifestFile)
2157 )
2158 return v
2159
2160 def projectsDiff(self, manifest):
2161 """return the projects differences between two manifests.
2162
2163 The diff will be from self to given manifest.
2164
2165 """
2166 fromProjects = self.paths
2167 toProjects = manifest.paths
2168
2169 fromKeys = sorted(fromProjects.keys())
2170 toKeys = sorted(toProjects.keys())
2171
2172 diff = {
2173 "added": [],
2174 "removed": [],
2175 "missing": [],
2176 "changed": [],
2177 "unreachable": [],
2178 }
2179
2180 for proj in fromKeys:
2181 if proj not in toKeys:
2182 diff["removed"].append(fromProjects[proj])
2183 elif not fromProjects[proj].Exists:
2184 diff["missing"].append(toProjects[proj])
2185 toKeys.remove(proj)
2186 else:
2187 fromProj = fromProjects[proj]
2188 toProj = toProjects[proj]
2189 try:
2190 fromRevId = fromProj.GetCommitRevisionId()
2191 toRevId = toProj.GetCommitRevisionId()
2192 except ManifestInvalidRevisionError:
2193 diff["unreachable"].append((fromProj, toProj))
2194 else:
2195 if fromRevId != toRevId:
2196 diff["changed"].append((fromProj, toProj))
2197 toKeys.remove(proj)
2198
2199 for proj in toKeys:
2200 diff["added"].append(toProjects[proj])
2201
2202 return diff
1974 2203
1975 2204
1976class GitcManifest(XmlManifest): 2205class GitcManifest(XmlManifest):
1977 """Parser for GitC (git-in-the-cloud) manifests.""" 2206 """Parser for GitC (git-in-the-cloud) manifests."""
1978 2207
1979 def _ParseProject(self, node, parent=None): 2208 def _ParseProject(self, node, parent=None):
1980 """Override _ParseProject and add support for GITC specific attributes.""" 2209 """Override _ParseProject and add support for GITC specific attributes.""" # noqa: E501
1981 return super()._ParseProject( 2210 return super()._ParseProject(
1982 node, parent=parent, old_revision=node.getAttribute('old-revision')) 2211 node, parent=parent, old_revision=node.getAttribute("old-revision")
2212 )
1983 2213
1984 def _output_manifest_project_extras(self, p, e): 2214 def _output_manifest_project_extras(self, p, e):
1985 """Output GITC Specific Project attributes""" 2215 """Output GITC Specific Project attributes"""
1986 if p.old_revision: 2216 if p.old_revision:
1987 e.setAttribute('old-revision', str(p.old_revision)) 2217 e.setAttribute("old-revision", str(p.old_revision))
1988 2218
1989 2219
1990class RepoClient(XmlManifest): 2220class RepoClient(XmlManifest):
1991 """Manages a repo client checkout.""" 2221 """Manages a repo client checkout."""
1992 2222
1993 def __init__(self, repodir, manifest_file=None, submanifest_path='', **kwargs): 2223 def __init__(
1994 """Initialize. 2224 self, repodir, manifest_file=None, submanifest_path="", **kwargs
1995 2225 ):
1996 Args: 2226 """Initialize.
1997 repodir: Path to the .repo/ dir for holding all internal checkout state. 2227
1998 It must be in the top directory of the repo client checkout. 2228 Args:
1999 manifest_file: Full path to the manifest file to parse. This will usually 2229 repodir: Path to the .repo/ dir for holding all internal checkout
2000 be |repodir|/|MANIFEST_FILE_NAME|. 2230 state. It must be in the top directory of the repo client
2001 submanifest_path: The submanifest root relative to the repo root. 2231 checkout.
2002 **kwargs: Additional keyword arguments, passed to XmlManifest. 2232 manifest_file: Full path to the manifest file to parse. This will
2003 """ 2233 usually be |repodir|/|MANIFEST_FILE_NAME|.
2004 self.isGitcClient = False 2234 submanifest_path: The submanifest root relative to the repo root.
2005 submanifest_path = submanifest_path or '' 2235 **kwargs: Additional keyword arguments, passed to XmlManifest.
2006 if submanifest_path: 2236 """
2007 self._CheckLocalPath(submanifest_path) 2237 self.isGitcClient = False
2008 prefix = os.path.join(repodir, SUBMANIFEST_DIR, submanifest_path) 2238 submanifest_path = submanifest_path or ""
2009 else: 2239 if submanifest_path:
2010 prefix = repodir 2240 self._CheckLocalPath(submanifest_path)
2011 2241 prefix = os.path.join(repodir, SUBMANIFEST_DIR, submanifest_path)
2012 if os.path.exists(os.path.join(prefix, LOCAL_MANIFEST_NAME)): 2242 else:
2013 print('error: %s is not supported; put local manifests in `%s` instead' % 2243 prefix = repodir
2014 (LOCAL_MANIFEST_NAME, os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME)), 2244
2015 file=sys.stderr) 2245 if os.path.exists(os.path.join(prefix, LOCAL_MANIFEST_NAME)):
2016 sys.exit(1) 2246 print(
2017 2247 "error: %s is not supported; put local manifests in `%s` "
2018 if manifest_file is None: 2248 "instead"
2019 manifest_file = os.path.join(prefix, MANIFEST_FILE_NAME) 2249 % (
2020 local_manifests = os.path.abspath(os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME)) 2250 LOCAL_MANIFEST_NAME,
2021 super().__init__(repodir, manifest_file, local_manifests, 2251 os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME),
2022 submanifest_path=submanifest_path, **kwargs) 2252 ),
2023 2253 file=sys.stderr,
2024 # TODO: Completely separate manifest logic out of the client. 2254 )
2025 self.manifest = self 2255 sys.exit(1)
2256
2257 if manifest_file is None:
2258 manifest_file = os.path.join(prefix, MANIFEST_FILE_NAME)
2259 local_manifests = os.path.abspath(
2260 os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME)
2261 )
2262 super().__init__(
2263 repodir,
2264 manifest_file,
2265 local_manifests,
2266 submanifest_path=submanifest_path,
2267 **kwargs,
2268 )
2269
2270 # TODO: Completely separate manifest logic out of the client.
2271 self.manifest = self
2026 2272
2027 2273
2028class GitcClient(RepoClient, GitcManifest): 2274class GitcClient(RepoClient, GitcManifest):
2029 """Manages a GitC client checkout.""" 2275 """Manages a GitC client checkout."""
2030 2276
2031 def __init__(self, repodir, gitc_client_name): 2277 def __init__(self, repodir, gitc_client_name):
2032 """Initialize the GitcManifest object.""" 2278 """Initialize the GitcManifest object."""
2033 self.gitc_client_name = gitc_client_name 2279 self.gitc_client_name = gitc_client_name
2034 self.gitc_client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(), 2280 self.gitc_client_dir = os.path.join(
2035 gitc_client_name) 2281 gitc_utils.get_gitc_manifest_dir(), gitc_client_name
2036 2282 )
2037 super().__init__(repodir, os.path.join(self.gitc_client_dir, '.manifest')) 2283
2038 self.isGitcClient = True 2284 super().__init__(
2285 repodir, os.path.join(self.gitc_client_dir, ".manifest")
2286 )
2287 self.isGitcClient = True