blob: 7e533a02d826795dc4d408f6ab91b6e536c9d8b3 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Raman Tenneti993af5e2021-05-12 12:00:31 -070015import collections
Colin Cross23acdd32012-04-21 00:33:54 -070016import itertools
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070017import os
Raman Tenneti080877e2021-03-09 15:19:06 -080018import platform
Conley Owensdb728cd2011-09-26 16:34:01 -070019import re
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070020import sys
Mike Frysingeracf63b22019-06-13 02:24:21 -040021import urllib.parse
Mike Frysinger64477332023-08-21 21:20:32 -040022import xml.dom.minidom
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070023
Mike Frysinger64477332023-08-21 21:20:32 -040024from error import ManifestInvalidPathError
25from error import ManifestInvalidRevisionError
26from error import ManifestParseError
Daniel Kutik035f22a2022-12-13 12:34:23 +010027from git_config import GitConfig
Mike Frysinger64477332023-08-21 21:20:32 -040028from git_refs import HEAD
29from git_refs import R_HEADS
LaMont Jonesd56e2eb2022-04-07 18:14:46 +000030from git_superproject import Superproject
Renaud Paquayd5cec5e2016-11-01 11:24:03 -070031import platform_utils
Mike Frysinger64477332023-08-21 21:20:32 -040032from project import Annotation
33from project import ManifestProject
34from project import Project
35from project import RemoteSpec
36from project import RepoProject
Raman Tenneti993af5e2021-05-12 12:00:31 -070037from wrapper import Wrapper
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070038
Mike Frysinger64477332023-08-21 21:20:32 -040039
Gavin Makea2e3302023-03-11 06:46:20 +000040MANIFEST_FILE_NAME = "manifest.xml"
41LOCAL_MANIFEST_NAME = "local_manifest.xml"
42LOCAL_MANIFESTS_DIR_NAME = "local_manifests"
43SUBMANIFEST_DIR = "submanifests"
LaMont Jonescc879a92021-11-18 22:40:18 +000044# Limit submanifests to an arbitrary depth for loop detection.
45MAX_SUBMANIFEST_DEPTH = 8
LaMont Jonesb308db12022-02-25 17:05:21 +000046# Add all projects from sub manifest into a group.
Gavin Makea2e3302023-03-11 06:46:20 +000047SUBMANIFEST_GROUP_PREFIX = "submanifest:"
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070048
Raman Tenneti78f4dd32021-06-07 13:27:37 -070049# Add all projects from local manifest into a group.
Gavin Makea2e3302023-03-11 06:46:20 +000050LOCAL_MANIFEST_GROUP_PREFIX = "local:"
Raman Tenneti78f4dd32021-06-07 13:27:37 -070051
Raman Tenneti993af5e2021-05-12 12:00:31 -070052# ContactInfo has the self-registered bug url, supplied by the manifest authors.
Gavin Makea2e3302023-03-11 06:46:20 +000053ContactInfo = collections.namedtuple("ContactInfo", "bugurl")
Raman Tenneti993af5e2021-05-12 12:00:31 -070054
Anthony Kingcb07ba72015-03-28 23:26:04 +000055# urljoin gets confused if the scheme is not known.
Gavin Makea2e3302023-03-11 06:46:20 +000056urllib.parse.uses_relative.extend(
57 ["ssh", "git", "persistent-https", "sso", "rpc"]
58)
59urllib.parse.uses_netloc.extend(
60 ["ssh", "git", "persistent-https", "sso", "rpc"]
61)
Conley Owensdb728cd2011-09-26 16:34:01 -070062
David Pursehouse819827a2020-02-12 15:20:19 +090063
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -050064def XmlBool(node, attr, default=None):
Gavin Makea2e3302023-03-11 06:46:20 +000065 """Determine boolean value of |node|'s |attr|.
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -050066
Gavin Makea2e3302023-03-11 06:46:20 +000067 Invalid values will issue a non-fatal warning.
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -050068
Gavin Makea2e3302023-03-11 06:46:20 +000069 Args:
70 node: XML node whose attributes we access.
71 attr: The attribute to access.
72 default: If the attribute is not set (value is empty), then use this.
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -050073
Gavin Makea2e3302023-03-11 06:46:20 +000074 Returns:
75 True if the attribute is a valid string representing true.
76 False if the attribute is a valid string representing false.
77 |default| otherwise.
78 """
79 value = node.getAttribute(attr)
80 s = value.lower()
81 if s == "":
82 return default
83 elif s in {"yes", "true", "1"}:
84 return True
85 elif s in {"no", "false", "0"}:
86 return False
87 else:
88 print(
89 'warning: manifest: %s="%s": ignoring invalid XML boolean'
90 % (attr, value),
91 file=sys.stderr,
92 )
93 return default
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -050094
95
96def XmlInt(node, attr, default=None):
Gavin Makea2e3302023-03-11 06:46:20 +000097 """Determine integer value of |node|'s |attr|.
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -050098
Gavin Makea2e3302023-03-11 06:46:20 +000099 Args:
100 node: XML node whose attributes we access.
101 attr: The attribute to access.
102 default: If the attribute is not set (value is empty), then use this.
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -0500103
Gavin Makea2e3302023-03-11 06:46:20 +0000104 Returns:
105 The number if the attribute is a valid number.
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -0500106
Gavin Makea2e3302023-03-11 06:46:20 +0000107 Raises:
108 ManifestParseError: The number is invalid.
109 """
110 value = node.getAttribute(attr)
111 if not value:
112 return default
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -0500113
Gavin Makea2e3302023-03-11 06:46:20 +0000114 try:
115 return int(value)
116 except ValueError:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400117 raise ManifestParseError(f'manifest: invalid {attr}="{value}" integer')
Mike Frysingerbb8ee7f2020-02-22 05:30:12 -0500118
119
Michael Kelly3652b492023-09-19 09:51:03 -0700120def normalize_url(url: str) -> str:
121 """Mutate input 'url' into normalized form:
122
123 * remove trailing slashes
124 * convert SCP-like syntax to SSH URL
125
126 Args:
127 url: URL to modify
128
129 Returns:
130 The normalized URL.
131 """
132
133 url = url.rstrip("/")
134 parsed_url = urllib.parse.urlparse(url)
135
136 # This matches patterns like "git@github.com:foo/bar".
Mike Frysinger48e41372023-12-18 16:31:11 -0500137 scp_like_url_re = r"^[^/:]+@[^/:]+:[^/]+/"
Michael Kelly3652b492023-09-19 09:51:03 -0700138
139 # If our URL is missing a schema and matches git's
140 # SCP-like syntax we should convert it to a proper
141 # SSH URL instead to make urljoin() happier.
142 #
143 # See: https://git-scm.com/docs/git-clone#URLS
144 if not parsed_url.scheme and re.match(scp_like_url_re, url):
145 return "ssh://" + url.replace(":", "/", 1)
146
147 return url
148
149
Mike Frysingerd4aee652023-10-19 05:13:32 -0400150class _Default:
Gavin Makea2e3302023-03-11 06:46:20 +0000151 """Project defaults within the manifest."""
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700152
Gavin Makea2e3302023-03-11 06:46:20 +0000153 revisionExpr = None
154 destBranchExpr = None
155 upstreamExpr = None
156 remote = None
157 sync_j = None
158 sync_c = False
159 sync_s = False
160 sync_tags = True
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700161
Gavin Makea2e3302023-03-11 06:46:20 +0000162 def __eq__(self, other):
163 if not isinstance(other, _Default):
164 return False
165 return self.__dict__ == other.__dict__
Julien Campergue74879922013-10-09 14:38:46 +0200166
Gavin Makea2e3302023-03-11 06:46:20 +0000167 def __ne__(self, other):
168 if not isinstance(other, _Default):
169 return True
170 return self.__dict__ != other.__dict__
Julien Campergue74879922013-10-09 14:38:46 +0200171
David Pursehouse819827a2020-02-12 15:20:19 +0900172
Mike Frysingerd4aee652023-10-19 05:13:32 -0400173class _XmlRemote:
Gavin Makea2e3302023-03-11 06:46:20 +0000174 def __init__(
175 self,
176 name,
177 alias=None,
178 fetch=None,
179 pushUrl=None,
180 manifestUrl=None,
181 review=None,
182 revision=None,
183 ):
184 self.name = name
185 self.fetchUrl = fetch
186 self.pushUrl = pushUrl
187 self.manifestUrl = manifestUrl
188 self.remoteAlias = alias
189 self.reviewUrl = review
190 self.revision = revision
191 self.resolvedFetchUrl = self._resolveFetchUrl()
192 self.annotations = []
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700193
Gavin Makea2e3302023-03-11 06:46:20 +0000194 def __eq__(self, other):
195 if not isinstance(other, _XmlRemote):
196 return False
197 return (
198 sorted(self.annotations) == sorted(other.annotations)
199 and self.name == other.name
200 and self.fetchUrl == other.fetchUrl
201 and self.pushUrl == other.pushUrl
202 and self.remoteAlias == other.remoteAlias
203 and self.reviewUrl == other.reviewUrl
204 and self.revision == other.revision
205 )
David Pursehouse717ece92012-11-13 08:49:16 +0900206
Gavin Makea2e3302023-03-11 06:46:20 +0000207 def __ne__(self, other):
208 return not self.__eq__(other)
David Pursehouse717ece92012-11-13 08:49:16 +0900209
Gavin Makea2e3302023-03-11 06:46:20 +0000210 def _resolveFetchUrl(self):
211 if self.fetchUrl is None:
212 return ""
Anthony Kingcb07ba72015-03-28 23:26:04 +0000213
Michael Kelly3652b492023-09-19 09:51:03 -0700214 fetch_url = normalize_url(self.fetchUrl)
215 manifest_url = normalize_url(self.manifestUrl)
216
217 # urljoin doesn't like URLs with no scheme in the base URL
218 # such as file paths. We handle this by prefixing it with
219 # an obscure protocol, gopher, and replacing it with the
220 # original after urljoin
221 if manifest_url.find(":") != manifest_url.find("/") - 1:
222 fetch_url = urllib.parse.urljoin(
223 "gopher://" + manifest_url, fetch_url
224 )
225 fetch_url = re.sub(r"^gopher://", "", fetch_url)
Gavin Makea2e3302023-03-11 06:46:20 +0000226 else:
Michael Kelly3652b492023-09-19 09:51:03 -0700227 fetch_url = urllib.parse.urljoin(manifest_url, fetch_url)
228 return fetch_url
Conley Owensceea3682011-10-20 10:45:47 -0700229
Gavin Makea2e3302023-03-11 06:46:20 +0000230 def ToRemoteSpec(self, projectName):
231 fetchUrl = self.resolvedFetchUrl.rstrip("/")
232 url = fetchUrl + "/" + projectName
233 remoteName = self.name
234 if self.remoteAlias:
235 remoteName = self.remoteAlias
236 return RemoteSpec(
237 remoteName,
238 url=url,
239 pushUrl=self.pushUrl,
240 review=self.reviewUrl,
241 orig_name=self.name,
242 fetchUrl=self.fetchUrl,
243 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700244
Gavin Makea2e3302023-03-11 06:46:20 +0000245 def AddAnnotation(self, name, value, keep):
246 self.annotations.append(Annotation(name, value, keep))
Jack Neus6ea0cae2021-07-20 20:52:33 +0000247
David Pursehouse819827a2020-02-12 15:20:19 +0900248
LaMont Jonescc879a92021-11-18 22:40:18 +0000249class _XmlSubmanifest:
Gavin Makea2e3302023-03-11 06:46:20 +0000250 """Manage the <submanifest> element specified in the manifest.
LaMont Jonescc879a92021-11-18 22:40:18 +0000251
Gavin Makea2e3302023-03-11 06:46:20 +0000252 Attributes:
253 name: a string, the name for this submanifest.
254 remote: a string, the remote.name for this submanifest.
255 project: a string, the name of the manifest project.
256 revision: a string, the commitish.
257 manifestName: a string, the submanifest file name.
258 groups: a list of strings, the groups to add to all projects in the
259 submanifest.
260 default_groups: a list of strings, the default groups to sync.
261 path: a string, the relative path for the submanifest checkout.
262 parent: an XmlManifest, the parent manifest.
263 annotations: (derived) a list of annotations.
264 present: (derived) a boolean, whether the sub manifest file is present.
265 """
LaMont Jonescc879a92021-11-18 22:40:18 +0000266
Gavin Makea2e3302023-03-11 06:46:20 +0000267 def __init__(
268 self,
269 name,
270 remote=None,
271 project=None,
272 revision=None,
273 manifestName=None,
274 groups=None,
275 default_groups=None,
276 path=None,
277 parent=None,
278 ):
279 self.name = name
280 self.remote = remote
281 self.project = project
282 self.revision = revision
283 self.manifestName = manifestName
284 self.groups = groups
285 self.default_groups = default_groups
286 self.path = path
287 self.parent = parent
288 self.annotations = []
289 outer_client = parent._outer_client or parent
290 if self.remote and not self.project:
291 raise ManifestParseError(
292 f"Submanifest {name}: must specify project when remote is "
293 "given."
294 )
295 # Construct the absolute path to the manifest file using the parent's
296 # method, so that we can correctly create our repo_client.
297 manifestFile = parent.SubmanifestInfoDir(
298 os.path.join(parent.path_prefix, self.relpath),
299 os.path.join("manifests", manifestName or "default.xml"),
300 )
301 linkFile = parent.SubmanifestInfoDir(
302 os.path.join(parent.path_prefix, self.relpath), MANIFEST_FILE_NAME
303 )
304 self.repo_client = RepoClient(
305 parent.repodir,
306 linkFile,
307 parent_groups=",".join(groups) or "",
Guillaume Micouin-Jordac984e8d2023-11-09 15:13:17 +0100308 submanifest_path=os.path.join(parent.path_prefix, self.relpath),
Gavin Makea2e3302023-03-11 06:46:20 +0000309 outer_client=outer_client,
310 default_groups=default_groups,
311 )
LaMont Jonescc879a92021-11-18 22:40:18 +0000312
Gavin Makea2e3302023-03-11 06:46:20 +0000313 self.present = os.path.exists(manifestFile)
LaMont Jonescc879a92021-11-18 22:40:18 +0000314
Gavin Makea2e3302023-03-11 06:46:20 +0000315 def __eq__(self, other):
316 if not isinstance(other, _XmlSubmanifest):
317 return False
318 return (
319 self.name == other.name
320 and self.remote == other.remote
321 and self.project == other.project
322 and self.revision == other.revision
323 and self.manifestName == other.manifestName
324 and self.groups == other.groups
325 and self.default_groups == other.default_groups
326 and self.path == other.path
327 and sorted(self.annotations) == sorted(other.annotations)
328 )
LaMont Jonescc879a92021-11-18 22:40:18 +0000329
Gavin Makea2e3302023-03-11 06:46:20 +0000330 def __ne__(self, other):
331 return not self.__eq__(other)
LaMont Jonescc879a92021-11-18 22:40:18 +0000332
Gavin Makea2e3302023-03-11 06:46:20 +0000333 def ToSubmanifestSpec(self):
334 """Return a SubmanifestSpec object, populating attributes"""
335 mp = self.parent.manifestProject
336 remote = self.parent.remotes[
337 self.remote or self.parent.default.remote.name
338 ]
339 # If a project was given, generate the url from the remote and project.
340 # If not, use this manifestProject's url.
341 if self.project:
342 manifestUrl = remote.ToRemoteSpec(self.project).url
343 else:
344 manifestUrl = mp.GetRemote().url
345 manifestName = self.manifestName or "default.xml"
346 revision = self.revision or self.name
347 path = self.path or revision.split("/")[-1]
348 groups = self.groups or []
LaMont Jonescc879a92021-11-18 22:40:18 +0000349
Gavin Makea2e3302023-03-11 06:46:20 +0000350 return SubmanifestSpec(
351 self.name, manifestUrl, manifestName, revision, path, groups
352 )
LaMont Jonescc879a92021-11-18 22:40:18 +0000353
Gavin Makea2e3302023-03-11 06:46:20 +0000354 @property
355 def relpath(self):
356 """The path of this submanifest relative to the parent manifest."""
357 revision = self.revision or self.name
358 return self.path or revision.split("/")[-1]
LaMont Jonescc879a92021-11-18 22:40:18 +0000359
Gavin Makea2e3302023-03-11 06:46:20 +0000360 def GetGroupsStr(self):
361 """Returns the `groups` given for this submanifest."""
362 if self.groups:
363 return ",".join(self.groups)
364 return ""
LaMont Jones501733c2022-04-20 16:42:32 +0000365
Gavin Makea2e3302023-03-11 06:46:20 +0000366 def GetDefaultGroupsStr(self):
367 """Returns the `default-groups` given for this submanifest."""
368 return ",".join(self.default_groups or [])
369
370 def AddAnnotation(self, name, value, keep):
371 """Add annotations to the submanifest."""
372 self.annotations.append(Annotation(name, value, keep))
LaMont Jonescc879a92021-11-18 22:40:18 +0000373
374
375class SubmanifestSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000376 """The submanifest element, with all fields expanded."""
LaMont Jonescc879a92021-11-18 22:40:18 +0000377
Gavin Makea2e3302023-03-11 06:46:20 +0000378 def __init__(self, name, manifestUrl, manifestName, revision, path, groups):
379 self.name = name
380 self.manifestUrl = manifestUrl
381 self.manifestName = manifestName
382 self.revision = revision
383 self.path = path
384 self.groups = groups or []
LaMont Jonescc879a92021-11-18 22:40:18 +0000385
386
Mike Frysingerd4aee652023-10-19 05:13:32 -0400387class XmlManifest:
Gavin Makea2e3302023-03-11 06:46:20 +0000388 """manages the repo configuration file"""
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700389
Gavin Makea2e3302023-03-11 06:46:20 +0000390 def __init__(
391 self,
392 repodir,
393 manifest_file,
394 local_manifests=None,
395 outer_client=None,
396 parent_groups="",
397 submanifest_path="",
398 default_groups=None,
399 ):
400 """Initialize.
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -0400401
Gavin Makea2e3302023-03-11 06:46:20 +0000402 Args:
403 repodir: Path to the .repo/ dir for holding all internal checkout
404 state. It must be in the top directory of the repo client
405 checkout.
406 manifest_file: Full path to the manifest file to parse. This will
407 usually be |repodir|/|MANIFEST_FILE_NAME|.
408 local_manifests: Full path to the directory of local override
409 manifests. This will usually be
410 |repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
411 outer_client: RepoClient of the outer manifest.
412 parent_groups: a string, the groups to apply to this projects.
413 submanifest_path: The submanifest root relative to the repo root.
414 default_groups: a string, the default manifest groups to use.
415 """
416 # TODO(vapier): Move this out of this class.
417 self.globalConfig = GitConfig.ForUser()
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -0400418
Gavin Makea2e3302023-03-11 06:46:20 +0000419 self.repodir = os.path.abspath(repodir)
420 self._CheckLocalPath(submanifest_path)
421 self.topdir = os.path.dirname(self.repodir)
422 if submanifest_path:
423 # This avoids a trailing os.path.sep when submanifest_path is empty.
424 self.topdir = os.path.join(self.topdir, submanifest_path)
425 if manifest_file != os.path.abspath(manifest_file):
426 raise ManifestParseError("manifest_file must be abspath")
427 self.manifestFile = manifest_file
428 if not outer_client or outer_client == self:
429 # manifestFileOverrides only exists in the outer_client's manifest,
430 # since that is the only instance left when Unload() is called on
431 # the outer manifest.
432 self.manifestFileOverrides = {}
433 self.local_manifests = local_manifests
434 self._load_local_manifests = True
435 self.parent_groups = parent_groups
436 self.default_groups = default_groups
LaMont Jonescc879a92021-11-18 22:40:18 +0000437
Gavin Makea2e3302023-03-11 06:46:20 +0000438 if outer_client and self.isGitcClient:
439 raise ManifestParseError(
440 "Multi-manifest is incompatible with `gitc-init`"
441 )
LaMont Jonescc879a92021-11-18 22:40:18 +0000442
Gavin Makea2e3302023-03-11 06:46:20 +0000443 if submanifest_path and not outer_client:
444 # If passing a submanifest_path, there must be an outer_client.
445 raise ManifestParseError(f"Bad call to {self.__class__.__name__}")
LaMont Jonescc879a92021-11-18 22:40:18 +0000446
Gavin Makea2e3302023-03-11 06:46:20 +0000447 # If self._outer_client is None, this is not a checkout that supports
448 # multi-tree.
449 self._outer_client = outer_client or self
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700450
Gavin Makea2e3302023-03-11 06:46:20 +0000451 self.repoProject = RepoProject(
452 self,
453 "repo",
454 gitdir=os.path.join(repodir, "repo/.git"),
455 worktree=os.path.join(repodir, "repo"),
456 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700457
Gavin Makea2e3302023-03-11 06:46:20 +0000458 mp = self.SubmanifestProject(self.path_prefix)
459 self.manifestProject = mp
Mike Frysinger979d5bd2020-02-09 02:28:34 -0500460
Gavin Makea2e3302023-03-11 06:46:20 +0000461 # This is a bit hacky, but we're in a chicken & egg situation: all the
462 # normal repo settings live in the manifestProject which we just setup
463 # above, so we couldn't easily query before that. We assume Project()
464 # init doesn't care if this changes afterwards.
465 if os.path.exists(mp.gitdir) and mp.use_worktree:
466 mp.use_git_worktrees = True
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700467
Gavin Makea2e3302023-03-11 06:46:20 +0000468 self.Unload()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700469
Gavin Makea2e3302023-03-11 06:46:20 +0000470 def Override(self, name, load_local_manifests=True):
471 """Use a different manifest, just for the current instantiation."""
472 path = None
Basil Gelloc7453502018-05-25 20:23:52 +0300473
Gavin Makea2e3302023-03-11 06:46:20 +0000474 # Look for a manifest by path in the filesystem (including the cwd).
475 if not load_local_manifests:
476 local_path = os.path.abspath(name)
477 if os.path.isfile(local_path):
478 path = local_path
Basil Gelloc7453502018-05-25 20:23:52 +0300479
Gavin Makea2e3302023-03-11 06:46:20 +0000480 # Look for manifests by name from the manifests repo.
481 if path is None:
482 path = os.path.join(self.manifestProject.worktree, name)
483 if not os.path.isfile(path):
484 raise ManifestParseError("manifest %s not found" % name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700485
Gavin Makea2e3302023-03-11 06:46:20 +0000486 self._load_local_manifests = load_local_manifests
487 self._outer_client.manifestFileOverrides[self.path_prefix] = path
488 self.Unload()
489 self._Load()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700490
Gavin Makea2e3302023-03-11 06:46:20 +0000491 def Link(self, name):
492 """Update the repo metadata to use a different manifest."""
493 self.Override(name)
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700494
Gavin Makea2e3302023-03-11 06:46:20 +0000495 # Old versions of repo would generate symlinks we need to clean up.
496 platform_utils.remove(self.manifestFile, missing_ok=True)
497 # This file is interpreted as if it existed inside the manifest repo.
498 # That allows us to use <include> with the relative file name.
499 with open(self.manifestFile, "w") as fp:
500 fp.write(
501 """<?xml version="1.0" encoding="UTF-8"?>
Mike Frysingera269b1c2020-02-21 00:49:41 -0500502<!--
503DO NOT EDIT THIS FILE! It is generated by repo and changes will be discarded.
504If you want to use a different manifest, use `repo init -m <file>` instead.
505
506If you want to customize your checkout by overriding manifest settings, use
507the local_manifests/ directory instead.
508
509For more information on repo manifests, check out:
510https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
511-->
512<manifest>
513 <include name="%s" />
514</manifest>
Gavin Makea2e3302023-03-11 06:46:20 +0000515"""
516 % (name,)
517 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700518
Gavin Makea2e3302023-03-11 06:46:20 +0000519 def _RemoteToXml(self, r, doc, root):
520 e = doc.createElement("remote")
521 root.appendChild(e)
522 e.setAttribute("name", r.name)
523 e.setAttribute("fetch", r.fetchUrl)
524 if r.pushUrl is not None:
525 e.setAttribute("pushurl", r.pushUrl)
526 if r.remoteAlias is not None:
527 e.setAttribute("alias", r.remoteAlias)
528 if r.reviewUrl is not None:
529 e.setAttribute("review", r.reviewUrl)
530 if r.revision is not None:
531 e.setAttribute("revision", r.revision)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800532
Gavin Makea2e3302023-03-11 06:46:20 +0000533 for a in r.annotations:
534 if a.keep == "true":
535 ae = doc.createElement("annotation")
536 ae.setAttribute("name", a.name)
537 ae.setAttribute("value", a.value)
538 e.appendChild(ae)
Jack Neus6ea0cae2021-07-20 20:52:33 +0000539
Gavin Makea2e3302023-03-11 06:46:20 +0000540 def _SubmanifestToXml(self, r, doc, root):
541 """Generate XML <submanifest/> node."""
542 e = doc.createElement("submanifest")
543 root.appendChild(e)
544 e.setAttribute("name", r.name)
545 if r.remote is not None:
546 e.setAttribute("remote", r.remote)
547 if r.project is not None:
548 e.setAttribute("project", r.project)
549 if r.manifestName is not None:
550 e.setAttribute("manifest-name", r.manifestName)
551 if r.revision is not None:
552 e.setAttribute("revision", r.revision)
553 if r.path is not None:
554 e.setAttribute("path", r.path)
555 if r.groups:
556 e.setAttribute("groups", r.GetGroupsStr())
557 if r.default_groups:
558 e.setAttribute("default-groups", r.GetDefaultGroupsStr())
LaMont Jonescc879a92021-11-18 22:40:18 +0000559
Gavin Makea2e3302023-03-11 06:46:20 +0000560 for a in r.annotations:
561 if a.keep == "true":
562 ae = doc.createElement("annotation")
563 ae.setAttribute("name", a.name)
564 ae.setAttribute("value", a.value)
565 e.appendChild(ae)
LaMont Jonescc879a92021-11-18 22:40:18 +0000566
Gavin Makea2e3302023-03-11 06:46:20 +0000567 def _ParseList(self, field):
568 """Parse fields that contain flattened lists.
Mike Frysinger51e39d52020-12-04 05:32:06 -0500569
Gavin Makea2e3302023-03-11 06:46:20 +0000570 These are whitespace & comma separated. Empty elements will be
571 discarded.
572 """
573 return [x for x in re.split(r"[,\s]+", field) if x]
Josh Triplett884a3872014-06-12 14:57:29 -0700574
Gavin Makea2e3302023-03-11 06:46:20 +0000575 def ToXml(
576 self,
577 peg_rev=False,
578 peg_rev_upstream=True,
579 peg_rev_dest_branch=True,
580 groups=None,
581 omit_local=False,
582 ):
583 """Return the current manifest XML."""
584 mp = self.manifestProject
Colin Cross5acde752012-03-28 20:15:45 -0700585
Gavin Makea2e3302023-03-11 06:46:20 +0000586 if groups is None:
587 groups = mp.manifest_groups
588 if groups:
589 groups = self._ParseList(groups)
Colin Cross5acde752012-03-28 20:15:45 -0700590
Gavin Makea2e3302023-03-11 06:46:20 +0000591 doc = xml.dom.minidom.Document()
592 root = doc.createElement("manifest")
593 if self.is_submanifest:
594 root.setAttribute("path", self.path_prefix)
595 doc.appendChild(root)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800596
Gavin Makea2e3302023-03-11 06:46:20 +0000597 # Save out the notice. There's a little bit of work here to give it the
598 # right whitespace, which assumes that the notice is automatically
599 # indented by 4 by minidom.
600 if self.notice:
601 notice_element = root.appendChild(doc.createElement("notice"))
602 notice_lines = self.notice.splitlines()
603 indented_notice = (
604 "\n".join(" " * 4 + line for line in notice_lines)
605 )[4:]
606 notice_element.appendChild(doc.createTextNode(indented_notice))
Doug Anderson2b8db3c2010-11-01 15:08:06 -0700607
Gavin Makea2e3302023-03-11 06:46:20 +0000608 d = self.default
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800609
Gavin Makea2e3302023-03-11 06:46:20 +0000610 for r in sorted(self.remotes):
611 self._RemoteToXml(self.remotes[r], doc, root)
612 if self.remotes:
613 root.appendChild(doc.createTextNode(""))
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800614
Gavin Makea2e3302023-03-11 06:46:20 +0000615 have_default = False
616 e = doc.createElement("default")
617 if d.remote:
618 have_default = True
619 e.setAttribute("remote", d.remote.name)
620 if d.revisionExpr:
621 have_default = True
622 e.setAttribute("revision", d.revisionExpr)
623 if d.destBranchExpr:
624 have_default = True
625 e.setAttribute("dest-branch", d.destBranchExpr)
626 if d.upstreamExpr:
627 have_default = True
628 e.setAttribute("upstream", d.upstreamExpr)
629 if d.sync_j is not None:
630 have_default = True
631 e.setAttribute("sync-j", "%d" % d.sync_j)
632 if d.sync_c:
633 have_default = True
634 e.setAttribute("sync-c", "true")
635 if d.sync_s:
636 have_default = True
637 e.setAttribute("sync-s", "true")
638 if not d.sync_tags:
639 have_default = True
640 e.setAttribute("sync-tags", "false")
641 if have_default:
642 root.appendChild(e)
643 root.appendChild(doc.createTextNode(""))
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800644
Gavin Makea2e3302023-03-11 06:46:20 +0000645 if self._manifest_server:
646 e = doc.createElement("manifest-server")
647 e.setAttribute("url", self._manifest_server)
648 root.appendChild(e)
649 root.appendChild(doc.createTextNode(""))
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700650
Gavin Makea2e3302023-03-11 06:46:20 +0000651 for r in sorted(self.submanifests):
652 self._SubmanifestToXml(self.submanifests[r], doc, root)
653 if self.submanifests:
654 root.appendChild(doc.createTextNode(""))
LaMont Jonescc879a92021-11-18 22:40:18 +0000655
Gavin Makea2e3302023-03-11 06:46:20 +0000656 def output_projects(parent, parent_node, projects):
657 for project_name in projects:
658 for project in self._projects[project_name]:
659 output_project(parent, parent_node, project)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800660
Gavin Makea2e3302023-03-11 06:46:20 +0000661 def output_project(parent, parent_node, p):
662 if not p.MatchesGroups(groups):
663 return
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800664
Gavin Makea2e3302023-03-11 06:46:20 +0000665 if omit_local and self.IsFromLocalManifest(p):
666 return
LaMont Jonesa8cf5752022-07-15 20:31:33 +0000667
Gavin Makea2e3302023-03-11 06:46:20 +0000668 name = p.name
669 relpath = p.relpath
670 if parent:
671 name = self._UnjoinName(parent.name, name)
672 relpath = self._UnjoinRelpath(parent.relpath, relpath)
Colin Cross5acde752012-03-28 20:15:45 -0700673
Gavin Makea2e3302023-03-11 06:46:20 +0000674 e = doc.createElement("project")
675 parent_node.appendChild(e)
676 e.setAttribute("name", name)
677 if relpath != name:
678 e.setAttribute("path", relpath)
679 remoteName = None
680 if d.remote:
681 remoteName = d.remote.name
682 if not d.remote or p.remote.orig_name != remoteName:
683 remoteName = p.remote.orig_name
684 e.setAttribute("remote", remoteName)
685 if peg_rev:
686 if self.IsMirror:
687 value = p.bare_git.rev_parse(p.revisionExpr + "^0")
688 else:
689 value = p.work_git.rev_parse(HEAD + "^0")
690 e.setAttribute("revision", value)
691 if peg_rev_upstream:
692 if p.upstream:
693 e.setAttribute("upstream", p.upstream)
694 elif value != p.revisionExpr:
695 # Only save the origin if the origin is not a sha1, and
696 # the default isn't our value
697 e.setAttribute("upstream", p.revisionExpr)
698
699 if peg_rev_dest_branch:
700 if p.dest_branch:
701 e.setAttribute("dest-branch", p.dest_branch)
702 elif value != p.revisionExpr:
703 e.setAttribute("dest-branch", p.revisionExpr)
704
705 else:
706 revision = (
707 self.remotes[p.remote.orig_name].revision or d.revisionExpr
708 )
709 if not revision or revision != p.revisionExpr:
710 e.setAttribute("revision", p.revisionExpr)
711 elif p.revisionId:
712 e.setAttribute("revision", p.revisionId)
713 if p.upstream and (
714 p.upstream != p.revisionExpr or p.upstream != d.upstreamExpr
715 ):
716 e.setAttribute("upstream", p.upstream)
717
718 if p.dest_branch and p.dest_branch != d.destBranchExpr:
719 e.setAttribute("dest-branch", p.dest_branch)
720
721 for c in p.copyfiles:
722 ce = doc.createElement("copyfile")
723 ce.setAttribute("src", c.src)
724 ce.setAttribute("dest", c.dest)
725 e.appendChild(ce)
726
727 for lf in p.linkfiles:
728 le = doc.createElement("linkfile")
729 le.setAttribute("src", lf.src)
730 le.setAttribute("dest", lf.dest)
731 e.appendChild(le)
732
733 default_groups = ["all", "name:%s" % p.name, "path:%s" % p.relpath]
734 egroups = [g for g in p.groups if g not in default_groups]
735 if egroups:
736 e.setAttribute("groups", ",".join(egroups))
737
738 for a in p.annotations:
739 if a.keep == "true":
740 ae = doc.createElement("annotation")
741 ae.setAttribute("name", a.name)
742 ae.setAttribute("value", a.value)
743 e.appendChild(ae)
744
745 if p.sync_c:
746 e.setAttribute("sync-c", "true")
747
748 if p.sync_s:
749 e.setAttribute("sync-s", "true")
750
751 if not p.sync_tags:
752 e.setAttribute("sync-tags", "false")
753
754 if p.clone_depth:
755 e.setAttribute("clone-depth", str(p.clone_depth))
756
757 self._output_manifest_project_extras(p, e)
758
759 if p.subprojects:
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545760 subprojects = {subp.name for subp in p.subprojects}
Gavin Makea2e3302023-03-11 06:46:20 +0000761 output_projects(p, e, list(sorted(subprojects)))
762
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545763 projects = {p.name for p in self._paths.values() if not p.parent}
Gavin Makea2e3302023-03-11 06:46:20 +0000764 output_projects(None, root, list(sorted(projects)))
765
766 if self._repo_hooks_project:
767 root.appendChild(doc.createTextNode(""))
768 e = doc.createElement("repo-hooks")
769 e.setAttribute("in-project", self._repo_hooks_project.name)
770 e.setAttribute(
771 "enabled-list",
772 " ".join(self._repo_hooks_project.enabled_repo_hooks),
773 )
774 root.appendChild(e)
775
776 if self._superproject:
777 root.appendChild(doc.createTextNode(""))
778 e = doc.createElement("superproject")
779 e.setAttribute("name", self._superproject.name)
780 remoteName = None
781 if d.remote:
782 remoteName = d.remote.name
783 remote = self._superproject.remote
784 if not d.remote or remote.orig_name != remoteName:
785 remoteName = remote.orig_name
786 e.setAttribute("remote", remoteName)
787 revision = remote.revision or d.revisionExpr
788 if not revision or revision != self._superproject.revision:
789 e.setAttribute("revision", self._superproject.revision)
790 root.appendChild(e)
791
792 if self._contactinfo.bugurl != Wrapper().BUG_URL:
793 root.appendChild(doc.createTextNode(""))
794 e = doc.createElement("contactinfo")
795 e.setAttribute("bugurl", self._contactinfo.bugurl)
796 root.appendChild(e)
797
798 return doc
799
800 def ToDict(self, **kwargs):
801 """Return the current manifest as a dictionary."""
802 # Elements that may only appear once.
803 SINGLE_ELEMENTS = {
804 "notice",
805 "default",
806 "manifest-server",
807 "repo-hooks",
808 "superproject",
809 "contactinfo",
810 }
811 # Elements that may be repeated.
812 MULTI_ELEMENTS = {
813 "remote",
814 "remove-project",
815 "project",
816 "extend-project",
817 "include",
818 "submanifest",
819 # These are children of 'project' nodes.
820 "annotation",
821 "project",
822 "copyfile",
823 "linkfile",
824 }
825
826 doc = self.ToXml(**kwargs)
827 ret = {}
828
829 def append_children(ret, node):
830 for child in node.childNodes:
831 if child.nodeType == xml.dom.Node.ELEMENT_NODE:
832 attrs = child.attributes
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545833 element = {
834 attrs.item(i).localName: attrs.item(i).value
Gavin Makea2e3302023-03-11 06:46:20 +0000835 for i in range(attrs.length)
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545836 }
Gavin Makea2e3302023-03-11 06:46:20 +0000837 if child.nodeName in SINGLE_ELEMENTS:
838 ret[child.nodeName] = element
839 elif child.nodeName in MULTI_ELEMENTS:
840 ret.setdefault(child.nodeName, []).append(element)
841 else:
842 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400843 f'Unhandled element "{child.nodeName}"'
Gavin Makea2e3302023-03-11 06:46:20 +0000844 )
845
846 append_children(element, child)
847
848 append_children(ret, doc.firstChild)
849
850 return ret
851
852 def Save(self, fd, **kwargs):
853 """Write the current manifest out to the given file descriptor."""
854 doc = self.ToXml(**kwargs)
855 doc.writexml(fd, "", " ", "\n", "UTF-8")
856
857 def _output_manifest_project_extras(self, p, e):
858 """Manifests can modify e if they support extra project attributes."""
859
860 @property
861 def is_multimanifest(self):
862 """Whether this is a multimanifest checkout.
863
864 This is safe to use as long as the outermost manifest XML has been
865 parsed.
866 """
867 return bool(self._outer_client._submanifests)
868
869 @property
870 def is_submanifest(self):
871 """Whether this manifest is a submanifest.
872
873 This is safe to use as long as the outermost manifest XML has been
874 parsed.
875 """
876 return self._outer_client and self._outer_client != self
877
878 @property
879 def outer_client(self):
880 """The instance of the outermost manifest client."""
881 self._Load()
882 return self._outer_client
883
884 @property
885 def all_manifests(self):
886 """Generator yielding all (sub)manifests, in depth-first order."""
887 self._Load()
888 outer = self._outer_client
889 yield outer
Jason R. Coombs8dd85212023-10-20 06:48:20 -0400890 yield from outer.all_children
Gavin Makea2e3302023-03-11 06:46:20 +0000891
892 @property
893 def all_children(self):
894 """Generator yielding all (present) child submanifests."""
895 self._Load()
896 for child in self._submanifests.values():
897 if child.repo_client:
898 yield child.repo_client
Jason R. Coombs8dd85212023-10-20 06:48:20 -0400899 yield from child.repo_client.all_children
Gavin Makea2e3302023-03-11 06:46:20 +0000900
901 @property
902 def path_prefix(self):
903 """The path of this submanifest, relative to the outermost manifest."""
904 if not self._outer_client or self == self._outer_client:
905 return ""
906 return os.path.relpath(self.topdir, self._outer_client.topdir)
907
908 @property
909 def all_paths(self):
910 """All project paths for all (sub)manifests.
911
912 See also `paths`.
913
914 Returns:
915 A dictionary of {path: Project()}. `path` is relative to the outer
916 manifest.
917 """
918 ret = {}
919 for tree in self.all_manifests:
920 prefix = tree.path_prefix
921 ret.update(
922 {os.path.join(prefix, k): v for k, v in tree.paths.items()}
923 )
924 return ret
925
926 @property
927 def all_projects(self):
928 """All projects for all (sub)manifests. See `projects`."""
929 return list(
930 itertools.chain.from_iterable(
931 x._paths.values() for x in self.all_manifests
932 )
933 )
934
935 @property
936 def paths(self):
937 """Return all paths for this manifest.
938
939 Returns:
940 A dictionary of {path: Project()}. `path` is relative to this
941 manifest.
942 """
943 self._Load()
944 return self._paths
945
946 @property
947 def projects(self):
948 """Return a list of all Projects in this manifest."""
949 self._Load()
950 return list(self._paths.values())
951
952 @property
953 def remotes(self):
954 """Return a list of remotes for this manifest."""
955 self._Load()
956 return self._remotes
957
958 @property
959 def default(self):
960 """Return default values for this manifest."""
961 self._Load()
962 return self._default
963
964 @property
965 def submanifests(self):
966 """All submanifests in this manifest."""
967 self._Load()
968 return self._submanifests
969
970 @property
971 def repo_hooks_project(self):
972 self._Load()
973 return self._repo_hooks_project
974
975 @property
976 def superproject(self):
977 self._Load()
978 return self._superproject
979
980 @property
981 def contactinfo(self):
982 self._Load()
983 return self._contactinfo
984
985 @property
986 def notice(self):
987 self._Load()
988 return self._notice
989
990 @property
991 def manifest_server(self):
992 self._Load()
993 return self._manifest_server
994
995 @property
996 def CloneBundle(self):
997 clone_bundle = self.manifestProject.clone_bundle
998 if clone_bundle is None:
999 return False if self.manifestProject.partial_clone else True
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -08001000 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001001 return clone_bundle
Sean McAllisteraf908cb2020-04-20 08:41:58 -06001002
Gavin Makea2e3302023-03-11 06:46:20 +00001003 @property
1004 def CloneFilter(self):
1005 if self.manifestProject.partial_clone:
1006 return self.manifestProject.clone_filter
1007 return None
Sean McAllisteraf908cb2020-04-20 08:41:58 -06001008
Gavin Makea2e3302023-03-11 06:46:20 +00001009 @property
Jason Chang17833322023-05-23 13:06:55 -07001010 def CloneFilterForDepth(self):
1011 if self.manifestProject.clone_filter_for_depth:
1012 return self.manifestProject.clone_filter_for_depth
1013 return None
1014
1015 @property
Gavin Makea2e3302023-03-11 06:46:20 +00001016 def PartialCloneExclude(self):
1017 exclude = self.manifest.manifestProject.partial_clone_exclude or ""
Jason R. Coombs0bcffd82023-10-20 23:29:42 +05451018 return {x.strip() for x in exclude.split(",")}
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -08001019
Gavin Makea2e3302023-03-11 06:46:20 +00001020 def SetManifestOverride(self, path):
1021 """Override manifestFile. The caller must call Unload()"""
1022 self._outer_client.manifest.manifestFileOverrides[
1023 self.path_prefix
1024 ] = path
Simon Ruggier7e59de22015-07-24 12:50:06 +02001025
Gavin Makea2e3302023-03-11 06:46:20 +00001026 @property
1027 def UseLocalManifests(self):
1028 return self._load_local_manifests
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -08001029
Gavin Makea2e3302023-03-11 06:46:20 +00001030 def SetUseLocalManifests(self, value):
1031 self._load_local_manifests = value
Jeff Hamiltone0df2322014-04-21 17:10:59 -05001032
Gavin Makea2e3302023-03-11 06:46:20 +00001033 @property
1034 def HasLocalManifests(self):
1035 return self._load_local_manifests and self.local_manifests
Colin Cross5acde752012-03-28 20:15:45 -07001036
Gavin Makea2e3302023-03-11 06:46:20 +00001037 def IsFromLocalManifest(self, project):
1038 """Is the project from a local manifest?"""
1039 return any(
1040 x.startswith(LOCAL_MANIFEST_GROUP_PREFIX) for x in project.groups
1041 )
James W. Mills24c13082012-04-12 15:04:13 -05001042
Gavin Makea2e3302023-03-11 06:46:20 +00001043 @property
1044 def IsMirror(self):
1045 return self.manifestProject.mirror
Anatol Pomazau79770d22012-04-20 14:41:59 -07001046
Gavin Makea2e3302023-03-11 06:46:20 +00001047 @property
1048 def UseGitWorktrees(self):
1049 return self.manifestProject.use_worktree
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001050
Gavin Makea2e3302023-03-11 06:46:20 +00001051 @property
1052 def IsArchive(self):
1053 return self.manifestProject.archive
YOUNG HO CHAa32c92c2018-02-14 16:57:31 +09001054
Gavin Makea2e3302023-03-11 06:46:20 +00001055 @property
1056 def HasSubmodules(self):
1057 return self.manifestProject.submodules
Dan Willemsen88409222015-08-17 15:29:10 -07001058
Gavin Makea2e3302023-03-11 06:46:20 +00001059 @property
1060 def EnableGitLfs(self):
1061 return self.manifestProject.git_lfs
Simran Basib9a1b732015-08-20 12:19:28 -07001062
Gavin Makea2e3302023-03-11 06:46:20 +00001063 def FindManifestByPath(self, path):
1064 """Returns the manifest containing path."""
1065 path = os.path.abspath(path)
1066 manifest = self._outer_client or self
1067 old = None
1068 while manifest._submanifests and manifest != old:
1069 old = manifest
1070 for name in manifest._submanifests:
1071 tree = manifest._submanifests[name]
1072 if path.startswith(tree.repo_client.manifest.topdir):
1073 manifest = tree.repo_client
1074 break
1075 return manifest
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001076
Gavin Makea2e3302023-03-11 06:46:20 +00001077 @property
1078 def subdir(self):
1079 """Returns the path for per-submanifest objects for this manifest."""
1080 return self.SubmanifestInfoDir(self.path_prefix)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001081
Gavin Makea2e3302023-03-11 06:46:20 +00001082 def SubmanifestInfoDir(self, submanifest_path, object_path=""):
1083 """Return the path to submanifest-specific info for a submanifest.
Doug Anderson37282b42011-03-04 11:54:18 -08001084
Gavin Makea2e3302023-03-11 06:46:20 +00001085 Return the full path of the directory in which to put per-manifest
1086 objects.
Raman Tenneti1bb4fb22021-01-07 16:50:45 -08001087
Gavin Makea2e3302023-03-11 06:46:20 +00001088 Args:
1089 submanifest_path: a string, the path of the submanifest, relative to
1090 the outermost topdir. If empty, then repodir is returned.
1091 object_path: a string, relative path to append to the submanifest
1092 info directory path.
1093 """
1094 if submanifest_path:
1095 return os.path.join(
1096 self.repodir, SUBMANIFEST_DIR, submanifest_path, object_path
1097 )
1098 else:
1099 return os.path.join(self.repodir, object_path)
Raman Tenneti1c3f57e2021-05-04 12:32:13 -07001100
Gavin Makea2e3302023-03-11 06:46:20 +00001101 def SubmanifestProject(self, submanifest_path):
1102 """Return a manifestProject for a submanifest."""
1103 subdir = self.SubmanifestInfoDir(submanifest_path)
1104 mp = ManifestProject(
1105 self,
1106 "manifests",
1107 gitdir=os.path.join(subdir, "manifests.git"),
1108 worktree=os.path.join(subdir, "manifests"),
1109 )
1110 return mp
Mike Frysinger23411d32020-09-02 04:31:10 -04001111
Gavin Makea2e3302023-03-11 06:46:20 +00001112 def GetDefaultGroupsStr(self, with_platform=True):
1113 """Returns the default group string to use.
Mike Frysinger23411d32020-09-02 04:31:10 -04001114
Gavin Makea2e3302023-03-11 06:46:20 +00001115 Args:
1116 with_platform: a boolean, whether to include the group for the
1117 underlying platform.
1118 """
1119 groups = ",".join(self.default_groups or ["default"])
1120 if with_platform:
1121 groups += f",platform-{platform.system().lower()}"
1122 return groups
Mike Frysinger23411d32020-09-02 04:31:10 -04001123
Gavin Makea2e3302023-03-11 06:46:20 +00001124 def GetGroupsStr(self):
1125 """Returns the manifest group string that should be synced."""
1126 return (
1127 self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
1128 )
Mike Frysinger23411d32020-09-02 04:31:10 -04001129
Gavin Makea2e3302023-03-11 06:46:20 +00001130 def Unload(self):
1131 """Unload the manifest.
Mike Frysinger23411d32020-09-02 04:31:10 -04001132
Gavin Makea2e3302023-03-11 06:46:20 +00001133 If the manifest files have been changed since Load() was called, this
1134 will cause the new/updated manifest to be used.
Mike Frysinger23411d32020-09-02 04:31:10 -04001135
Gavin Makea2e3302023-03-11 06:46:20 +00001136 """
1137 self._loaded = False
1138 self._projects = {}
1139 self._paths = {}
1140 self._remotes = {}
1141 self._default = None
1142 self._submanifests = {}
1143 self._repo_hooks_project = None
1144 self._superproject = None
1145 self._contactinfo = ContactInfo(Wrapper().BUG_URL)
1146 self._notice = None
1147 self.branch = None
1148 self._manifest_server = None
Mike Frysinger23411d32020-09-02 04:31:10 -04001149
Gavin Makea2e3302023-03-11 06:46:20 +00001150 def Load(self):
1151 """Read the manifest into memory."""
1152 # Do not expose internal arguments.
1153 self._Load()
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -08001154
Gavin Makea2e3302023-03-11 06:46:20 +00001155 def _Load(self, initial_client=None, submanifest_depth=0):
1156 if submanifest_depth > MAX_SUBMANIFEST_DEPTH:
1157 raise ManifestParseError(
1158 "maximum submanifest depth %d exceeded." % MAX_SUBMANIFEST_DEPTH
1159 )
1160 if not self._loaded:
1161 if self._outer_client and self._outer_client != self:
1162 # This will load all clients.
1163 self._outer_client._Load(initial_client=self)
Simran Basib9a1b732015-08-20 12:19:28 -07001164
Gavin Makea2e3302023-03-11 06:46:20 +00001165 savedManifestFile = self.manifestFile
1166 override = self._outer_client.manifestFileOverrides.get(
1167 self.path_prefix
1168 )
1169 if override:
1170 self.manifestFile = override
Mike Frysinger1d00a7e2021-12-21 00:40:31 -05001171
Gavin Makea2e3302023-03-11 06:46:20 +00001172 try:
1173 m = self.manifestProject
1174 b = m.GetBranch(m.CurrentBranch).merge
1175 if b is not None and b.startswith(R_HEADS):
1176 b = b[len(R_HEADS) :]
1177 self.branch = b
LaMont Jonescc879a92021-11-18 22:40:18 +00001178
Gavin Makea2e3302023-03-11 06:46:20 +00001179 parent_groups = self.parent_groups
1180 if self.path_prefix:
1181 parent_groups = (
1182 f"{SUBMANIFEST_GROUP_PREFIX}:path:"
1183 f"{self.path_prefix},{parent_groups}"
1184 )
LaMont Jonesff6b1da2022-06-01 21:03:34 +00001185
Gavin Makea2e3302023-03-11 06:46:20 +00001186 # The manifestFile was specified by the user which is why we
1187 # allow include paths to point anywhere.
1188 nodes = []
1189 nodes.append(
1190 self._ParseManifestXml(
1191 self.manifestFile,
1192 self.manifestProject.worktree,
1193 parent_groups=parent_groups,
1194 restrict_includes=False,
1195 )
1196 )
LaMont Jonescc879a92021-11-18 22:40:18 +00001197
Gavin Makea2e3302023-03-11 06:46:20 +00001198 if self._load_local_manifests and self.local_manifests:
1199 try:
1200 for local_file in sorted(
1201 platform_utils.listdir(self.local_manifests)
1202 ):
1203 if local_file.endswith(".xml"):
1204 local = os.path.join(
1205 self.local_manifests, local_file
1206 )
1207 # Since local manifests are entirely managed by
1208 # the user, allow them to point anywhere the
1209 # user wants.
1210 local_group = (
1211 f"{LOCAL_MANIFEST_GROUP_PREFIX}:"
1212 f"{local_file[:-4]}"
1213 )
1214 nodes.append(
1215 self._ParseManifestXml(
1216 local,
1217 self.subdir,
1218 parent_groups=(
1219 f"{local_group},{parent_groups}"
1220 ),
1221 restrict_includes=False,
1222 )
1223 )
1224 except OSError:
1225 pass
Raman Tenneti080877e2021-03-09 15:19:06 -08001226
Gavin Makea2e3302023-03-11 06:46:20 +00001227 try:
1228 self._ParseManifest(nodes)
1229 except ManifestParseError as e:
1230 # There was a problem parsing, unload ourselves in case they
1231 # catch this error and try again later, we will show the
1232 # correct error
1233 self.Unload()
1234 raise e
Raman Tenneti080877e2021-03-09 15:19:06 -08001235
Gavin Makea2e3302023-03-11 06:46:20 +00001236 if self.IsMirror:
1237 self._AddMetaProjectMirror(self.repoProject)
1238 self._AddMetaProjectMirror(self.manifestProject)
LaMont Jonesa2ff20d2022-04-07 16:49:06 +00001239
Gavin Makea2e3302023-03-11 06:46:20 +00001240 self._loaded = True
1241 finally:
1242 if override:
1243 self.manifestFile = savedManifestFile
LaMont Jonesa2ff20d2022-04-07 16:49:06 +00001244
Gavin Makea2e3302023-03-11 06:46:20 +00001245 # Now that we have loaded this manifest, load any submanifests as
1246 # well. We need to do this after self._loaded is set to avoid
1247 # looping.
1248 for name in self._submanifests:
1249 tree = self._submanifests[name]
1250 tree.ToSubmanifestSpec()
1251 present = os.path.exists(
1252 os.path.join(self.subdir, MANIFEST_FILE_NAME)
1253 )
1254 if present and tree.present and not tree.repo_client:
1255 if initial_client and initial_client.topdir == self.topdir:
1256 tree.repo_client = self
1257 tree.present = present
1258 elif not os.path.exists(self.subdir):
1259 tree.present = False
1260 if present and tree.present:
1261 tree.repo_client._Load(
1262 initial_client=initial_client,
1263 submanifest_depth=submanifest_depth + 1,
1264 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001265
Gavin Makea2e3302023-03-11 06:46:20 +00001266 def _ParseManifestXml(
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001267 self,
1268 path,
1269 include_root,
1270 parent_groups="",
1271 restrict_includes=True,
1272 parent_node=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001273 ):
1274 """Parse a manifest XML and return the computed nodes.
LaMont Jonesa2ff20d2022-04-07 16:49:06 +00001275
Gavin Makea2e3302023-03-11 06:46:20 +00001276 Args:
1277 path: The XML file to read & parse.
1278 include_root: The path to interpret include "name"s relative to.
1279 parent_groups: The groups to apply to this projects.
1280 restrict_includes: Whether to constrain the "name" attribute of
1281 includes.
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001282 parent_node: The parent include node, to apply attribute to this
1283 projects.
LaMont Jonescc879a92021-11-18 22:40:18 +00001284
Gavin Makea2e3302023-03-11 06:46:20 +00001285 Returns:
1286 List of XML nodes.
1287 """
1288 try:
1289 root = xml.dom.minidom.parse(path)
1290 except (OSError, xml.parsers.expat.ExpatError) as e:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001291 raise ManifestParseError(f"error parsing manifest {path}: {e}")
David Pursehouse2d5a0df2012-11-13 02:50:36 +09001292
Gavin Makea2e3302023-03-11 06:46:20 +00001293 if not root or not root.childNodes:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001294 raise ManifestParseError(f"no root node in {path}")
Shawn O. Pearce5cc66792008-10-23 16:19:27 -07001295
Gavin Makea2e3302023-03-11 06:46:20 +00001296 for manifest in root.childNodes:
Chris Allen7393f6b2023-10-20 16:35:39 +01001297 if (
1298 manifest.nodeType == manifest.ELEMENT_NODE
1299 and manifest.nodeName == "manifest"
1300 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001301 break
1302 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001303 raise ManifestParseError(f"no <manifest> in {path}")
Shawn O. Pearcee284ad12008-11-04 07:37:10 -08001304
LaMont Jonesb90a4222022-04-14 15:00:09 +00001305 nodes = []
Gavin Makea2e3302023-03-11 06:46:20 +00001306 for node in manifest.childNodes:
1307 if node.nodeName == "include":
1308 name = self._reqatt(node, "name")
1309 if restrict_includes:
1310 msg = self._CheckLocalPath(name)
1311 if msg:
1312 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001313 f'<include> invalid "name": {name}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001314 )
1315 include_groups = ""
1316 if parent_groups:
1317 include_groups = parent_groups
1318 if node.hasAttribute("groups"):
1319 include_groups = (
1320 node.getAttribute("groups") + "," + include_groups
1321 )
1322 fp = os.path.join(include_root, name)
1323 if not os.path.isfile(fp):
1324 raise ManifestParseError(
1325 "include [%s/]%s doesn't exist or isn't a file"
1326 % (include_root, name)
1327 )
1328 try:
1329 nodes.extend(
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001330 self._ParseManifestXml(
1331 fp, include_root, include_groups, parent_node=node
1332 )
Gavin Makea2e3302023-03-11 06:46:20 +00001333 )
1334 # should isolate this to the exact exception, but that's
1335 # tricky. actual parsing implementation may vary.
1336 except (
1337 KeyboardInterrupt,
1338 RuntimeError,
1339 SystemExit,
1340 ManifestParseError,
1341 ):
1342 raise
1343 except Exception as e:
1344 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001345 f"failed parsing included manifest {name}: {e}"
Gavin Makea2e3302023-03-11 06:46:20 +00001346 )
1347 else:
1348 if parent_groups and node.nodeName == "project":
1349 nodeGroups = parent_groups
1350 if node.hasAttribute("groups"):
1351 nodeGroups = (
1352 node.getAttribute("groups") + "," + nodeGroups
1353 )
1354 node.setAttribute("groups", nodeGroups)
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001355 if (
1356 parent_node
1357 and node.nodeName == "project"
1358 and not node.hasAttribute("revision")
1359 ):
1360 node.setAttribute(
1361 "revision", parent_node.getAttribute("revision")
1362 )
Gavin Makea2e3302023-03-11 06:46:20 +00001363 nodes.append(node)
1364 return nodes
LaMont Jonesb90a4222022-04-14 15:00:09 +00001365
Gavin Makea2e3302023-03-11 06:46:20 +00001366 def _ParseManifest(self, node_list):
1367 for node in itertools.chain(*node_list):
1368 if node.nodeName == "remote":
1369 remote = self._ParseRemote(node)
1370 if remote:
1371 if remote.name in self._remotes:
1372 if remote != self._remotes[remote.name]:
1373 raise ManifestParseError(
1374 "remote %s already exists with different "
1375 "attributes" % (remote.name)
1376 )
1377 else:
1378 self._remotes[remote.name] = remote
LaMont Jonesb90a4222022-04-14 15:00:09 +00001379
Gavin Makea2e3302023-03-11 06:46:20 +00001380 for node in itertools.chain(*node_list):
1381 if node.nodeName == "default":
1382 new_default = self._ParseDefault(node)
1383 emptyDefault = (
1384 not node.hasAttributes() and not node.hasChildNodes()
1385 )
1386 if self._default is None:
1387 self._default = new_default
1388 elif not emptyDefault and new_default != self._default:
1389 raise ManifestParseError(
1390 "duplicate default in %s" % (self.manifestFile)
1391 )
LaMont Jonesb90a4222022-04-14 15:00:09 +00001392
Julien Campergue74879922013-10-09 14:38:46 +02001393 if self._default is None:
Gavin Makea2e3302023-03-11 06:46:20 +00001394 self._default = _Default()
Julien Campergue74879922013-10-09 14:38:46 +02001395
Gavin Makea2e3302023-03-11 06:46:20 +00001396 submanifest_paths = set()
1397 for node in itertools.chain(*node_list):
1398 if node.nodeName == "submanifest":
1399 submanifest = self._ParseSubmanifest(node)
1400 if submanifest:
1401 if submanifest.name in self._submanifests:
1402 if submanifest != self._submanifests[submanifest.name]:
1403 raise ManifestParseError(
1404 "submanifest %s already exists with different "
1405 "attributes" % (submanifest.name)
1406 )
1407 else:
1408 self._submanifests[submanifest.name] = submanifest
1409 submanifest_paths.add(submanifest.relpath)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001410
Gavin Makea2e3302023-03-11 06:46:20 +00001411 for node in itertools.chain(*node_list):
1412 if node.nodeName == "notice":
1413 if self._notice is not None:
1414 raise ManifestParseError(
1415 "duplicate notice in %s" % (self.manifestFile)
1416 )
1417 self._notice = self._ParseNotice(node)
LaMont Jonescc879a92021-11-18 22:40:18 +00001418
Gavin Makea2e3302023-03-11 06:46:20 +00001419 for node in itertools.chain(*node_list):
1420 if node.nodeName == "manifest-server":
1421 url = self._reqatt(node, "url")
1422 if self._manifest_server is not None:
1423 raise ManifestParseError(
1424 "duplicate manifest-server in %s" % (self.manifestFile)
1425 )
1426 self._manifest_server = url
Doug Anderson2b8db3c2010-11-01 15:08:06 -07001427
Gavin Makea2e3302023-03-11 06:46:20 +00001428 def recursively_add_projects(project):
1429 projects = self._projects.setdefault(project.name, [])
1430 if project.relpath is None:
1431 raise ManifestParseError(
1432 "missing path for %s in %s"
1433 % (project.name, self.manifestFile)
1434 )
1435 if project.relpath in self._paths:
1436 raise ManifestParseError(
1437 "duplicate path %s in %s"
1438 % (project.relpath, self.manifestFile)
1439 )
1440 for tree in submanifest_paths:
1441 if project.relpath.startswith(tree):
1442 raise ManifestParseError(
1443 "project %s conflicts with submanifest path %s"
1444 % (project.relpath, tree)
1445 )
1446 self._paths[project.relpath] = project
1447 projects.append(project)
1448 for subproject in project.subprojects:
1449 recursively_add_projects(subproject)
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -07001450
Gavin Makea2e3302023-03-11 06:46:20 +00001451 repo_hooks_project = None
1452 enabled_repo_hooks = None
1453 for node in itertools.chain(*node_list):
1454 if node.nodeName == "project":
1455 project = self._ParseProject(node)
1456 recursively_add_projects(project)
1457 if node.nodeName == "extend-project":
1458 name = self._reqatt(node, "name")
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001459
Gavin Makea2e3302023-03-11 06:46:20 +00001460 if name not in self._projects:
1461 raise ManifestParseError(
1462 "extend-project element specifies non-existent "
1463 "project: %s" % name
1464 )
1465
1466 path = node.getAttribute("path")
1467 dest_path = node.getAttribute("dest-path")
1468 groups = node.getAttribute("groups")
1469 if groups:
1470 groups = self._ParseList(groups)
1471 revision = node.getAttribute("revision")
1472 remote_name = node.getAttribute("remote")
1473 if not remote_name:
1474 remote = self._default.remote
1475 else:
1476 remote = self._get_remote(node)
1477 dest_branch = node.getAttribute("dest-branch")
1478 upstream = node.getAttribute("upstream")
1479
1480 named_projects = self._projects[name]
1481 if dest_path and not path and len(named_projects) > 1:
1482 raise ManifestParseError(
1483 "extend-project cannot use dest-path when "
1484 "matching multiple projects: %s" % name
1485 )
1486 for p in self._projects[name]:
1487 if path and p.relpath != path:
1488 continue
1489 if groups:
1490 p.groups.extend(groups)
1491 if revision:
1492 p.SetRevision(revision)
1493
1494 if remote_name:
1495 p.remote = remote.ToRemoteSpec(name)
1496 if dest_branch:
1497 p.dest_branch = dest_branch
1498 if upstream:
1499 p.upstream = upstream
1500
1501 if dest_path:
1502 del self._paths[p.relpath]
1503 (
1504 relpath,
1505 worktree,
1506 gitdir,
1507 objdir,
1508 _,
1509 ) = self.GetProjectPaths(name, dest_path, remote.name)
1510 p.UpdatePaths(relpath, worktree, gitdir, objdir)
1511 self._paths[p.relpath] = p
1512
1513 if node.nodeName == "repo-hooks":
1514 # Only one project can be the hooks project
1515 if repo_hooks_project is not None:
1516 raise ManifestParseError(
1517 "duplicate repo-hooks in %s" % (self.manifestFile)
1518 )
1519
1520 # Get the name of the project and the (space-separated) list of
1521 # enabled.
1522 repo_hooks_project = self._reqatt(node, "in-project")
1523 enabled_repo_hooks = self._ParseList(
1524 self._reqatt(node, "enabled-list")
1525 )
1526 if node.nodeName == "superproject":
1527 name = self._reqatt(node, "name")
1528 # There can only be one superproject.
1529 if self._superproject:
1530 raise ManifestParseError(
1531 "duplicate superproject in %s" % (self.manifestFile)
1532 )
1533 remote_name = node.getAttribute("remote")
1534 if not remote_name:
1535 remote = self._default.remote
1536 else:
1537 remote = self._get_remote(node)
1538 if remote is None:
1539 raise ManifestParseError(
1540 "no remote for superproject %s within %s"
1541 % (name, self.manifestFile)
1542 )
1543 revision = node.getAttribute("revision") or remote.revision
1544 if not revision:
1545 revision = self._default.revisionExpr
1546 if not revision:
1547 raise ManifestParseError(
1548 "no revision for superproject %s within %s"
1549 % (name, self.manifestFile)
1550 )
1551 self._superproject = Superproject(
1552 self,
1553 name=name,
1554 remote=remote.ToRemoteSpec(name),
1555 revision=revision,
1556 )
1557 if node.nodeName == "contactinfo":
1558 bugurl = self._reqatt(node, "bugurl")
1559 # This element can be repeated, later entries will clobber
1560 # earlier ones.
1561 self._contactinfo = ContactInfo(bugurl)
1562
1563 if node.nodeName == "remove-project":
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001564 name = node.getAttribute("name")
1565 path = node.getAttribute("path")
Gavin Makea2e3302023-03-11 06:46:20 +00001566
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001567 # Name or path needed.
1568 if not name and not path:
1569 raise ManifestParseError(
1570 "remove-project must have name and/or path"
1571 )
Gavin Makea2e3302023-03-11 06:46:20 +00001572
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001573 removed_project = ""
1574
1575 # Find and remove projects based on name and/or path.
1576 for projname, projects in list(self._projects.items()):
1577 for p in projects:
1578 if name == projname and not path:
1579 del self._paths[p.relpath]
1580 if not removed_project:
1581 del self._projects[name]
1582 removed_project = name
1583 elif path == p.relpath and (
1584 name == projname or not name
1585 ):
1586 self._projects[projname].remove(p)
1587 del self._paths[p.relpath]
1588 removed_project = p.name
1589
1590 # If the manifest removes the hooks project, treat it as if
1591 # it deleted the repo-hooks element too.
1592 if (
1593 removed_project
1594 and removed_project not in self._projects
1595 and repo_hooks_project == removed_project
1596 ):
1597 repo_hooks_project = None
1598
1599 if not removed_project and not XmlBool(node, "optional", False):
Gavin Makea2e3302023-03-11 06:46:20 +00001600 raise ManifestParseError(
1601 "remove-project element specifies non-existent "
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001602 "project: %s" % node.toxml()
Gavin Makea2e3302023-03-11 06:46:20 +00001603 )
1604
1605 # Store repo hooks project information.
1606 if repo_hooks_project:
1607 # Store a reference to the Project.
1608 try:
1609 repo_hooks_projects = self._projects[repo_hooks_project]
1610 except KeyError:
1611 raise ManifestParseError(
1612 "project %s not found for repo-hooks" % (repo_hooks_project)
1613 )
1614
1615 if len(repo_hooks_projects) != 1:
1616 raise ManifestParseError(
1617 "internal error parsing repo-hooks in %s"
1618 % (self.manifestFile)
1619 )
1620 self._repo_hooks_project = repo_hooks_projects[0]
1621 # Store the enabled hooks in the Project object.
1622 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
1623
1624 def _AddMetaProjectMirror(self, m):
1625 name = None
1626 m_url = m.GetRemote().url
1627 if m_url.endswith("/.git"):
1628 raise ManifestParseError("refusing to mirror %s" % m_url)
1629
1630 if self._default and self._default.remote:
1631 url = self._default.remote.resolvedFetchUrl
1632 if not url.endswith("/"):
1633 url += "/"
1634 if m_url.startswith(url):
1635 remote = self._default.remote
1636 name = m_url[len(url) :]
1637
1638 if name is None:
1639 s = m_url.rindex("/") + 1
1640 manifestUrl = self.manifestProject.config.GetString(
1641 "remote.origin.url"
1642 )
1643 remote = _XmlRemote(
1644 "origin", fetch=m_url[:s], manifestUrl=manifestUrl
1645 )
1646 name = m_url[s:]
1647
1648 if name.endswith(".git"):
1649 name = name[:-4]
Josh Triplett884a3872014-06-12 14:57:29 -07001650
1651 if name not in self._projects:
Gavin Makea2e3302023-03-11 06:46:20 +00001652 m.PreSync()
1653 gitdir = os.path.join(self.topdir, "%s.git" % name)
1654 project = Project(
1655 manifest=self,
1656 name=name,
1657 remote=remote.ToRemoteSpec(name),
1658 gitdir=gitdir,
1659 objdir=gitdir,
1660 worktree=None,
1661 relpath=name or None,
1662 revisionExpr=m.revisionExpr,
1663 revisionId=None,
1664 )
1665 self._projects[project.name] = [project]
1666 self._paths[project.relpath] = project
Josh Triplett884a3872014-06-12 14:57:29 -07001667
Gavin Makea2e3302023-03-11 06:46:20 +00001668 def _ParseRemote(self, node):
1669 """
1670 reads a <remote> element from the manifest file
1671 """
1672 name = self._reqatt(node, "name")
1673 alias = node.getAttribute("alias")
1674 if alias == "":
1675 alias = None
1676 fetch = self._reqatt(node, "fetch")
1677 pushUrl = node.getAttribute("pushurl")
1678 if pushUrl == "":
1679 pushUrl = None
1680 review = node.getAttribute("review")
1681 if review == "":
1682 review = None
1683 revision = node.getAttribute("revision")
1684 if revision == "":
1685 revision = None
1686 manifestUrl = self.manifestProject.config.GetString("remote.origin.url")
1687
1688 remote = _XmlRemote(
1689 name, alias, fetch, pushUrl, manifestUrl, review, revision
1690 )
1691
1692 for n in node.childNodes:
1693 if n.nodeName == "annotation":
1694 self._ParseAnnotation(remote, n)
1695
1696 return remote
1697
1698 def _ParseDefault(self, node):
1699 """
1700 reads a <default> element from the manifest file
1701 """
1702 d = _Default()
1703 d.remote = self._get_remote(node)
1704 d.revisionExpr = node.getAttribute("revision")
1705 if d.revisionExpr == "":
1706 d.revisionExpr = None
1707
1708 d.destBranchExpr = node.getAttribute("dest-branch") or None
1709 d.upstreamExpr = node.getAttribute("upstream") or None
1710
1711 d.sync_j = XmlInt(node, "sync-j", None)
1712 if d.sync_j is not None and d.sync_j <= 0:
1713 raise ManifestParseError(
1714 '%s: sync-j must be greater than 0, not "%s"'
1715 % (self.manifestFile, d.sync_j)
1716 )
1717
1718 d.sync_c = XmlBool(node, "sync-c", False)
1719 d.sync_s = XmlBool(node, "sync-s", False)
1720 d.sync_tags = XmlBool(node, "sync-tags", True)
1721 return d
1722
1723 def _ParseNotice(self, node):
1724 """
1725 reads a <notice> element from the manifest file
1726
1727 The <notice> element is distinct from other tags in the XML in that the
1728 data is conveyed between the start and end tag (it's not an
1729 empty-element tag).
1730
1731 The white space (carriage returns, indentation) for the notice element
1732 is relevant and is parsed in a way that is based on how python
1733 docstrings work. In fact, the code is remarkably similar to here:
1734 http://www.python.org/dev/peps/pep-0257/
1735 """
1736 # Get the data out of the node...
1737 notice = node.childNodes[0].data
1738
1739 # Figure out minimum indentation, skipping the first line (the same line
1740 # as the <notice> tag)...
1741 minIndent = sys.maxsize
1742 lines = notice.splitlines()
1743 for line in lines[1:]:
1744 lstrippedLine = line.lstrip()
1745 if lstrippedLine:
1746 indent = len(line) - len(lstrippedLine)
1747 minIndent = min(indent, minIndent)
1748
1749 # Strip leading / trailing blank lines and also indentation.
1750 cleanLines = [lines[0].strip()]
1751 for line in lines[1:]:
1752 cleanLines.append(line[minIndent:].rstrip())
1753
1754 # Clear completely blank lines from front and back...
1755 while cleanLines and not cleanLines[0]:
1756 del cleanLines[0]
1757 while cleanLines and not cleanLines[-1]:
1758 del cleanLines[-1]
1759
1760 return "\n".join(cleanLines)
1761
1762 def _ParseSubmanifest(self, node):
1763 """Reads a <submanifest> element from the manifest file."""
1764 name = self._reqatt(node, "name")
1765 remote = node.getAttribute("remote")
1766 if remote == "":
1767 remote = None
1768 project = node.getAttribute("project")
1769 if project == "":
1770 project = None
1771 revision = node.getAttribute("revision")
1772 if revision == "":
1773 revision = None
1774 manifestName = node.getAttribute("manifest-name")
1775 if manifestName == "":
1776 manifestName = None
1777 groups = ""
1778 if node.hasAttribute("groups"):
1779 groups = node.getAttribute("groups")
1780 groups = self._ParseList(groups)
1781 default_groups = self._ParseList(node.getAttribute("default-groups"))
1782 path = node.getAttribute("path")
1783 if path == "":
1784 path = None
1785 if revision:
1786 msg = self._CheckLocalPath(revision.split("/")[-1])
1787 if msg:
1788 raise ManifestInvalidPathError(
1789 '<submanifest> invalid "revision": %s: %s'
1790 % (revision, msg)
1791 )
1792 else:
1793 msg = self._CheckLocalPath(name)
1794 if msg:
1795 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001796 f'<submanifest> invalid "name": {name}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001797 )
LaMont Jonescc879a92021-11-18 22:40:18 +00001798 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001799 msg = self._CheckLocalPath(path)
1800 if msg:
1801 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001802 f'<submanifest> invalid "path": {path}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001803 )
Josh Triplett884a3872014-06-12 14:57:29 -07001804
Gavin Makea2e3302023-03-11 06:46:20 +00001805 submanifest = _XmlSubmanifest(
1806 name,
1807 remote,
1808 project,
1809 revision,
1810 manifestName,
1811 groups,
1812 default_groups,
1813 path,
1814 self,
1815 )
Michael Kelly2f3c3312020-07-21 19:40:38 -07001816
Gavin Makea2e3302023-03-11 06:46:20 +00001817 for n in node.childNodes:
1818 if n.nodeName == "annotation":
1819 self._ParseAnnotation(submanifest, n)
Michael Kelly2f3c3312020-07-21 19:40:38 -07001820
Gavin Makea2e3302023-03-11 06:46:20 +00001821 return submanifest
Michael Kelly37c21c22020-06-13 02:10:40 -07001822
Gavin Makea2e3302023-03-11 06:46:20 +00001823 def _JoinName(self, parent_name, name):
1824 return os.path.join(parent_name, name)
Doug Anderson37282b42011-03-04 11:54:18 -08001825
Gavin Makea2e3302023-03-11 06:46:20 +00001826 def _UnjoinName(self, parent_name, name):
1827 return os.path.relpath(name, parent_name)
1828
1829 def _ParseProject(self, node, parent=None, **extra_proj_attrs):
1830 """
1831 reads a <project> element from the manifest file
1832 """
1833 name = self._reqatt(node, "name")
1834 msg = self._CheckLocalPath(name, dir_ok=True)
1835 if msg:
1836 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001837 f'<project> invalid "name": {name}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001838 )
1839 if parent:
1840 name = self._JoinName(parent.name, name)
1841
1842 remote = self._get_remote(node)
Raman Tenneti1bb4fb22021-01-07 16:50:45 -08001843 if remote is None:
Gavin Makea2e3302023-03-11 06:46:20 +00001844 remote = self._default.remote
1845 if remote is None:
1846 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001847 f"no remote for project {name} within {self.manifestFile}"
Gavin Makea2e3302023-03-11 06:46:20 +00001848 )
Raman Tenneti993af5e2021-05-12 12:00:31 -07001849
Gavin Makea2e3302023-03-11 06:46:20 +00001850 revisionExpr = node.getAttribute("revision") or remote.revision
1851 if not revisionExpr:
1852 revisionExpr = self._default.revisionExpr
1853 if not revisionExpr:
1854 raise ManifestParseError(
1855 "no revision for project %s within %s"
1856 % (name, self.manifestFile)
1857 )
David Jamesb8433df2014-01-30 10:11:17 -08001858
Gavin Makea2e3302023-03-11 06:46:20 +00001859 path = node.getAttribute("path")
1860 if not path:
1861 path = name
Julien Camperguedd654222014-01-09 16:21:37 +01001862 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001863 # NB: The "." project is handled specially in
1864 # Project.Sync_LocalHalf.
1865 msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
1866 if msg:
1867 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001868 f'<project> invalid "path": {path}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001869 )
Julien Camperguedd654222014-01-09 16:21:37 +01001870
Gavin Makea2e3302023-03-11 06:46:20 +00001871 rebase = XmlBool(node, "rebase", True)
1872 sync_c = XmlBool(node, "sync-c", False)
1873 sync_s = XmlBool(node, "sync-s", self._default.sync_s)
1874 sync_tags = XmlBool(node, "sync-tags", self._default.sync_tags)
Julien Camperguedd654222014-01-09 16:21:37 +01001875
Gavin Makea2e3302023-03-11 06:46:20 +00001876 clone_depth = XmlInt(node, "clone-depth")
1877 if clone_depth is not None and clone_depth <= 0:
1878 raise ManifestParseError(
1879 '%s: clone-depth must be greater than 0, not "%s"'
1880 % (self.manifestFile, clone_depth)
1881 )
1882
1883 dest_branch = (
1884 node.getAttribute("dest-branch") or self._default.destBranchExpr
1885 )
1886
1887 upstream = node.getAttribute("upstream") or self._default.upstreamExpr
1888
1889 groups = ""
1890 if node.hasAttribute("groups"):
1891 groups = node.getAttribute("groups")
1892 groups = self._ParseList(groups)
1893
1894 if parent is None:
1895 (
1896 relpath,
1897 worktree,
1898 gitdir,
1899 objdir,
1900 use_git_worktrees,
1901 ) = self.GetProjectPaths(name, path, remote.name)
1902 else:
1903 use_git_worktrees = False
1904 relpath, worktree, gitdir, objdir = self.GetSubprojectPaths(
1905 parent, name, path
1906 )
1907
1908 default_groups = ["all", "name:%s" % name, "path:%s" % relpath]
1909 groups.extend(set(default_groups).difference(groups))
1910
1911 if self.IsMirror and node.hasAttribute("force-path"):
1912 if XmlBool(node, "force-path", False):
1913 gitdir = os.path.join(self.topdir, "%s.git" % path)
1914
1915 project = Project(
1916 manifest=self,
1917 name=name,
1918 remote=remote.ToRemoteSpec(name),
1919 gitdir=gitdir,
1920 objdir=objdir,
1921 worktree=worktree,
1922 relpath=relpath,
1923 revisionExpr=revisionExpr,
1924 revisionId=None,
1925 rebase=rebase,
1926 groups=groups,
1927 sync_c=sync_c,
1928 sync_s=sync_s,
1929 sync_tags=sync_tags,
1930 clone_depth=clone_depth,
1931 upstream=upstream,
1932 parent=parent,
1933 dest_branch=dest_branch,
1934 use_git_worktrees=use_git_worktrees,
1935 **extra_proj_attrs,
1936 )
1937
1938 for n in node.childNodes:
1939 if n.nodeName == "copyfile":
1940 self._ParseCopyFile(project, n)
1941 if n.nodeName == "linkfile":
1942 self._ParseLinkFile(project, n)
1943 if n.nodeName == "annotation":
1944 self._ParseAnnotation(project, n)
1945 if n.nodeName == "project":
1946 project.subprojects.append(
1947 self._ParseProject(n, parent=project)
1948 )
1949
1950 return project
1951
1952 def GetProjectPaths(self, name, path, remote):
1953 """Return the paths for a project.
1954
1955 Args:
1956 name: a string, the name of the project.
1957 path: a string, the path of the project.
1958 remote: a string, the remote.name of the project.
1959
1960 Returns:
1961 A tuple of (relpath, worktree, gitdir, objdir, use_git_worktrees)
1962 for the project with |name| and |path|.
1963 """
1964 # The manifest entries might have trailing slashes. Normalize them to
1965 # avoid unexpected filesystem behavior since we do string concatenation
1966 # below.
1967 path = path.rstrip("/")
1968 name = name.rstrip("/")
1969 remote = remote.rstrip("/")
1970 use_git_worktrees = False
1971 use_remote_name = self.is_multimanifest
1972 relpath = path
1973 if self.IsMirror:
1974 worktree = None
1975 gitdir = os.path.join(self.topdir, "%s.git" % name)
1976 objdir = gitdir
1977 else:
1978 if use_remote_name:
1979 namepath = os.path.join(remote, f"{name}.git")
1980 else:
1981 namepath = f"{name}.git"
1982 worktree = os.path.join(self.topdir, path).replace("\\", "/")
1983 gitdir = os.path.join(self.subdir, "projects", "%s.git" % path)
1984 # We allow people to mix git worktrees & non-git worktrees for now.
1985 # This allows for in situ migration of repo clients.
1986 if os.path.exists(gitdir) or not self.UseGitWorktrees:
1987 objdir = os.path.join(self.repodir, "project-objects", namepath)
1988 else:
1989 use_git_worktrees = True
1990 gitdir = os.path.join(self.repodir, "worktrees", namepath)
1991 objdir = gitdir
1992 return relpath, worktree, gitdir, objdir, use_git_worktrees
1993
1994 def GetProjectsWithName(self, name, all_manifests=False):
1995 """All projects with |name|.
1996
1997 Args:
1998 name: a string, the name of the project.
1999 all_manifests: a boolean, if True, then all manifests are searched.
2000 If False, then only this manifest is searched.
2001
2002 Returns:
2003 A list of Project instances with name |name|.
2004 """
2005 if all_manifests:
2006 return list(
2007 itertools.chain.from_iterable(
2008 x._projects.get(name, []) for x in self.all_manifests
2009 )
2010 )
2011 return self._projects.get(name, [])
2012
2013 def GetSubprojectName(self, parent, submodule_path):
2014 return os.path.join(parent.name, submodule_path)
2015
2016 def _JoinRelpath(self, parent_relpath, relpath):
2017 return os.path.join(parent_relpath, relpath)
2018
2019 def _UnjoinRelpath(self, parent_relpath, relpath):
2020 return os.path.relpath(relpath, parent_relpath)
2021
2022 def GetSubprojectPaths(self, parent, name, path):
2023 # The manifest entries might have trailing slashes. Normalize them to
2024 # avoid unexpected filesystem behavior since we do string concatenation
2025 # below.
2026 path = path.rstrip("/")
2027 name = name.rstrip("/")
2028 relpath = self._JoinRelpath(parent.relpath, path)
2029 gitdir = os.path.join(parent.gitdir, "subprojects", "%s.git" % path)
2030 objdir = os.path.join(
2031 parent.gitdir, "subproject-objects", "%s.git" % name
2032 )
2033 if self.IsMirror:
2034 worktree = None
2035 else:
2036 worktree = os.path.join(parent.worktree, path).replace("\\", "/")
2037 return relpath, worktree, gitdir, objdir
2038
2039 @staticmethod
2040 def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False):
2041 """Verify |path| is reasonable for use in filesystem paths.
2042
2043 Used with <copyfile> & <linkfile> & <project> elements.
2044
2045 This only validates the |path| in isolation: it does not check against
2046 the current filesystem state. Thus it is suitable as a first-past in a
2047 parser.
2048
2049 It enforces a number of constraints:
2050 * No empty paths.
2051 * No "~" in paths.
2052 * No Unicode codepoints that filesystems might elide when normalizing.
2053 * No relative path components like "." or "..".
2054 * No absolute paths.
2055 * No ".git" or ".repo*" path components.
2056
2057 Args:
2058 path: The path name to validate.
2059 dir_ok: Whether |path| may force a directory (e.g. end in a /).
2060 cwd_dot_ok: Whether |path| may be just ".".
2061
2062 Returns:
2063 None if |path| is OK, a failure message otherwise.
2064 """
2065 if not path:
2066 return "empty paths not allowed"
2067
2068 if "~" in path:
2069 return "~ not allowed (due to 8.3 filenames on Windows filesystems)"
2070
2071 path_codepoints = set(path)
2072
2073 # Some filesystems (like Apple's HFS+) try to normalize Unicode
2074 # codepoints which means there are alternative names for ".git". Reject
2075 # paths with these in it as there shouldn't be any reasonable need for
2076 # them here. The set of codepoints here was cribbed from jgit's
2077 # implementation:
2078 # https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
2079 BAD_CODEPOINTS = {
2080 "\u200C", # ZERO WIDTH NON-JOINER
2081 "\u200D", # ZERO WIDTH JOINER
2082 "\u200E", # LEFT-TO-RIGHT MARK
2083 "\u200F", # RIGHT-TO-LEFT MARK
2084 "\u202A", # LEFT-TO-RIGHT EMBEDDING
2085 "\u202B", # RIGHT-TO-LEFT EMBEDDING
2086 "\u202C", # POP DIRECTIONAL FORMATTING
2087 "\u202D", # LEFT-TO-RIGHT OVERRIDE
2088 "\u202E", # RIGHT-TO-LEFT OVERRIDE
2089 "\u206A", # INHIBIT SYMMETRIC SWAPPING
2090 "\u206B", # ACTIVATE SYMMETRIC SWAPPING
2091 "\u206C", # INHIBIT ARABIC FORM SHAPING
2092 "\u206D", # ACTIVATE ARABIC FORM SHAPING
2093 "\u206E", # NATIONAL DIGIT SHAPES
2094 "\u206F", # NOMINAL DIGIT SHAPES
2095 "\uFEFF", # ZERO WIDTH NO-BREAK SPACE
2096 }
2097 if BAD_CODEPOINTS & path_codepoints:
2098 # This message is more expansive than reality, but should be fine.
2099 return "Unicode combining characters not allowed"
2100
2101 # Reject newlines as there shouldn't be any legitmate use for them,
2102 # they'll be confusing to users, and they can easily break tools that
2103 # expect to be able to iterate over newline delimited lists. This even
2104 # applies to our own code like .repo/project.list.
2105 if {"\r", "\n"} & path_codepoints:
2106 return "Newlines not allowed"
2107
2108 # Assume paths might be used on case-insensitive filesystems.
2109 path = path.lower()
2110
2111 # Split up the path by its components. We can't use os.path.sep
2112 # exclusively as some platforms (like Windows) will convert / to \ and
2113 # that bypasses all our constructed logic here. Especially since
2114 # manifest authors only use / in their paths.
2115 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
2116 # Strip off trailing slashes as those only produce '' elements, and we
2117 # use parts to look for individual bad components.
2118 parts = resep.split(path.rstrip("/"))
2119
2120 # Some people use src="." to create stable links to projects. Lets
2121 # allow that but reject all other uses of "." to keep things simple.
2122 if not cwd_dot_ok or parts != ["."]:
2123 for part in set(parts):
2124 if part in {".", "..", ".git"} or part.startswith(".repo"):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002125 return f"bad component: {part}"
Gavin Makea2e3302023-03-11 06:46:20 +00002126
2127 if not dir_ok and resep.match(path[-1]):
2128 return "dirs not allowed"
2129
2130 # NB: The two abspath checks here are to handle platforms with multiple
2131 # filesystem path styles (e.g. Windows).
2132 norm = os.path.normpath(path)
2133 if (
2134 norm == ".."
2135 or (
2136 len(norm) >= 3
2137 and norm.startswith("..")
2138 and resep.match(norm[0])
2139 )
2140 or os.path.isabs(norm)
2141 or norm.startswith("/")
2142 ):
2143 return "path cannot be outside"
2144
2145 @classmethod
2146 def _ValidateFilePaths(cls, element, src, dest):
2147 """Verify |src| & |dest| are reasonable for <copyfile> & <linkfile>.
2148
2149 We verify the path independent of any filesystem state as we won't have
2150 a checkout available to compare to. i.e. This is for parsing validation
2151 purposes only.
2152
2153 We'll do full/live sanity checking before we do the actual filesystem
2154 modifications in _CopyFile/_LinkFile/etc...
2155 """
2156 # |dest| is the file we write to or symlink we create.
2157 # It is relative to the top of the repo client checkout.
2158 msg = cls._CheckLocalPath(dest)
2159 if msg:
2160 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002161 f'<{element}> invalid "dest": {dest}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00002162 )
2163
2164 # |src| is the file we read from or path we point to for symlinks.
2165 # It is relative to the top of the git project checkout.
2166 is_linkfile = element == "linkfile"
2167 msg = cls._CheckLocalPath(
2168 src, dir_ok=is_linkfile, cwd_dot_ok=is_linkfile
2169 )
2170 if msg:
2171 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002172 f'<{element}> invalid "src": {src}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00002173 )
2174
2175 def _ParseCopyFile(self, project, node):
2176 src = self._reqatt(node, "src")
2177 dest = self._reqatt(node, "dest")
2178 if not self.IsMirror:
2179 # src is project relative;
2180 # dest is relative to the top of the tree.
2181 # We only validate paths if we actually plan to process them.
2182 self._ValidateFilePaths("copyfile", src, dest)
2183 project.AddCopyFile(src, dest, self.topdir)
2184
2185 def _ParseLinkFile(self, project, node):
2186 src = self._reqatt(node, "src")
2187 dest = self._reqatt(node, "dest")
2188 if not self.IsMirror:
2189 # src is project relative;
2190 # dest is relative to the top of the tree.
2191 # We only validate paths if we actually plan to process them.
2192 self._ValidateFilePaths("linkfile", src, dest)
2193 project.AddLinkFile(src, dest, self.topdir)
2194
2195 def _ParseAnnotation(self, element, node):
2196 name = self._reqatt(node, "name")
2197 value = self._reqatt(node, "value")
2198 try:
2199 keep = self._reqatt(node, "keep").lower()
2200 except ManifestParseError:
2201 keep = "true"
2202 if keep != "true" and keep != "false":
2203 raise ManifestParseError(
2204 'optional "keep" attribute must be ' '"true" or "false"'
2205 )
2206 element.AddAnnotation(name, value, keep)
2207
2208 def _get_remote(self, node):
2209 name = node.getAttribute("remote")
2210 if not name:
2211 return None
2212
2213 v = self._remotes.get(name)
2214 if not v:
2215 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002216 f"remote {name} not defined in {self.manifestFile}"
Gavin Makea2e3302023-03-11 06:46:20 +00002217 )
2218 return v
2219
2220 def _reqatt(self, node, attname):
2221 """
2222 reads a required attribute from the node.
2223 """
2224 v = node.getAttribute(attname)
2225 if not v:
2226 raise ManifestParseError(
2227 "no %s in <%s> within %s"
2228 % (attname, node.nodeName, self.manifestFile)
2229 )
2230 return v
2231
2232 def projectsDiff(self, manifest):
2233 """return the projects differences between two manifests.
2234
2235 The diff will be from self to given manifest.
2236
2237 """
2238 fromProjects = self.paths
2239 toProjects = manifest.paths
2240
2241 fromKeys = sorted(fromProjects.keys())
Sylvain25d6c7c2023-08-19 23:21:49 +02002242 toKeys = set(toProjects.keys())
Gavin Makea2e3302023-03-11 06:46:20 +00002243
2244 diff = {
2245 "added": [],
2246 "removed": [],
2247 "missing": [],
2248 "changed": [],
2249 "unreachable": [],
2250 }
2251
2252 for proj in fromKeys:
Sylvain25d6c7c2023-08-19 23:21:49 +02002253 fromProj = fromProjects[proj]
Gavin Makea2e3302023-03-11 06:46:20 +00002254 if proj not in toKeys:
Sylvain25d6c7c2023-08-19 23:21:49 +02002255 diff["removed"].append(fromProj)
2256 elif not fromProj.Exists:
Gavin Makea2e3302023-03-11 06:46:20 +00002257 diff["missing"].append(toProjects[proj])
2258 toKeys.remove(proj)
2259 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002260 toProj = toProjects[proj]
2261 try:
2262 fromRevId = fromProj.GetCommitRevisionId()
2263 toRevId = toProj.GetCommitRevisionId()
2264 except ManifestInvalidRevisionError:
2265 diff["unreachable"].append((fromProj, toProj))
2266 else:
2267 if fromRevId != toRevId:
2268 diff["changed"].append((fromProj, toProj))
2269 toKeys.remove(proj)
2270
Sylvain25d6c7c2023-08-19 23:21:49 +02002271 diff["added"].extend(toProjects[proj] for proj in sorted(toKeys))
Gavin Makea2e3302023-03-11 06:46:20 +00002272
2273 return diff
Simran Basib9a1b732015-08-20 12:19:28 -07002274
2275
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002276class RepoClient(XmlManifest):
Gavin Makea2e3302023-03-11 06:46:20 +00002277 """Manages a repo client checkout."""
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002278
Gavin Makea2e3302023-03-11 06:46:20 +00002279 def __init__(
2280 self, repodir, manifest_file=None, submanifest_path="", **kwargs
2281 ):
2282 """Initialize.
LaMont Jonesff6b1da2022-06-01 21:03:34 +00002283
Gavin Makea2e3302023-03-11 06:46:20 +00002284 Args:
2285 repodir: Path to the .repo/ dir for holding all internal checkout
2286 state. It must be in the top directory of the repo client
2287 checkout.
2288 manifest_file: Full path to the manifest file to parse. This will
2289 usually be |repodir|/|MANIFEST_FILE_NAME|.
2290 submanifest_path: The submanifest root relative to the repo root.
2291 **kwargs: Additional keyword arguments, passed to XmlManifest.
2292 """
2293 self.isGitcClient = False
2294 submanifest_path = submanifest_path or ""
2295 if submanifest_path:
2296 self._CheckLocalPath(submanifest_path)
2297 prefix = os.path.join(repodir, SUBMANIFEST_DIR, submanifest_path)
2298 else:
2299 prefix = repodir
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002300
Gavin Makea2e3302023-03-11 06:46:20 +00002301 if os.path.exists(os.path.join(prefix, LOCAL_MANIFEST_NAME)):
2302 print(
2303 "error: %s is not supported; put local manifests in `%s` "
2304 "instead"
2305 % (
2306 LOCAL_MANIFEST_NAME,
2307 os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME),
2308 ),
2309 file=sys.stderr,
2310 )
2311 sys.exit(1)
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002312
Gavin Makea2e3302023-03-11 06:46:20 +00002313 if manifest_file is None:
2314 manifest_file = os.path.join(prefix, MANIFEST_FILE_NAME)
2315 local_manifests = os.path.abspath(
2316 os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME)
2317 )
2318 super().__init__(
2319 repodir,
2320 manifest_file,
2321 local_manifests,
2322 submanifest_path=submanifest_path,
2323 **kwargs,
2324 )
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002325
Gavin Makea2e3302023-03-11 06:46:20 +00002326 # TODO: Completely separate manifest logic out of the client.
2327 self.manifest = self