blob: 7987061d64bb71700e62d58a5222df9269a22b2c [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
Vitalii Dmitriev449b23b2023-12-18 11:25:16 +0200136 # This matches patterns like "[email protected]:foo".
137 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.
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100258 groups: a set of strings, the groups to add to all projects in the
Gavin Makea2e3302023-03-11 06:46:20 +0000259 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
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100284 self.groups = groups or set()
Gavin Makea2e3302023-03-11 06:46:20 +0000285 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,
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100307 parent_groups=groups,
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]
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100348 groups = self.groups
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."""
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100362 return ",".join(sorted(self.groups))
LaMont Jones501733c2022-04-20 16:42:32 +0000363
Gavin Makea2e3302023-03-11 06:46:20 +0000364 def GetDefaultGroupsStr(self):
365 """Returns the `default-groups` given for this submanifest."""
366 return ",".join(self.default_groups or [])
367
368 def AddAnnotation(self, name, value, keep):
369 """Add annotations to the submanifest."""
370 self.annotations.append(Annotation(name, value, keep))
LaMont Jonescc879a92021-11-18 22:40:18 +0000371
372
373class SubmanifestSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000374 """The submanifest element, with all fields expanded."""
LaMont Jonescc879a92021-11-18 22:40:18 +0000375
Gavin Makea2e3302023-03-11 06:46:20 +0000376 def __init__(self, name, manifestUrl, manifestName, revision, path, groups):
377 self.name = name
378 self.manifestUrl = manifestUrl
379 self.manifestName = manifestName
380 self.revision = revision
381 self.path = path
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100382 self.groups = groups
LaMont Jonescc879a92021-11-18 22:40:18 +0000383
384
Mike Frysingerd4aee652023-10-19 05:13:32 -0400385class XmlManifest:
Gavin Makea2e3302023-03-11 06:46:20 +0000386 """manages the repo configuration file"""
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700387
Gavin Makea2e3302023-03-11 06:46:20 +0000388 def __init__(
389 self,
390 repodir,
391 manifest_file,
392 local_manifests=None,
393 outer_client=None,
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100394 parent_groups=None,
Gavin Makea2e3302023-03-11 06:46:20 +0000395 submanifest_path="",
396 default_groups=None,
397 ):
398 """Initialize.
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -0400399
Gavin Makea2e3302023-03-11 06:46:20 +0000400 Args:
401 repodir: Path to the .repo/ dir for holding all internal checkout
402 state. It must be in the top directory of the repo client
403 checkout.
404 manifest_file: Full path to the manifest file to parse. This will
405 usually be |repodir|/|MANIFEST_FILE_NAME|.
406 local_manifests: Full path to the directory of local override
407 manifests. This will usually be
408 |repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
409 outer_client: RepoClient of the outer manifest.
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100410 parent_groups: a set of strings, the groups to apply to this
411 manifest.
Gavin Makea2e3302023-03-11 06:46:20 +0000412 submanifest_path: The submanifest root relative to the repo root.
413 default_groups: a string, the default manifest groups to use.
414 """
415 # TODO(vapier): Move this out of this class.
416 self.globalConfig = GitConfig.ForUser()
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -0400417
Gavin Makea2e3302023-03-11 06:46:20 +0000418 self.repodir = os.path.abspath(repodir)
419 self._CheckLocalPath(submanifest_path)
420 self.topdir = os.path.dirname(self.repodir)
421 if submanifest_path:
422 # This avoids a trailing os.path.sep when submanifest_path is empty.
423 self.topdir = os.path.join(self.topdir, submanifest_path)
424 if manifest_file != os.path.abspath(manifest_file):
425 raise ManifestParseError("manifest_file must be abspath")
426 self.manifestFile = manifest_file
427 if not outer_client or outer_client == self:
428 # manifestFileOverrides only exists in the outer_client's manifest,
429 # since that is the only instance left when Unload() is called on
430 # the outer manifest.
431 self.manifestFileOverrides = {}
432 self.local_manifests = local_manifests
433 self._load_local_manifests = True
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100434 self.parent_groups = parent_groups or set()
Gavin Makea2e3302023-03-11 06:46:20 +0000435 self.default_groups = default_groups
LaMont Jonescc879a92021-11-18 22:40:18 +0000436
Gavin Makea2e3302023-03-11 06:46:20 +0000437 if submanifest_path and not outer_client:
438 # If passing a submanifest_path, there must be an outer_client.
439 raise ManifestParseError(f"Bad call to {self.__class__.__name__}")
LaMont Jonescc879a92021-11-18 22:40:18 +0000440
Gavin Makea2e3302023-03-11 06:46:20 +0000441 # If self._outer_client is None, this is not a checkout that supports
442 # multi-tree.
443 self._outer_client = outer_client or self
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700444
Gavin Makea2e3302023-03-11 06:46:20 +0000445 self.repoProject = RepoProject(
446 self,
447 "repo",
448 gitdir=os.path.join(repodir, "repo/.git"),
449 worktree=os.path.join(repodir, "repo"),
450 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700451
Gavin Makea2e3302023-03-11 06:46:20 +0000452 mp = self.SubmanifestProject(self.path_prefix)
453 self.manifestProject = mp
Mike Frysinger979d5bd2020-02-09 02:28:34 -0500454
Gavin Makea2e3302023-03-11 06:46:20 +0000455 # This is a bit hacky, but we're in a chicken & egg situation: all the
456 # normal repo settings live in the manifestProject which we just setup
457 # above, so we couldn't easily query before that. We assume Project()
458 # init doesn't care if this changes afterwards.
459 if os.path.exists(mp.gitdir) and mp.use_worktree:
460 mp.use_git_worktrees = True
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700461
Gavin Makea2e3302023-03-11 06:46:20 +0000462 self.Unload()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700463
Gavin Makea2e3302023-03-11 06:46:20 +0000464 def Override(self, name, load_local_manifests=True):
465 """Use a different manifest, just for the current instantiation."""
466 path = None
Basil Gelloc7453502018-05-25 20:23:52 +0300467
Gavin Makea2e3302023-03-11 06:46:20 +0000468 # Look for a manifest by path in the filesystem (including the cwd).
469 if not load_local_manifests:
470 local_path = os.path.abspath(name)
471 if os.path.isfile(local_path):
472 path = local_path
Basil Gelloc7453502018-05-25 20:23:52 +0300473
Gavin Makea2e3302023-03-11 06:46:20 +0000474 # Look for manifests by name from the manifests repo.
475 if path is None:
476 path = os.path.join(self.manifestProject.worktree, name)
477 if not os.path.isfile(path):
478 raise ManifestParseError("manifest %s not found" % name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700479
Gavin Makea2e3302023-03-11 06:46:20 +0000480 self._load_local_manifests = load_local_manifests
481 self._outer_client.manifestFileOverrides[self.path_prefix] = path
482 self.Unload()
483 self._Load()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700484
Gavin Makea2e3302023-03-11 06:46:20 +0000485 def Link(self, name):
486 """Update the repo metadata to use a different manifest."""
487 self.Override(name)
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700488
Gavin Makea2e3302023-03-11 06:46:20 +0000489 # Old versions of repo would generate symlinks we need to clean up.
490 platform_utils.remove(self.manifestFile, missing_ok=True)
491 # This file is interpreted as if it existed inside the manifest repo.
492 # That allows us to use <include> with the relative file name.
493 with open(self.manifestFile, "w") as fp:
494 fp.write(
495 """<?xml version="1.0" encoding="UTF-8"?>
Mike Frysingera269b1c2020-02-21 00:49:41 -0500496<!--
497DO NOT EDIT THIS FILE! It is generated by repo and changes will be discarded.
498If you want to use a different manifest, use `repo init -m <file>` instead.
499
500If you want to customize your checkout by overriding manifest settings, use
501the local_manifests/ directory instead.
502
503For more information on repo manifests, check out:
504https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
505-->
506<manifest>
507 <include name="%s" />
508</manifest>
Gavin Makea2e3302023-03-11 06:46:20 +0000509"""
510 % (name,)
511 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700512
Gavin Makea2e3302023-03-11 06:46:20 +0000513 def _RemoteToXml(self, r, doc, root):
514 e = doc.createElement("remote")
515 root.appendChild(e)
516 e.setAttribute("name", r.name)
517 e.setAttribute("fetch", r.fetchUrl)
518 if r.pushUrl is not None:
519 e.setAttribute("pushurl", r.pushUrl)
520 if r.remoteAlias is not None:
521 e.setAttribute("alias", r.remoteAlias)
522 if r.reviewUrl is not None:
523 e.setAttribute("review", r.reviewUrl)
524 if r.revision is not None:
525 e.setAttribute("revision", r.revision)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800526
Gavin Makea2e3302023-03-11 06:46:20 +0000527 for a in r.annotations:
528 if a.keep == "true":
529 ae = doc.createElement("annotation")
530 ae.setAttribute("name", a.name)
531 ae.setAttribute("value", a.value)
532 e.appendChild(ae)
Jack Neus6ea0cae2021-07-20 20:52:33 +0000533
Gavin Makea2e3302023-03-11 06:46:20 +0000534 def _SubmanifestToXml(self, r, doc, root):
535 """Generate XML <submanifest/> node."""
536 e = doc.createElement("submanifest")
537 root.appendChild(e)
538 e.setAttribute("name", r.name)
539 if r.remote is not None:
540 e.setAttribute("remote", r.remote)
541 if r.project is not None:
542 e.setAttribute("project", r.project)
543 if r.manifestName is not None:
544 e.setAttribute("manifest-name", r.manifestName)
545 if r.revision is not None:
546 e.setAttribute("revision", r.revision)
547 if r.path is not None:
548 e.setAttribute("path", r.path)
549 if r.groups:
550 e.setAttribute("groups", r.GetGroupsStr())
551 if r.default_groups:
552 e.setAttribute("default-groups", r.GetDefaultGroupsStr())
LaMont Jonescc879a92021-11-18 22:40:18 +0000553
Gavin Makea2e3302023-03-11 06:46:20 +0000554 for a in r.annotations:
555 if a.keep == "true":
556 ae = doc.createElement("annotation")
557 ae.setAttribute("name", a.name)
558 ae.setAttribute("value", a.value)
559 e.appendChild(ae)
LaMont Jonescc879a92021-11-18 22:40:18 +0000560
Gavin Makea2e3302023-03-11 06:46:20 +0000561 def _ParseList(self, field):
562 """Parse fields that contain flattened lists.
Mike Frysinger51e39d52020-12-04 05:32:06 -0500563
Gavin Makea2e3302023-03-11 06:46:20 +0000564 These are whitespace & comma separated. Empty elements will be
565 discarded.
566 """
567 return [x for x in re.split(r"[,\s]+", field) if x]
Josh Triplett884a3872014-06-12 14:57:29 -0700568
Peter Kjellerstedt75773b82025-11-08 02:36:56 +0100569 def _ParseSet(self, field):
570 """Parse fields that contain flattened sets.
571
572 These are whitespace & comma separated. Empty elements will be
573 discarded.
574 """
575 return set(self._ParseList(field))
576
Gavin Makea2e3302023-03-11 06:46:20 +0000577 def ToXml(
578 self,
579 peg_rev=False,
580 peg_rev_upstream=True,
581 peg_rev_dest_branch=True,
Peter Kjellerstedt91ec9982025-11-18 20:05:57 +0100582 filter_groups=None,
Gavin Makea2e3302023-03-11 06:46:20 +0000583 omit_local=False,
584 ):
585 """Return the current manifest XML."""
586 mp = self.manifestProject
Colin Cross5acde752012-03-28 20:15:45 -0700587
Peter Kjellerstedt91ec9982025-11-18 20:05:57 +0100588 if filter_groups is None:
589 filter_groups = mp.manifest_groups
590 if filter_groups:
591 filter_groups = self._ParseList(filter_groups)
Colin Cross5acde752012-03-28 20:15:45 -0700592
Gavin Makea2e3302023-03-11 06:46:20 +0000593 doc = xml.dom.minidom.Document()
594 root = doc.createElement("manifest")
595 if self.is_submanifest:
596 root.setAttribute("path", self.path_prefix)
597 doc.appendChild(root)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800598
Gavin Makea2e3302023-03-11 06:46:20 +0000599 # Save out the notice. There's a little bit of work here to give it the
600 # right whitespace, which assumes that the notice is automatically
601 # indented by 4 by minidom.
602 if self.notice:
603 notice_element = root.appendChild(doc.createElement("notice"))
604 notice_lines = self.notice.splitlines()
605 indented_notice = (
606 "\n".join(" " * 4 + line for line in notice_lines)
607 )[4:]
608 notice_element.appendChild(doc.createTextNode(indented_notice))
Doug Anderson2b8db3c2010-11-01 15:08:06 -0700609
Gavin Makea2e3302023-03-11 06:46:20 +0000610 d = self.default
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800611
Gavin Makea2e3302023-03-11 06:46:20 +0000612 for r in sorted(self.remotes):
613 self._RemoteToXml(self.remotes[r], doc, root)
614 if self.remotes:
615 root.appendChild(doc.createTextNode(""))
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800616
Gavin Makea2e3302023-03-11 06:46:20 +0000617 have_default = False
618 e = doc.createElement("default")
619 if d.remote:
620 have_default = True
621 e.setAttribute("remote", d.remote.name)
622 if d.revisionExpr:
623 have_default = True
624 e.setAttribute("revision", d.revisionExpr)
625 if d.destBranchExpr:
626 have_default = True
627 e.setAttribute("dest-branch", d.destBranchExpr)
628 if d.upstreamExpr:
629 have_default = True
630 e.setAttribute("upstream", d.upstreamExpr)
631 if d.sync_j is not None:
632 have_default = True
633 e.setAttribute("sync-j", "%d" % d.sync_j)
634 if d.sync_c:
635 have_default = True
636 e.setAttribute("sync-c", "true")
637 if d.sync_s:
638 have_default = True
639 e.setAttribute("sync-s", "true")
640 if not d.sync_tags:
641 have_default = True
642 e.setAttribute("sync-tags", "false")
643 if have_default:
644 root.appendChild(e)
645 root.appendChild(doc.createTextNode(""))
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800646
Gavin Makea2e3302023-03-11 06:46:20 +0000647 if self._manifest_server:
648 e = doc.createElement("manifest-server")
649 e.setAttribute("url", self._manifest_server)
650 root.appendChild(e)
651 root.appendChild(doc.createTextNode(""))
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700652
Gavin Makea2e3302023-03-11 06:46:20 +0000653 for r in sorted(self.submanifests):
654 self._SubmanifestToXml(self.submanifests[r], doc, root)
655 if self.submanifests:
656 root.appendChild(doc.createTextNode(""))
LaMont Jonescc879a92021-11-18 22:40:18 +0000657
Gavin Makea2e3302023-03-11 06:46:20 +0000658 def output_projects(parent, parent_node, projects):
659 for project_name in projects:
660 for project in self._projects[project_name]:
661 output_project(parent, parent_node, project)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800662
Gavin Makea2e3302023-03-11 06:46:20 +0000663 def output_project(parent, parent_node, p):
Peter Kjellerstedt91ec9982025-11-18 20:05:57 +0100664 if not p.MatchesGroups(filter_groups):
Gavin Makea2e3302023-03-11 06:46:20 +0000665 return
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800666
Gavin Makea2e3302023-03-11 06:46:20 +0000667 if omit_local and self.IsFromLocalManifest(p):
668 return
LaMont Jonesa8cf5752022-07-15 20:31:33 +0000669
Gavin Makea2e3302023-03-11 06:46:20 +0000670 name = p.name
671 relpath = p.relpath
672 if parent:
673 name = self._UnjoinName(parent.name, name)
674 relpath = self._UnjoinRelpath(parent.relpath, relpath)
Colin Cross5acde752012-03-28 20:15:45 -0700675
Gavin Makea2e3302023-03-11 06:46:20 +0000676 e = doc.createElement("project")
677 parent_node.appendChild(e)
678 e.setAttribute("name", name)
679 if relpath != name:
680 e.setAttribute("path", relpath)
681 remoteName = None
682 if d.remote:
683 remoteName = d.remote.name
684 if not d.remote or p.remote.orig_name != remoteName:
685 remoteName = p.remote.orig_name
686 e.setAttribute("remote", remoteName)
687 if peg_rev:
688 if self.IsMirror:
689 value = p.bare_git.rev_parse(p.revisionExpr + "^0")
690 else:
691 value = p.work_git.rev_parse(HEAD + "^0")
692 e.setAttribute("revision", value)
693 if peg_rev_upstream:
694 if p.upstream:
695 e.setAttribute("upstream", p.upstream)
696 elif value != p.revisionExpr:
697 # Only save the origin if the origin is not a sha1, and
698 # the default isn't our value
699 e.setAttribute("upstream", p.revisionExpr)
700
701 if peg_rev_dest_branch:
702 if p.dest_branch:
703 e.setAttribute("dest-branch", p.dest_branch)
704 elif value != p.revisionExpr:
705 e.setAttribute("dest-branch", p.revisionExpr)
706
707 else:
708 revision = (
709 self.remotes[p.remote.orig_name].revision or d.revisionExpr
710 )
711 if not revision or revision != p.revisionExpr:
712 e.setAttribute("revision", p.revisionExpr)
713 elif p.revisionId:
714 e.setAttribute("revision", p.revisionId)
715 if p.upstream and (
716 p.upstream != p.revisionExpr or p.upstream != d.upstreamExpr
717 ):
718 e.setAttribute("upstream", p.upstream)
719
720 if p.dest_branch and p.dest_branch != d.destBranchExpr:
721 e.setAttribute("dest-branch", p.dest_branch)
722
723 for c in p.copyfiles:
724 ce = doc.createElement("copyfile")
725 ce.setAttribute("src", c.src)
726 ce.setAttribute("dest", c.dest)
727 e.appendChild(ce)
728
729 for lf in p.linkfiles:
730 le = doc.createElement("linkfile")
731 le.setAttribute("src", lf.src)
732 le.setAttribute("dest", lf.dest)
733 e.appendChild(le)
734
Peter Kjellerstedt91ec9982025-11-18 20:05:57 +0100735 groups = p.groups - {"all", f"name:{p.name}", f"path:{p.relpath}"}
736 if groups:
737 e.setAttribute("groups", ",".join(sorted(groups)))
Gavin Makea2e3302023-03-11 06:46:20 +0000738
739 for a in p.annotations:
740 if a.keep == "true":
741 ae = doc.createElement("annotation")
742 ae.setAttribute("name", a.name)
743 ae.setAttribute("value", a.value)
744 e.appendChild(ae)
745
746 if p.sync_c:
747 e.setAttribute("sync-c", "true")
748
749 if p.sync_s:
750 e.setAttribute("sync-s", "true")
751
752 if not p.sync_tags:
753 e.setAttribute("sync-tags", "false")
754
755 if p.clone_depth:
756 e.setAttribute("clone-depth", str(p.clone_depth))
757
758 self._output_manifest_project_extras(p, e)
759
760 if p.subprojects:
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545761 subprojects = {subp.name for subp in p.subprojects}
Gavin Makea2e3302023-03-11 06:46:20 +0000762 output_projects(p, e, list(sorted(subprojects)))
763
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545764 projects = {p.name for p in self._paths.values() if not p.parent}
Gavin Makea2e3302023-03-11 06:46:20 +0000765 output_projects(None, root, list(sorted(projects)))
766
767 if self._repo_hooks_project:
768 root.appendChild(doc.createTextNode(""))
769 e = doc.createElement("repo-hooks")
770 e.setAttribute("in-project", self._repo_hooks_project.name)
771 e.setAttribute(
772 "enabled-list",
773 " ".join(self._repo_hooks_project.enabled_repo_hooks),
774 )
775 root.appendChild(e)
776
777 if self._superproject:
778 root.appendChild(doc.createTextNode(""))
779 e = doc.createElement("superproject")
780 e.setAttribute("name", self._superproject.name)
781 remoteName = None
782 if d.remote:
783 remoteName = d.remote.name
784 remote = self._superproject.remote
785 if not d.remote or remote.orig_name != remoteName:
786 remoteName = remote.orig_name
787 e.setAttribute("remote", remoteName)
788 revision = remote.revision or d.revisionExpr
789 if not revision or revision != self._superproject.revision:
790 e.setAttribute("revision", self._superproject.revision)
791 root.appendChild(e)
792
793 if self._contactinfo.bugurl != Wrapper().BUG_URL:
794 root.appendChild(doc.createTextNode(""))
795 e = doc.createElement("contactinfo")
796 e.setAttribute("bugurl", self._contactinfo.bugurl)
797 root.appendChild(e)
798
799 return doc
800
801 def ToDict(self, **kwargs):
802 """Return the current manifest as a dictionary."""
803 # Elements that may only appear once.
804 SINGLE_ELEMENTS = {
805 "notice",
806 "default",
807 "manifest-server",
808 "repo-hooks",
809 "superproject",
810 "contactinfo",
811 }
812 # Elements that may be repeated.
813 MULTI_ELEMENTS = {
814 "remote",
815 "remove-project",
816 "project",
817 "extend-project",
818 "include",
819 "submanifest",
820 # These are children of 'project' nodes.
821 "annotation",
822 "project",
823 "copyfile",
824 "linkfile",
825 }
826
827 doc = self.ToXml(**kwargs)
828 ret = {}
829
830 def append_children(ret, node):
831 for child in node.childNodes:
832 if child.nodeType == xml.dom.Node.ELEMENT_NODE:
833 attrs = child.attributes
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545834 element = {
835 attrs.item(i).localName: attrs.item(i).value
Gavin Makea2e3302023-03-11 06:46:20 +0000836 for i in range(attrs.length)
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545837 }
Gavin Makea2e3302023-03-11 06:46:20 +0000838 if child.nodeName in SINGLE_ELEMENTS:
839 ret[child.nodeName] = element
840 elif child.nodeName in MULTI_ELEMENTS:
841 ret.setdefault(child.nodeName, []).append(element)
842 else:
843 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400844 f'Unhandled element "{child.nodeName}"'
Gavin Makea2e3302023-03-11 06:46:20 +0000845 )
846
847 append_children(element, child)
848
849 append_children(ret, doc.firstChild)
850
851 return ret
852
853 def Save(self, fd, **kwargs):
854 """Write the current manifest out to the given file descriptor."""
855 doc = self.ToXml(**kwargs)
856 doc.writexml(fd, "", " ", "\n", "UTF-8")
857
858 def _output_manifest_project_extras(self, p, e):
859 """Manifests can modify e if they support extra project attributes."""
860
861 @property
862 def is_multimanifest(self):
863 """Whether this is a multimanifest checkout.
864
865 This is safe to use as long as the outermost manifest XML has been
866 parsed.
867 """
868 return bool(self._outer_client._submanifests)
869
870 @property
871 def is_submanifest(self):
872 """Whether this manifest is a submanifest.
873
874 This is safe to use as long as the outermost manifest XML has been
875 parsed.
876 """
877 return self._outer_client and self._outer_client != self
878
879 @property
880 def outer_client(self):
881 """The instance of the outermost manifest client."""
882 self._Load()
883 return self._outer_client
884
885 @property
886 def all_manifests(self):
887 """Generator yielding all (sub)manifests, in depth-first order."""
888 self._Load()
889 outer = self._outer_client
890 yield outer
Jason R. Coombs8dd85212023-10-20 06:48:20 -0400891 yield from outer.all_children
Gavin Makea2e3302023-03-11 06:46:20 +0000892
893 @property
894 def all_children(self):
895 """Generator yielding all (present) child submanifests."""
896 self._Load()
897 for child in self._submanifests.values():
898 if child.repo_client:
899 yield child.repo_client
Jason R. Coombs8dd85212023-10-20 06:48:20 -0400900 yield from child.repo_client.all_children
Gavin Makea2e3302023-03-11 06:46:20 +0000901
902 @property
903 def path_prefix(self):
904 """The path of this submanifest, relative to the outermost manifest."""
905 if not self._outer_client or self == self._outer_client:
906 return ""
907 return os.path.relpath(self.topdir, self._outer_client.topdir)
908
909 @property
910 def all_paths(self):
911 """All project paths for all (sub)manifests.
912
913 See also `paths`.
914
915 Returns:
916 A dictionary of {path: Project()}. `path` is relative to the outer
917 manifest.
918 """
919 ret = {}
920 for tree in self.all_manifests:
921 prefix = tree.path_prefix
922 ret.update(
923 {os.path.join(prefix, k): v for k, v in tree.paths.items()}
924 )
925 return ret
926
927 @property
928 def all_projects(self):
929 """All projects for all (sub)manifests. See `projects`."""
930 return list(
931 itertools.chain.from_iterable(
932 x._paths.values() for x in self.all_manifests
933 )
934 )
935
936 @property
937 def paths(self):
938 """Return all paths for this manifest.
939
940 Returns:
941 A dictionary of {path: Project()}. `path` is relative to this
942 manifest.
943 """
944 self._Load()
945 return self._paths
946
947 @property
948 def projects(self):
949 """Return a list of all Projects in this manifest."""
950 self._Load()
951 return list(self._paths.values())
952
953 @property
954 def remotes(self):
955 """Return a list of remotes for this manifest."""
956 self._Load()
957 return self._remotes
958
959 @property
960 def default(self):
961 """Return default values for this manifest."""
962 self._Load()
963 return self._default
964
965 @property
966 def submanifests(self):
967 """All submanifests in this manifest."""
968 self._Load()
969 return self._submanifests
970
971 @property
972 def repo_hooks_project(self):
973 self._Load()
974 return self._repo_hooks_project
975
976 @property
977 def superproject(self):
978 self._Load()
979 return self._superproject
980
981 @property
982 def contactinfo(self):
983 self._Load()
984 return self._contactinfo
985
986 @property
987 def notice(self):
988 self._Load()
989 return self._notice
990
991 @property
992 def manifest_server(self):
993 self._Load()
994 return self._manifest_server
995
996 @property
997 def CloneBundle(self):
998 clone_bundle = self.manifestProject.clone_bundle
999 if clone_bundle is None:
1000 return False if self.manifestProject.partial_clone else True
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -08001001 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001002 return clone_bundle
Sean McAllisteraf908cb2020-04-20 08:41:58 -06001003
Gavin Makea2e3302023-03-11 06:46:20 +00001004 @property
1005 def CloneFilter(self):
1006 if self.manifestProject.partial_clone:
1007 return self.manifestProject.clone_filter
1008 return None
Sean McAllisteraf908cb2020-04-20 08:41:58 -06001009
Gavin Makea2e3302023-03-11 06:46:20 +00001010 @property
Jason Chang17833322023-05-23 13:06:55 -07001011 def CloneFilterForDepth(self):
1012 if self.manifestProject.clone_filter_for_depth:
1013 return self.manifestProject.clone_filter_for_depth
1014 return None
1015
1016 @property
Gavin Makea2e3302023-03-11 06:46:20 +00001017 def PartialCloneExclude(self):
1018 exclude = self.manifest.manifestProject.partial_clone_exclude or ""
Jason R. Coombs0bcffd82023-10-20 23:29:42 +05451019 return {x.strip() for x in exclude.split(",")}
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -08001020
Gavin Makea2e3302023-03-11 06:46:20 +00001021 def SetManifestOverride(self, path):
1022 """Override manifestFile. The caller must call Unload()"""
Mike Frysingercd391e72025-03-25 12:53:55 -04001023 self._outer_client.manifest.manifestFileOverrides[self.path_prefix] = (
1024 path
1025 )
Simon Ruggier7e59de22015-07-24 12:50:06 +02001026
Gavin Makea2e3302023-03-11 06:46:20 +00001027 @property
1028 def UseLocalManifests(self):
1029 return self._load_local_manifests
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -08001030
Gavin Makea2e3302023-03-11 06:46:20 +00001031 def SetUseLocalManifests(self, value):
1032 self._load_local_manifests = value
Jeff Hamiltone0df2322014-04-21 17:10:59 -05001033
Gavin Makea2e3302023-03-11 06:46:20 +00001034 @property
1035 def HasLocalManifests(self):
1036 return self._load_local_manifests and self.local_manifests
Colin Cross5acde752012-03-28 20:15:45 -07001037
Gavin Makea2e3302023-03-11 06:46:20 +00001038 def IsFromLocalManifest(self, project):
1039 """Is the project from a local manifest?"""
1040 return any(
1041 x.startswith(LOCAL_MANIFEST_GROUP_PREFIX) for x in project.groups
1042 )
James W. Mills24c13082012-04-12 15:04:13 -05001043
Gavin Makea2e3302023-03-11 06:46:20 +00001044 @property
1045 def IsMirror(self):
1046 return self.manifestProject.mirror
Anatol Pomazau79770d22012-04-20 14:41:59 -07001047
Gavin Makea2e3302023-03-11 06:46:20 +00001048 @property
1049 def UseGitWorktrees(self):
1050 return self.manifestProject.use_worktree
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001051
Gavin Makea2e3302023-03-11 06:46:20 +00001052 @property
1053 def IsArchive(self):
1054 return self.manifestProject.archive
YOUNG HO CHAa32c92c2018-02-14 16:57:31 +09001055
Gavin Makea2e3302023-03-11 06:46:20 +00001056 @property
1057 def HasSubmodules(self):
1058 return self.manifestProject.submodules
Dan Willemsen88409222015-08-17 15:29:10 -07001059
Gavin Makea2e3302023-03-11 06:46:20 +00001060 @property
1061 def EnableGitLfs(self):
1062 return self.manifestProject.git_lfs
Simran Basib9a1b732015-08-20 12:19:28 -07001063
Gavin Makea2e3302023-03-11 06:46:20 +00001064 def FindManifestByPath(self, path):
1065 """Returns the manifest containing path."""
1066 path = os.path.abspath(path)
1067 manifest = self._outer_client or self
1068 old = None
1069 while manifest._submanifests and manifest != old:
1070 old = manifest
1071 for name in manifest._submanifests:
1072 tree = manifest._submanifests[name]
1073 if path.startswith(tree.repo_client.manifest.topdir):
1074 manifest = tree.repo_client
1075 break
1076 return manifest
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001077
Gavin Makea2e3302023-03-11 06:46:20 +00001078 @property
1079 def subdir(self):
1080 """Returns the path for per-submanifest objects for this manifest."""
1081 return self.SubmanifestInfoDir(self.path_prefix)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001082
Gavin Makea2e3302023-03-11 06:46:20 +00001083 def SubmanifestInfoDir(self, submanifest_path, object_path=""):
1084 """Return the path to submanifest-specific info for a submanifest.
Doug Anderson37282b42011-03-04 11:54:18 -08001085
Gavin Makea2e3302023-03-11 06:46:20 +00001086 Return the full path of the directory in which to put per-manifest
1087 objects.
Raman Tenneti1bb4fb22021-01-07 16:50:45 -08001088
Gavin Makea2e3302023-03-11 06:46:20 +00001089 Args:
1090 submanifest_path: a string, the path of the submanifest, relative to
1091 the outermost topdir. If empty, then repodir is returned.
1092 object_path: a string, relative path to append to the submanifest
1093 info directory path.
1094 """
1095 if submanifest_path:
1096 return os.path.join(
1097 self.repodir, SUBMANIFEST_DIR, submanifest_path, object_path
1098 )
1099 else:
1100 return os.path.join(self.repodir, object_path)
Raman Tenneti1c3f57e2021-05-04 12:32:13 -07001101
Gavin Makea2e3302023-03-11 06:46:20 +00001102 def SubmanifestProject(self, submanifest_path):
1103 """Return a manifestProject for a submanifest."""
1104 subdir = self.SubmanifestInfoDir(submanifest_path)
1105 mp = ManifestProject(
1106 self,
1107 "manifests",
1108 gitdir=os.path.join(subdir, "manifests.git"),
1109 worktree=os.path.join(subdir, "manifests"),
1110 )
1111 return mp
Mike Frysinger23411d32020-09-02 04:31:10 -04001112
Gavin Makea2e3302023-03-11 06:46:20 +00001113 def GetDefaultGroupsStr(self, with_platform=True):
1114 """Returns the default group string to use.
Mike Frysinger23411d32020-09-02 04:31:10 -04001115
Gavin Makea2e3302023-03-11 06:46:20 +00001116 Args:
1117 with_platform: a boolean, whether to include the group for the
1118 underlying platform.
1119 """
1120 groups = ",".join(self.default_groups or ["default"])
1121 if with_platform:
1122 groups += f",platform-{platform.system().lower()}"
1123 return groups
Mike Frysinger23411d32020-09-02 04:31:10 -04001124
Peter Kjellerstedt2b6de522025-11-18 20:13:03 +01001125 def GetManifestGroupsStr(self):
Gavin Makea2e3302023-03-11 06:46:20 +00001126 """Returns the manifest group string that should be synced."""
1127 return (
1128 self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
1129 )
Mike Frysinger23411d32020-09-02 04:31:10 -04001130
Gavin Makea2e3302023-03-11 06:46:20 +00001131 def Unload(self):
1132 """Unload the manifest.
Mike Frysinger23411d32020-09-02 04:31:10 -04001133
Gavin Makea2e3302023-03-11 06:46:20 +00001134 If the manifest files have been changed since Load() was called, this
1135 will cause the new/updated manifest to be used.
Mike Frysinger23411d32020-09-02 04:31:10 -04001136
Gavin Makea2e3302023-03-11 06:46:20 +00001137 """
1138 self._loaded = False
1139 self._projects = {}
1140 self._paths = {}
1141 self._remotes = {}
1142 self._default = None
1143 self._submanifests = {}
1144 self._repo_hooks_project = None
1145 self._superproject = None
1146 self._contactinfo = ContactInfo(Wrapper().BUG_URL)
1147 self._notice = None
1148 self.branch = None
1149 self._manifest_server = None
Mike Frysinger23411d32020-09-02 04:31:10 -04001150
Gavin Makea2e3302023-03-11 06:46:20 +00001151 def Load(self):
1152 """Read the manifest into memory."""
1153 # Do not expose internal arguments.
1154 self._Load()
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -08001155
Gavin Makea2e3302023-03-11 06:46:20 +00001156 def _Load(self, initial_client=None, submanifest_depth=0):
1157 if submanifest_depth > MAX_SUBMANIFEST_DEPTH:
1158 raise ManifestParseError(
1159 "maximum submanifest depth %d exceeded." % MAX_SUBMANIFEST_DEPTH
1160 )
1161 if not self._loaded:
1162 if self._outer_client and self._outer_client != self:
1163 # This will load all clients.
1164 self._outer_client._Load(initial_client=self)
Simran Basib9a1b732015-08-20 12:19:28 -07001165
Gavin Makea2e3302023-03-11 06:46:20 +00001166 savedManifestFile = self.manifestFile
1167 override = self._outer_client.manifestFileOverrides.get(
1168 self.path_prefix
1169 )
1170 if override:
1171 self.manifestFile = override
Mike Frysinger1d00a7e2021-12-21 00:40:31 -05001172
Gavin Makea2e3302023-03-11 06:46:20 +00001173 try:
1174 m = self.manifestProject
1175 b = m.GetBranch(m.CurrentBranch).merge
1176 if b is not None and b.startswith(R_HEADS):
1177 b = b[len(R_HEADS) :]
1178 self.branch = b
LaMont Jonescc879a92021-11-18 22:40:18 +00001179
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001180 parent_groups = self.parent_groups.copy()
Gavin Makea2e3302023-03-11 06:46:20 +00001181 if self.path_prefix:
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001182 parent_groups |= {
Gavin Makea2e3302023-03-11 06:46:20 +00001183 f"{SUBMANIFEST_GROUP_PREFIX}:path:"
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001184 f"{self.path_prefix}"
1185 }
LaMont Jonesff6b1da2022-06-01 21:03:34 +00001186
Gavin Makea2e3302023-03-11 06:46:20 +00001187 # The manifestFile was specified by the user which is why we
1188 # allow include paths to point anywhere.
1189 nodes = []
1190 nodes.append(
1191 self._ParseManifestXml(
1192 self.manifestFile,
1193 self.manifestProject.worktree,
1194 parent_groups=parent_groups,
1195 restrict_includes=False,
1196 )
1197 )
LaMont Jonescc879a92021-11-18 22:40:18 +00001198
Gavin Makea2e3302023-03-11 06:46:20 +00001199 if self._load_local_manifests and self.local_manifests:
1200 try:
1201 for local_file in sorted(
1202 platform_utils.listdir(self.local_manifests)
1203 ):
1204 if local_file.endswith(".xml"):
1205 local = os.path.join(
1206 self.local_manifests, local_file
1207 )
1208 # Since local manifests are entirely managed by
1209 # the user, allow them to point anywhere the
1210 # user wants.
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001211 local_group = {
Gavin Makea2e3302023-03-11 06:46:20 +00001212 f"{LOCAL_MANIFEST_GROUP_PREFIX}:"
1213 f"{local_file[:-4]}"
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001214 }
Gavin Makea2e3302023-03-11 06:46:20 +00001215 nodes.append(
1216 self._ParseManifestXml(
1217 local,
1218 self.subdir,
1219 parent_groups=(
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001220 local_group | parent_groups
Gavin Makea2e3302023-03-11 06:46:20 +00001221 ),
1222 restrict_includes=False,
1223 )
1224 )
1225 except OSError:
1226 pass
Raman Tenneti080877e2021-03-09 15:19:06 -08001227
Gavin Makea2e3302023-03-11 06:46:20 +00001228 try:
1229 self._ParseManifest(nodes)
1230 except ManifestParseError as e:
1231 # There was a problem parsing, unload ourselves in case they
1232 # catch this error and try again later, we will show the
1233 # correct error
1234 self.Unload()
1235 raise e
Raman Tenneti080877e2021-03-09 15:19:06 -08001236
Gavin Makea2e3302023-03-11 06:46:20 +00001237 if self.IsMirror:
1238 self._AddMetaProjectMirror(self.repoProject)
1239 self._AddMetaProjectMirror(self.manifestProject)
LaMont Jonesa2ff20d2022-04-07 16:49:06 +00001240
Gavin Makea2e3302023-03-11 06:46:20 +00001241 self._loaded = True
1242 finally:
1243 if override:
1244 self.manifestFile = savedManifestFile
LaMont Jonesa2ff20d2022-04-07 16:49:06 +00001245
Gavin Makea2e3302023-03-11 06:46:20 +00001246 # Now that we have loaded this manifest, load any submanifests as
1247 # well. We need to do this after self._loaded is set to avoid
1248 # looping.
1249 for name in self._submanifests:
1250 tree = self._submanifests[name]
1251 tree.ToSubmanifestSpec()
1252 present = os.path.exists(
1253 os.path.join(self.subdir, MANIFEST_FILE_NAME)
1254 )
1255 if present and tree.present and not tree.repo_client:
1256 if initial_client and initial_client.topdir == self.topdir:
1257 tree.repo_client = self
1258 tree.present = present
1259 elif not os.path.exists(self.subdir):
1260 tree.present = False
1261 if present and tree.present:
1262 tree.repo_client._Load(
1263 initial_client=initial_client,
1264 submanifest_depth=submanifest_depth + 1,
1265 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001266
Gavin Makea2e3302023-03-11 06:46:20 +00001267 def _ParseManifestXml(
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001268 self,
1269 path,
1270 include_root,
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001271 parent_groups=None,
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001272 restrict_includes=True,
1273 parent_node=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001274 ):
1275 """Parse a manifest XML and return the computed nodes.
LaMont Jonesa2ff20d2022-04-07 16:49:06 +00001276
Gavin Makea2e3302023-03-11 06:46:20 +00001277 Args:
1278 path: The XML file to read & parse.
1279 include_root: The path to interpret include "name"s relative to.
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001280 parent_groups: The set of groups to apply to this manifest.
Gavin Makea2e3302023-03-11 06:46:20 +00001281 restrict_includes: Whether to constrain the "name" attribute of
1282 includes.
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001283 parent_node: The parent include node, to apply attributes to this
1284 manifest.
LaMont Jonescc879a92021-11-18 22:40:18 +00001285
Gavin Makea2e3302023-03-11 06:46:20 +00001286 Returns:
1287 List of XML nodes.
1288 """
1289 try:
1290 root = xml.dom.minidom.parse(path)
1291 except (OSError, xml.parsers.expat.ExpatError) as e:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001292 raise ManifestParseError(f"error parsing manifest {path}: {e}")
David Pursehouse2d5a0df2012-11-13 02:50:36 +09001293
Gavin Makea2e3302023-03-11 06:46:20 +00001294 if not root or not root.childNodes:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001295 raise ManifestParseError(f"no root node in {path}")
Shawn O. Pearce5cc66792008-10-23 16:19:27 -07001296
Gavin Makea2e3302023-03-11 06:46:20 +00001297 for manifest in root.childNodes:
Chris Allen7393f6b2023-10-20 16:35:39 +01001298 if (
1299 manifest.nodeType == manifest.ELEMENT_NODE
1300 and manifest.nodeName == "manifest"
1301 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001302 break
1303 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001304 raise ManifestParseError(f"no <manifest> in {path}")
Shawn O. Pearcee284ad12008-11-04 07:37:10 -08001305
LaMont Jonesb90a4222022-04-14 15:00:09 +00001306 nodes = []
Gavin Makea2e3302023-03-11 06:46:20 +00001307 for node in manifest.childNodes:
Peter Kjellerstedt3073a902025-11-08 06:42:53 +01001308 if (
1309 parent_node
1310 and node.nodeName in ("include", "project")
1311 and not node.hasAttribute("revision")
1312 ):
1313 node.setAttribute(
1314 "revision", parent_node.getAttribute("revision")
1315 )
Gavin Makea2e3302023-03-11 06:46:20 +00001316 if node.nodeName == "include":
1317 name = self._reqatt(node, "name")
1318 if restrict_includes:
1319 msg = self._CheckLocalPath(name)
1320 if msg:
1321 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001322 f'<include> invalid "name": {name}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001323 )
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001324 include_groups = (parent_groups or set()).copy()
Gavin Makea2e3302023-03-11 06:46:20 +00001325 if node.hasAttribute("groups"):
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001326 include_groups |= self._ParseSet(
1327 node.getAttribute("groups")
Gavin Makea2e3302023-03-11 06:46:20 +00001328 )
1329 fp = os.path.join(include_root, name)
1330 if not os.path.isfile(fp):
1331 raise ManifestParseError(
1332 "include [%s/]%s doesn't exist or isn't a file"
1333 % (include_root, name)
1334 )
1335 try:
1336 nodes.extend(
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001337 self._ParseManifestXml(
1338 fp, include_root, include_groups, parent_node=node
1339 )
Gavin Makea2e3302023-03-11 06:46:20 +00001340 )
1341 # should isolate this to the exact exception, but that's
1342 # tricky. actual parsing implementation may vary.
Erik Elmekec0615932025-04-21 08:04:27 +02001343 except (RuntimeError, ManifestParseError):
Gavin Makea2e3302023-03-11 06:46:20 +00001344 raise
1345 except Exception as e:
1346 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001347 f"failed parsing included manifest {name}: {e}"
Gavin Makea2e3302023-03-11 06:46:20 +00001348 )
1349 else:
Peter Kjellerstedt47c24b52025-11-07 23:09:57 +01001350 if parent_groups and node.nodeName in (
1351 "project",
1352 "extend-project",
1353 ):
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001354 nodeGroups = parent_groups.copy()
Gavin Makea2e3302023-03-11 06:46:20 +00001355 if node.hasAttribute("groups"):
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001356 nodeGroups |= self._ParseSet(
1357 node.getAttribute("groups")
Gavin Makea2e3302023-03-11 06:46:20 +00001358 )
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001359 node.setAttribute("groups", ",".join(sorted(nodeGroups)))
Gavin Makea2e3302023-03-11 06:46:20 +00001360 nodes.append(node)
1361 return nodes
LaMont Jonesb90a4222022-04-14 15:00:09 +00001362
Gavin Makea2e3302023-03-11 06:46:20 +00001363 def _ParseManifest(self, node_list):
1364 for node in itertools.chain(*node_list):
1365 if node.nodeName == "remote":
1366 remote = self._ParseRemote(node)
1367 if remote:
1368 if remote.name in self._remotes:
1369 if remote != self._remotes[remote.name]:
1370 raise ManifestParseError(
1371 "remote %s already exists with different "
1372 "attributes" % (remote.name)
1373 )
1374 else:
1375 self._remotes[remote.name] = remote
LaMont Jonesb90a4222022-04-14 15:00:09 +00001376
Gavin Makea2e3302023-03-11 06:46:20 +00001377 for node in itertools.chain(*node_list):
1378 if node.nodeName == "default":
1379 new_default = self._ParseDefault(node)
1380 emptyDefault = (
1381 not node.hasAttributes() and not node.hasChildNodes()
1382 )
1383 if self._default is None:
1384 self._default = new_default
1385 elif not emptyDefault and new_default != self._default:
1386 raise ManifestParseError(
1387 "duplicate default in %s" % (self.manifestFile)
1388 )
LaMont Jonesb90a4222022-04-14 15:00:09 +00001389
Julien Campergue74879922013-10-09 14:38:46 +02001390 if self._default is None:
Gavin Makea2e3302023-03-11 06:46:20 +00001391 self._default = _Default()
Julien Campergue74879922013-10-09 14:38:46 +02001392
Gavin Makea2e3302023-03-11 06:46:20 +00001393 submanifest_paths = set()
1394 for node in itertools.chain(*node_list):
1395 if node.nodeName == "submanifest":
1396 submanifest = self._ParseSubmanifest(node)
1397 if submanifest:
1398 if submanifest.name in self._submanifests:
1399 if submanifest != self._submanifests[submanifest.name]:
1400 raise ManifestParseError(
1401 "submanifest %s already exists with different "
1402 "attributes" % (submanifest.name)
1403 )
1404 else:
1405 self._submanifests[submanifest.name] = submanifest
1406 submanifest_paths.add(submanifest.relpath)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001407
Gavin Makea2e3302023-03-11 06:46:20 +00001408 for node in itertools.chain(*node_list):
1409 if node.nodeName == "notice":
1410 if self._notice is not None:
1411 raise ManifestParseError(
1412 "duplicate notice in %s" % (self.manifestFile)
1413 )
1414 self._notice = self._ParseNotice(node)
LaMont Jonescc879a92021-11-18 22:40:18 +00001415
Gavin Makea2e3302023-03-11 06:46:20 +00001416 for node in itertools.chain(*node_list):
1417 if node.nodeName == "manifest-server":
1418 url = self._reqatt(node, "url")
1419 if self._manifest_server is not None:
1420 raise ManifestParseError(
1421 "duplicate manifest-server in %s" % (self.manifestFile)
1422 )
1423 self._manifest_server = url
Doug Anderson2b8db3c2010-11-01 15:08:06 -07001424
Gavin Makea2e3302023-03-11 06:46:20 +00001425 def recursively_add_projects(project):
1426 projects = self._projects.setdefault(project.name, [])
1427 if project.relpath is None:
1428 raise ManifestParseError(
1429 "missing path for %s in %s"
1430 % (project.name, self.manifestFile)
1431 )
1432 if project.relpath in self._paths:
1433 raise ManifestParseError(
1434 "duplicate path %s in %s"
1435 % (project.relpath, self.manifestFile)
1436 )
1437 for tree in submanifest_paths:
1438 if project.relpath.startswith(tree):
1439 raise ManifestParseError(
1440 "project %s conflicts with submanifest path %s"
1441 % (project.relpath, tree)
1442 )
1443 self._paths[project.relpath] = project
1444 projects.append(project)
1445 for subproject in project.subprojects:
1446 recursively_add_projects(subproject)
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -07001447
Gavin Makea2e3302023-03-11 06:46:20 +00001448 repo_hooks_project = None
1449 enabled_repo_hooks = None
Fredrik de Groot303bd962024-09-09 15:54:57 +02001450 failed_revision_changes = []
Gavin Makea2e3302023-03-11 06:46:20 +00001451 for node in itertools.chain(*node_list):
1452 if node.nodeName == "project":
1453 project = self._ParseProject(node)
1454 recursively_add_projects(project)
1455 if node.nodeName == "extend-project":
1456 name = self._reqatt(node, "name")
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001457
Gavin Makea2e3302023-03-11 06:46:20 +00001458 if name not in self._projects:
1459 raise ManifestParseError(
1460 "extend-project element specifies non-existent "
1461 "project: %s" % name
1462 )
1463
1464 path = node.getAttribute("path")
1465 dest_path = node.getAttribute("dest-path")
1466 groups = node.getAttribute("groups")
1467 if groups:
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001468 groups = self._ParseSet(groups or "")
Gavin Makea2e3302023-03-11 06:46:20 +00001469 revision = node.getAttribute("revision")
1470 remote_name = node.getAttribute("remote")
1471 if not remote_name:
1472 remote = self._default.remote
1473 else:
1474 remote = self._get_remote(node)
1475 dest_branch = node.getAttribute("dest-branch")
1476 upstream = node.getAttribute("upstream")
Fredrik de Groot303bd962024-09-09 15:54:57 +02001477 base_revision = node.getAttribute("base-rev")
Gavin Makea2e3302023-03-11 06:46:20 +00001478
1479 named_projects = self._projects[name]
1480 if dest_path and not path and len(named_projects) > 1:
1481 raise ManifestParseError(
1482 "extend-project cannot use dest-path when "
1483 "matching multiple projects: %s" % name
1484 )
1485 for p in self._projects[name]:
1486 if path and p.relpath != path:
1487 continue
1488 if groups:
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001489 p.groups |= groups
Gavin Makea2e3302023-03-11 06:46:20 +00001490 if revision:
Fredrik de Groot303bd962024-09-09 15:54:57 +02001491 if base_revision:
1492 if p.revisionExpr != base_revision:
1493 failed_revision_changes.append(
1494 "extend-project name %s mismatch base "
1495 "%s vs revision %s"
1496 % (name, base_revision, p.revisionExpr)
1497 )
Gavin Makea2e3302023-03-11 06:46:20 +00001498 p.SetRevision(revision)
1499
1500 if remote_name:
1501 p.remote = remote.ToRemoteSpec(name)
1502 if dest_branch:
1503 p.dest_branch = dest_branch
1504 if upstream:
1505 p.upstream = upstream
1506
1507 if dest_path:
1508 del self._paths[p.relpath]
1509 (
1510 relpath,
1511 worktree,
1512 gitdir,
1513 objdir,
1514 _,
1515 ) = self.GetProjectPaths(name, dest_path, remote.name)
1516 p.UpdatePaths(relpath, worktree, gitdir, objdir)
1517 self._paths[p.relpath] = p
1518
Peter Kjellerstedt4ab22842025-10-16 20:29:28 +02001519 for n in node.childNodes:
1520 if n.nodeName == "copyfile":
1521 self._ParseCopyFile(p, n)
1522 elif n.nodeName == "linkfile":
1523 self._ParseLinkFile(p, n)
1524 elif n.nodeName == "annotation":
1525 self._ParseAnnotation(p, n)
1526
Gavin Makea2e3302023-03-11 06:46:20 +00001527 if node.nodeName == "repo-hooks":
1528 # Only one project can be the hooks project
1529 if repo_hooks_project is not None:
1530 raise ManifestParseError(
1531 "duplicate repo-hooks in %s" % (self.manifestFile)
1532 )
1533
1534 # Get the name of the project and the (space-separated) list of
1535 # enabled.
1536 repo_hooks_project = self._reqatt(node, "in-project")
1537 enabled_repo_hooks = self._ParseList(
1538 self._reqatt(node, "enabled-list")
1539 )
1540 if node.nodeName == "superproject":
1541 name = self._reqatt(node, "name")
1542 # There can only be one superproject.
1543 if self._superproject:
1544 raise ManifestParseError(
1545 "duplicate superproject in %s" % (self.manifestFile)
1546 )
1547 remote_name = node.getAttribute("remote")
1548 if not remote_name:
1549 remote = self._default.remote
1550 else:
1551 remote = self._get_remote(node)
1552 if remote is None:
1553 raise ManifestParseError(
1554 "no remote for superproject %s within %s"
1555 % (name, self.manifestFile)
1556 )
1557 revision = node.getAttribute("revision") or remote.revision
1558 if not revision:
1559 revision = self._default.revisionExpr
1560 if not revision:
1561 raise ManifestParseError(
1562 "no revision for superproject %s within %s"
1563 % (name, self.manifestFile)
1564 )
1565 self._superproject = Superproject(
1566 self,
1567 name=name,
1568 remote=remote.ToRemoteSpec(name),
1569 revision=revision,
1570 )
1571 if node.nodeName == "contactinfo":
1572 bugurl = self._reqatt(node, "bugurl")
1573 # This element can be repeated, later entries will clobber
1574 # earlier ones.
1575 self._contactinfo = ContactInfo(bugurl)
1576
1577 if node.nodeName == "remove-project":
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001578 name = node.getAttribute("name")
1579 path = node.getAttribute("path")
Fredrik de Groot303bd962024-09-09 15:54:57 +02001580 base_revision = node.getAttribute("base-rev")
Gavin Makea2e3302023-03-11 06:46:20 +00001581
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001582 # Name or path needed.
1583 if not name and not path:
1584 raise ManifestParseError(
1585 "remove-project must have name and/or path"
1586 )
Gavin Makea2e3302023-03-11 06:46:20 +00001587
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001588 removed_project = ""
1589
1590 # Find and remove projects based on name and/or path.
1591 for projname, projects in list(self._projects.items()):
1592 for p in projects:
1593 if name == projname and not path:
Fredrik de Groot303bd962024-09-09 15:54:57 +02001594 if base_revision:
1595 if p.revisionExpr != base_revision:
1596 failed_revision_changes.append(
1597 "remove-project name %s mismatch base "
1598 "%s vs revision %s"
1599 % (name, base_revision, p.revisionExpr)
1600 )
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001601 del self._paths[p.relpath]
1602 if not removed_project:
1603 del self._projects[name]
1604 removed_project = name
1605 elif path == p.relpath and (
1606 name == projname or not name
1607 ):
Fredrik de Groot303bd962024-09-09 15:54:57 +02001608 if base_revision:
1609 if p.revisionExpr != base_revision:
1610 failed_revision_changes.append(
1611 "remove-project path %s mismatch base "
1612 "%s vs revision %s"
1613 % (
1614 p.relpath,
1615 base_revision,
1616 p.revisionExpr,
1617 )
1618 )
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001619 self._projects[projname].remove(p)
1620 del self._paths[p.relpath]
1621 removed_project = p.name
1622
1623 # If the manifest removes the hooks project, treat it as if
1624 # it deleted the repo-hooks element too.
1625 if (
1626 removed_project
1627 and removed_project not in self._projects
1628 and repo_hooks_project == removed_project
1629 ):
1630 repo_hooks_project = None
1631
1632 if not removed_project and not XmlBool(node, "optional", False):
Gavin Makea2e3302023-03-11 06:46:20 +00001633 raise ManifestParseError(
1634 "remove-project element specifies non-existent "
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001635 "project: %s" % node.toxml()
Gavin Makea2e3302023-03-11 06:46:20 +00001636 )
1637
Fredrik de Groot303bd962024-09-09 15:54:57 +02001638 if failed_revision_changes:
1639 raise ManifestParseError(
1640 "revision base check failed, rebase patches and update "
1641 "base revs for: ",
1642 failed_revision_changes,
1643 )
1644
Gavin Makea2e3302023-03-11 06:46:20 +00001645 # Store repo hooks project information.
1646 if repo_hooks_project:
1647 # Store a reference to the Project.
1648 try:
1649 repo_hooks_projects = self._projects[repo_hooks_project]
1650 except KeyError:
1651 raise ManifestParseError(
1652 "project %s not found for repo-hooks" % (repo_hooks_project)
1653 )
1654
1655 if len(repo_hooks_projects) != 1:
1656 raise ManifestParseError(
1657 "internal error parsing repo-hooks in %s"
1658 % (self.manifestFile)
1659 )
1660 self._repo_hooks_project = repo_hooks_projects[0]
1661 # Store the enabled hooks in the Project object.
1662 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
1663
1664 def _AddMetaProjectMirror(self, m):
1665 name = None
1666 m_url = m.GetRemote().url
1667 if m_url.endswith("/.git"):
1668 raise ManifestParseError("refusing to mirror %s" % m_url)
1669
1670 if self._default and self._default.remote:
1671 url = self._default.remote.resolvedFetchUrl
1672 if not url.endswith("/"):
1673 url += "/"
1674 if m_url.startswith(url):
1675 remote = self._default.remote
1676 name = m_url[len(url) :]
1677
1678 if name is None:
1679 s = m_url.rindex("/") + 1
1680 manifestUrl = self.manifestProject.config.GetString(
1681 "remote.origin.url"
1682 )
1683 remote = _XmlRemote(
1684 "origin", fetch=m_url[:s], manifestUrl=manifestUrl
1685 )
1686 name = m_url[s:]
1687
1688 if name.endswith(".git"):
1689 name = name[:-4]
Josh Triplett884a3872014-06-12 14:57:29 -07001690
1691 if name not in self._projects:
Gavin Makea2e3302023-03-11 06:46:20 +00001692 m.PreSync()
1693 gitdir = os.path.join(self.topdir, "%s.git" % name)
1694 project = Project(
1695 manifest=self,
1696 name=name,
1697 remote=remote.ToRemoteSpec(name),
1698 gitdir=gitdir,
1699 objdir=gitdir,
1700 worktree=None,
1701 relpath=name or None,
1702 revisionExpr=m.revisionExpr,
1703 revisionId=None,
1704 )
1705 self._projects[project.name] = [project]
1706 self._paths[project.relpath] = project
Josh Triplett884a3872014-06-12 14:57:29 -07001707
Gavin Makea2e3302023-03-11 06:46:20 +00001708 def _ParseRemote(self, node):
1709 """
1710 reads a <remote> element from the manifest file
1711 """
1712 name = self._reqatt(node, "name")
1713 alias = node.getAttribute("alias")
1714 if alias == "":
1715 alias = None
1716 fetch = self._reqatt(node, "fetch")
1717 pushUrl = node.getAttribute("pushurl")
1718 if pushUrl == "":
1719 pushUrl = None
1720 review = node.getAttribute("review")
1721 if review == "":
1722 review = None
1723 revision = node.getAttribute("revision")
1724 if revision == "":
1725 revision = None
1726 manifestUrl = self.manifestProject.config.GetString("remote.origin.url")
1727
1728 remote = _XmlRemote(
1729 name, alias, fetch, pushUrl, manifestUrl, review, revision
1730 )
1731
1732 for n in node.childNodes:
1733 if n.nodeName == "annotation":
1734 self._ParseAnnotation(remote, n)
1735
1736 return remote
1737
1738 def _ParseDefault(self, node):
1739 """
1740 reads a <default> element from the manifest file
1741 """
1742 d = _Default()
1743 d.remote = self._get_remote(node)
1744 d.revisionExpr = node.getAttribute("revision")
1745 if d.revisionExpr == "":
1746 d.revisionExpr = None
1747
1748 d.destBranchExpr = node.getAttribute("dest-branch") or None
1749 d.upstreamExpr = node.getAttribute("upstream") or None
1750
1751 d.sync_j = XmlInt(node, "sync-j", None)
1752 if d.sync_j is not None and d.sync_j <= 0:
1753 raise ManifestParseError(
1754 '%s: sync-j must be greater than 0, not "%s"'
1755 % (self.manifestFile, d.sync_j)
1756 )
1757
1758 d.sync_c = XmlBool(node, "sync-c", False)
1759 d.sync_s = XmlBool(node, "sync-s", False)
1760 d.sync_tags = XmlBool(node, "sync-tags", True)
1761 return d
1762
1763 def _ParseNotice(self, node):
1764 """
1765 reads a <notice> element from the manifest file
1766
1767 The <notice> element is distinct from other tags in the XML in that the
1768 data is conveyed between the start and end tag (it's not an
1769 empty-element tag).
1770
1771 The white space (carriage returns, indentation) for the notice element
1772 is relevant and is parsed in a way that is based on how python
1773 docstrings work. In fact, the code is remarkably similar to here:
1774 http://www.python.org/dev/peps/pep-0257/
1775 """
1776 # Get the data out of the node...
1777 notice = node.childNodes[0].data
1778
1779 # Figure out minimum indentation, skipping the first line (the same line
1780 # as the <notice> tag)...
1781 minIndent = sys.maxsize
1782 lines = notice.splitlines()
1783 for line in lines[1:]:
1784 lstrippedLine = line.lstrip()
1785 if lstrippedLine:
1786 indent = len(line) - len(lstrippedLine)
1787 minIndent = min(indent, minIndent)
1788
1789 # Strip leading / trailing blank lines and also indentation.
1790 cleanLines = [lines[0].strip()]
1791 for line in lines[1:]:
1792 cleanLines.append(line[minIndent:].rstrip())
1793
1794 # Clear completely blank lines from front and back...
1795 while cleanLines and not cleanLines[0]:
1796 del cleanLines[0]
1797 while cleanLines and not cleanLines[-1]:
1798 del cleanLines[-1]
1799
1800 return "\n".join(cleanLines)
1801
1802 def _ParseSubmanifest(self, node):
1803 """Reads a <submanifest> element from the manifest file."""
1804 name = self._reqatt(node, "name")
1805 remote = node.getAttribute("remote")
1806 if remote == "":
1807 remote = None
1808 project = node.getAttribute("project")
1809 if project == "":
1810 project = None
1811 revision = node.getAttribute("revision")
1812 if revision == "":
1813 revision = None
1814 manifestName = node.getAttribute("manifest-name")
1815 if manifestName == "":
1816 manifestName = None
1817 groups = ""
1818 if node.hasAttribute("groups"):
1819 groups = node.getAttribute("groups")
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001820 groups = self._ParseSet(groups)
Gavin Makea2e3302023-03-11 06:46:20 +00001821 default_groups = self._ParseList(node.getAttribute("default-groups"))
1822 path = node.getAttribute("path")
1823 if path == "":
1824 path = None
1825 if revision:
1826 msg = self._CheckLocalPath(revision.split("/")[-1])
1827 if msg:
1828 raise ManifestInvalidPathError(
1829 '<submanifest> invalid "revision": %s: %s'
1830 % (revision, msg)
1831 )
1832 else:
1833 msg = self._CheckLocalPath(name)
1834 if msg:
1835 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001836 f'<submanifest> invalid "name": {name}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001837 )
LaMont Jonescc879a92021-11-18 22:40:18 +00001838 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001839 msg = self._CheckLocalPath(path)
1840 if msg:
1841 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001842 f'<submanifest> invalid "path": {path}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001843 )
Josh Triplett884a3872014-06-12 14:57:29 -07001844
Gavin Makea2e3302023-03-11 06:46:20 +00001845 submanifest = _XmlSubmanifest(
1846 name,
1847 remote,
1848 project,
1849 revision,
1850 manifestName,
1851 groups,
1852 default_groups,
1853 path,
1854 self,
1855 )
Michael Kelly2f3c3312020-07-21 19:40:38 -07001856
Gavin Makea2e3302023-03-11 06:46:20 +00001857 for n in node.childNodes:
1858 if n.nodeName == "annotation":
1859 self._ParseAnnotation(submanifest, n)
Michael Kelly2f3c3312020-07-21 19:40:38 -07001860
Gavin Makea2e3302023-03-11 06:46:20 +00001861 return submanifest
Michael Kelly37c21c22020-06-13 02:10:40 -07001862
Gavin Makea2e3302023-03-11 06:46:20 +00001863 def _JoinName(self, parent_name, name):
1864 return os.path.join(parent_name, name)
Doug Anderson37282b42011-03-04 11:54:18 -08001865
Gavin Makea2e3302023-03-11 06:46:20 +00001866 def _UnjoinName(self, parent_name, name):
1867 return os.path.relpath(name, parent_name)
1868
1869 def _ParseProject(self, node, parent=None, **extra_proj_attrs):
1870 """
1871 reads a <project> element from the manifest file
1872 """
1873 name = self._reqatt(node, "name")
1874 msg = self._CheckLocalPath(name, dir_ok=True)
1875 if msg:
1876 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001877 f'<project> invalid "name": {name}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001878 )
1879 if parent:
1880 name = self._JoinName(parent.name, name)
1881
1882 remote = self._get_remote(node)
Raman Tenneti1bb4fb22021-01-07 16:50:45 -08001883 if remote is None:
Gavin Makea2e3302023-03-11 06:46:20 +00001884 remote = self._default.remote
1885 if remote is None:
1886 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001887 f"no remote for project {name} within {self.manifestFile}"
Gavin Makea2e3302023-03-11 06:46:20 +00001888 )
Raman Tenneti993af5e2021-05-12 12:00:31 -07001889
Gavin Makea2e3302023-03-11 06:46:20 +00001890 revisionExpr = node.getAttribute("revision") or remote.revision
1891 if not revisionExpr:
1892 revisionExpr = self._default.revisionExpr
1893 if not revisionExpr:
1894 raise ManifestParseError(
1895 "no revision for project %s within %s"
1896 % (name, self.manifestFile)
1897 )
David Jamesb8433df2014-01-30 10:11:17 -08001898
Gavin Makea2e3302023-03-11 06:46:20 +00001899 path = node.getAttribute("path")
1900 if not path:
1901 path = name
Julien Camperguedd654222014-01-09 16:21:37 +01001902 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001903 # NB: The "." project is handled specially in
1904 # Project.Sync_LocalHalf.
1905 msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
1906 if msg:
1907 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001908 f'<project> invalid "path": {path}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001909 )
Julien Camperguedd654222014-01-09 16:21:37 +01001910
Gavin Makea2e3302023-03-11 06:46:20 +00001911 rebase = XmlBool(node, "rebase", True)
1912 sync_c = XmlBool(node, "sync-c", False)
1913 sync_s = XmlBool(node, "sync-s", self._default.sync_s)
1914 sync_tags = XmlBool(node, "sync-tags", self._default.sync_tags)
Julien Camperguedd654222014-01-09 16:21:37 +01001915
Gavin Makea2e3302023-03-11 06:46:20 +00001916 clone_depth = XmlInt(node, "clone-depth")
1917 if clone_depth is not None and clone_depth <= 0:
1918 raise ManifestParseError(
1919 '%s: clone-depth must be greater than 0, not "%s"'
1920 % (self.manifestFile, clone_depth)
1921 )
1922
1923 dest_branch = (
1924 node.getAttribute("dest-branch") or self._default.destBranchExpr
1925 )
1926
1927 upstream = node.getAttribute("upstream") or self._default.upstreamExpr
1928
Gavin Makea2e3302023-03-11 06:46:20 +00001929 if parent is None:
1930 (
1931 relpath,
1932 worktree,
1933 gitdir,
1934 objdir,
1935 use_git_worktrees,
1936 ) = self.GetProjectPaths(name, path, remote.name)
1937 else:
1938 use_git_worktrees = False
1939 relpath, worktree, gitdir, objdir = self.GetSubprojectPaths(
1940 parent, name, path
1941 )
1942
Peter Kjellerstedt75773b82025-11-08 02:36:56 +01001943 groups = ""
1944 if node.hasAttribute("groups"):
1945 groups = node.getAttribute("groups")
1946 groups = self._ParseSet(groups)
1947 groups |= {"all", f"name:{name}", f"path:{relpath}"}
Gavin Makea2e3302023-03-11 06:46:20 +00001948
1949 if self.IsMirror and node.hasAttribute("force-path"):
1950 if XmlBool(node, "force-path", False):
1951 gitdir = os.path.join(self.topdir, "%s.git" % path)
1952
1953 project = Project(
1954 manifest=self,
1955 name=name,
1956 remote=remote.ToRemoteSpec(name),
1957 gitdir=gitdir,
1958 objdir=objdir,
1959 worktree=worktree,
1960 relpath=relpath,
1961 revisionExpr=revisionExpr,
1962 revisionId=None,
1963 rebase=rebase,
1964 groups=groups,
1965 sync_c=sync_c,
1966 sync_s=sync_s,
1967 sync_tags=sync_tags,
1968 clone_depth=clone_depth,
1969 upstream=upstream,
1970 parent=parent,
1971 dest_branch=dest_branch,
1972 use_git_worktrees=use_git_worktrees,
1973 **extra_proj_attrs,
1974 )
1975
1976 for n in node.childNodes:
1977 if n.nodeName == "copyfile":
1978 self._ParseCopyFile(project, n)
Peter Kjellerstedt4ab22842025-10-16 20:29:28 +02001979 elif n.nodeName == "linkfile":
Gavin Makea2e3302023-03-11 06:46:20 +00001980 self._ParseLinkFile(project, n)
Peter Kjellerstedt4ab22842025-10-16 20:29:28 +02001981 elif n.nodeName == "annotation":
Gavin Makea2e3302023-03-11 06:46:20 +00001982 self._ParseAnnotation(project, n)
Peter Kjellerstedt4ab22842025-10-16 20:29:28 +02001983 elif n.nodeName == "project":
Gavin Makea2e3302023-03-11 06:46:20 +00001984 project.subprojects.append(
1985 self._ParseProject(n, parent=project)
1986 )
1987
1988 return project
1989
1990 def GetProjectPaths(self, name, path, remote):
1991 """Return the paths for a project.
1992
1993 Args:
1994 name: a string, the name of the project.
1995 path: a string, the path of the project.
1996 remote: a string, the remote.name of the project.
1997
1998 Returns:
1999 A tuple of (relpath, worktree, gitdir, objdir, use_git_worktrees)
2000 for the project with |name| and |path|.
2001 """
2002 # The manifest entries might have trailing slashes. Normalize them to
2003 # avoid unexpected filesystem behavior since we do string concatenation
2004 # below.
2005 path = path.rstrip("/")
2006 name = name.rstrip("/")
2007 remote = remote.rstrip("/")
2008 use_git_worktrees = False
2009 use_remote_name = self.is_multimanifest
2010 relpath = path
2011 if self.IsMirror:
2012 worktree = None
2013 gitdir = os.path.join(self.topdir, "%s.git" % name)
2014 objdir = gitdir
2015 else:
2016 if use_remote_name:
2017 namepath = os.path.join(remote, f"{name}.git")
2018 else:
2019 namepath = f"{name}.git"
2020 worktree = os.path.join(self.topdir, path).replace("\\", "/")
2021 gitdir = os.path.join(self.subdir, "projects", "%s.git" % path)
2022 # We allow people to mix git worktrees & non-git worktrees for now.
2023 # This allows for in situ migration of repo clients.
2024 if os.path.exists(gitdir) or not self.UseGitWorktrees:
2025 objdir = os.path.join(self.repodir, "project-objects", namepath)
2026 else:
2027 use_git_worktrees = True
2028 gitdir = os.path.join(self.repodir, "worktrees", namepath)
2029 objdir = gitdir
2030 return relpath, worktree, gitdir, objdir, use_git_worktrees
2031
2032 def GetProjectsWithName(self, name, all_manifests=False):
2033 """All projects with |name|.
2034
2035 Args:
2036 name: a string, the name of the project.
2037 all_manifests: a boolean, if True, then all manifests are searched.
2038 If False, then only this manifest is searched.
2039
2040 Returns:
2041 A list of Project instances with name |name|.
2042 """
2043 if all_manifests:
2044 return list(
2045 itertools.chain.from_iterable(
2046 x._projects.get(name, []) for x in self.all_manifests
2047 )
2048 )
2049 return self._projects.get(name, [])
2050
2051 def GetSubprojectName(self, parent, submodule_path):
2052 return os.path.join(parent.name, submodule_path)
2053
2054 def _JoinRelpath(self, parent_relpath, relpath):
2055 return os.path.join(parent_relpath, relpath)
2056
2057 def _UnjoinRelpath(self, parent_relpath, relpath):
2058 return os.path.relpath(relpath, parent_relpath)
2059
2060 def GetSubprojectPaths(self, parent, name, path):
2061 # The manifest entries might have trailing slashes. Normalize them to
2062 # avoid unexpected filesystem behavior since we do string concatenation
2063 # below.
2064 path = path.rstrip("/")
2065 name = name.rstrip("/")
2066 relpath = self._JoinRelpath(parent.relpath, path)
Kaushik Lingarkarcf9a2a22024-12-17 12:49:14 -08002067 subprojects = os.path.join(parent.gitdir, "subprojects", f"{path}.git")
2068 modules = os.path.join(parent.gitdir, "modules", path)
2069 if platform_utils.isdir(subprojects):
2070 gitdir = subprojects
2071 else:
2072 gitdir = modules
Gavin Makea2e3302023-03-11 06:46:20 +00002073 objdir = os.path.join(
2074 parent.gitdir, "subproject-objects", "%s.git" % name
2075 )
2076 if self.IsMirror:
2077 worktree = None
2078 else:
2079 worktree = os.path.join(parent.worktree, path).replace("\\", "/")
2080 return relpath, worktree, gitdir, objdir
2081
2082 @staticmethod
2083 def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False):
2084 """Verify |path| is reasonable for use in filesystem paths.
2085
2086 Used with <copyfile> & <linkfile> & <project> elements.
2087
2088 This only validates the |path| in isolation: it does not check against
2089 the current filesystem state. Thus it is suitable as a first-past in a
2090 parser.
2091
2092 It enforces a number of constraints:
2093 * No empty paths.
2094 * No "~" in paths.
2095 * No Unicode codepoints that filesystems might elide when normalizing.
2096 * No relative path components like "." or "..".
2097 * No absolute paths.
2098 * No ".git" or ".repo*" path components.
2099
2100 Args:
2101 path: The path name to validate.
2102 dir_ok: Whether |path| may force a directory (e.g. end in a /).
2103 cwd_dot_ok: Whether |path| may be just ".".
2104
2105 Returns:
2106 None if |path| is OK, a failure message otherwise.
2107 """
2108 if not path:
2109 return "empty paths not allowed"
2110
2111 if "~" in path:
2112 return "~ not allowed (due to 8.3 filenames on Windows filesystems)"
2113
2114 path_codepoints = set(path)
2115
2116 # Some filesystems (like Apple's HFS+) try to normalize Unicode
2117 # codepoints which means there are alternative names for ".git". Reject
2118 # paths with these in it as there shouldn't be any reasonable need for
2119 # them here. The set of codepoints here was cribbed from jgit's
2120 # implementation:
2121 # https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
2122 BAD_CODEPOINTS = {
Mike Frysingercd391e72025-03-25 12:53:55 -04002123 "\u200c", # ZERO WIDTH NON-JOINER
2124 "\u200d", # ZERO WIDTH JOINER
2125 "\u200e", # LEFT-TO-RIGHT MARK
2126 "\u200f", # RIGHT-TO-LEFT MARK
2127 "\u202a", # LEFT-TO-RIGHT EMBEDDING
2128 "\u202b", # RIGHT-TO-LEFT EMBEDDING
2129 "\u202c", # POP DIRECTIONAL FORMATTING
2130 "\u202d", # LEFT-TO-RIGHT OVERRIDE
2131 "\u202e", # RIGHT-TO-LEFT OVERRIDE
2132 "\u206a", # INHIBIT SYMMETRIC SWAPPING
2133 "\u206b", # ACTIVATE SYMMETRIC SWAPPING
2134 "\u206c", # INHIBIT ARABIC FORM SHAPING
2135 "\u206d", # ACTIVATE ARABIC FORM SHAPING
2136 "\u206e", # NATIONAL DIGIT SHAPES
2137 "\u206f", # NOMINAL DIGIT SHAPES
2138 "\ufeff", # ZERO WIDTH NO-BREAK SPACE
Gavin Makea2e3302023-03-11 06:46:20 +00002139 }
2140 if BAD_CODEPOINTS & path_codepoints:
2141 # This message is more expansive than reality, but should be fine.
2142 return "Unicode combining characters not allowed"
2143
2144 # Reject newlines as there shouldn't be any legitmate use for them,
2145 # they'll be confusing to users, and they can easily break tools that
2146 # expect to be able to iterate over newline delimited lists. This even
2147 # applies to our own code like .repo/project.list.
2148 if {"\r", "\n"} & path_codepoints:
2149 return "Newlines not allowed"
2150
2151 # Assume paths might be used on case-insensitive filesystems.
2152 path = path.lower()
2153
2154 # Split up the path by its components. We can't use os.path.sep
2155 # exclusively as some platforms (like Windows) will convert / to \ and
2156 # that bypasses all our constructed logic here. Especially since
2157 # manifest authors only use / in their paths.
2158 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
2159 # Strip off trailing slashes as those only produce '' elements, and we
2160 # use parts to look for individual bad components.
2161 parts = resep.split(path.rstrip("/"))
2162
2163 # Some people use src="." to create stable links to projects. Lets
2164 # allow that but reject all other uses of "." to keep things simple.
2165 if not cwd_dot_ok or parts != ["."]:
2166 for part in set(parts):
2167 if part in {".", "..", ".git"} or part.startswith(".repo"):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002168 return f"bad component: {part}"
Gavin Makea2e3302023-03-11 06:46:20 +00002169
2170 if not dir_ok and resep.match(path[-1]):
2171 return "dirs not allowed"
2172
2173 # NB: The two abspath checks here are to handle platforms with multiple
2174 # filesystem path styles (e.g. Windows).
2175 norm = os.path.normpath(path)
2176 if (
2177 norm == ".."
2178 or (
2179 len(norm) >= 3
2180 and norm.startswith("..")
2181 and resep.match(norm[0])
2182 )
2183 or os.path.isabs(norm)
2184 or norm.startswith("/")
2185 ):
2186 return "path cannot be outside"
2187
2188 @classmethod
2189 def _ValidateFilePaths(cls, element, src, dest):
2190 """Verify |src| & |dest| are reasonable for <copyfile> & <linkfile>.
2191
2192 We verify the path independent of any filesystem state as we won't have
2193 a checkout available to compare to. i.e. This is for parsing validation
2194 purposes only.
2195
2196 We'll do full/live sanity checking before we do the actual filesystem
2197 modifications in _CopyFile/_LinkFile/etc...
2198 """
2199 # |dest| is the file we write to or symlink we create.
2200 # It is relative to the top of the repo client checkout.
2201 msg = cls._CheckLocalPath(dest)
2202 if msg:
2203 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002204 f'<{element}> invalid "dest": {dest}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00002205 )
2206
2207 # |src| is the file we read from or path we point to for symlinks.
2208 # It is relative to the top of the git project checkout.
2209 is_linkfile = element == "linkfile"
2210 msg = cls._CheckLocalPath(
2211 src, dir_ok=is_linkfile, cwd_dot_ok=is_linkfile
2212 )
2213 if msg:
2214 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002215 f'<{element}> invalid "src": {src}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00002216 )
2217
2218 def _ParseCopyFile(self, project, node):
2219 src = self._reqatt(node, "src")
2220 dest = self._reqatt(node, "dest")
2221 if not self.IsMirror:
2222 # src is project relative;
2223 # dest is relative to the top of the tree.
2224 # We only validate paths if we actually plan to process them.
2225 self._ValidateFilePaths("copyfile", src, dest)
2226 project.AddCopyFile(src, dest, self.topdir)
2227
2228 def _ParseLinkFile(self, project, node):
2229 src = self._reqatt(node, "src")
2230 dest = self._reqatt(node, "dest")
2231 if not self.IsMirror:
2232 # src is project relative;
2233 # dest is relative to the top of the tree.
2234 # We only validate paths if we actually plan to process them.
2235 self._ValidateFilePaths("linkfile", src, dest)
2236 project.AddLinkFile(src, dest, self.topdir)
2237
2238 def _ParseAnnotation(self, element, node):
2239 name = self._reqatt(node, "name")
2240 value = self._reqatt(node, "value")
2241 try:
2242 keep = self._reqatt(node, "keep").lower()
2243 except ManifestParseError:
2244 keep = "true"
2245 if keep != "true" and keep != "false":
2246 raise ManifestParseError(
2247 'optional "keep" attribute must be ' '"true" or "false"'
2248 )
2249 element.AddAnnotation(name, value, keep)
2250
2251 def _get_remote(self, node):
2252 name = node.getAttribute("remote")
2253 if not name:
2254 return None
2255
2256 v = self._remotes.get(name)
2257 if not v:
2258 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002259 f"remote {name} not defined in {self.manifestFile}"
Gavin Makea2e3302023-03-11 06:46:20 +00002260 )
2261 return v
2262
2263 def _reqatt(self, node, attname):
2264 """
2265 reads a required attribute from the node.
2266 """
2267 v = node.getAttribute(attname)
2268 if not v:
2269 raise ManifestParseError(
2270 "no %s in <%s> within %s"
2271 % (attname, node.nodeName, self.manifestFile)
2272 )
2273 return v
2274
2275 def projectsDiff(self, manifest):
2276 """return the projects differences between two manifests.
2277
2278 The diff will be from self to given manifest.
2279
2280 """
2281 fromProjects = self.paths
2282 toProjects = manifest.paths
2283
2284 fromKeys = sorted(fromProjects.keys())
Sylvain25d6c7c2023-08-19 23:21:49 +02002285 toKeys = set(toProjects.keys())
Gavin Makea2e3302023-03-11 06:46:20 +00002286
2287 diff = {
2288 "added": [],
2289 "removed": [],
2290 "missing": [],
2291 "changed": [],
2292 "unreachable": [],
2293 }
2294
2295 for proj in fromKeys:
Sylvain25d6c7c2023-08-19 23:21:49 +02002296 fromProj = fromProjects[proj]
Gavin Makea2e3302023-03-11 06:46:20 +00002297 if proj not in toKeys:
Sylvain25d6c7c2023-08-19 23:21:49 +02002298 diff["removed"].append(fromProj)
2299 elif not fromProj.Exists:
Gavin Makea2e3302023-03-11 06:46:20 +00002300 diff["missing"].append(toProjects[proj])
2301 toKeys.remove(proj)
2302 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002303 toProj = toProjects[proj]
2304 try:
2305 fromRevId = fromProj.GetCommitRevisionId()
2306 toRevId = toProj.GetCommitRevisionId()
2307 except ManifestInvalidRevisionError:
2308 diff["unreachable"].append((fromProj, toProj))
2309 else:
2310 if fromRevId != toRevId:
2311 diff["changed"].append((fromProj, toProj))
2312 toKeys.remove(proj)
2313
Sylvain25d6c7c2023-08-19 23:21:49 +02002314 diff["added"].extend(toProjects[proj] for proj in sorted(toKeys))
Gavin Makea2e3302023-03-11 06:46:20 +00002315
2316 return diff
Simran Basib9a1b732015-08-20 12:19:28 -07002317
2318
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002319class RepoClient(XmlManifest):
Gavin Makea2e3302023-03-11 06:46:20 +00002320 """Manages a repo client checkout."""
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002321
Gavin Makea2e3302023-03-11 06:46:20 +00002322 def __init__(
2323 self, repodir, manifest_file=None, submanifest_path="", **kwargs
2324 ):
2325 """Initialize.
LaMont Jonesff6b1da2022-06-01 21:03:34 +00002326
Gavin Makea2e3302023-03-11 06:46:20 +00002327 Args:
2328 repodir: Path to the .repo/ dir for holding all internal checkout
2329 state. It must be in the top directory of the repo client
2330 checkout.
2331 manifest_file: Full path to the manifest file to parse. This will
2332 usually be |repodir|/|MANIFEST_FILE_NAME|.
2333 submanifest_path: The submanifest root relative to the repo root.
2334 **kwargs: Additional keyword arguments, passed to XmlManifest.
2335 """
Gavin Makea2e3302023-03-11 06:46:20 +00002336 submanifest_path = submanifest_path or ""
2337 if submanifest_path:
2338 self._CheckLocalPath(submanifest_path)
2339 prefix = os.path.join(repodir, SUBMANIFEST_DIR, submanifest_path)
2340 else:
2341 prefix = repodir
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002342
Gavin Makea2e3302023-03-11 06:46:20 +00002343 if os.path.exists(os.path.join(prefix, LOCAL_MANIFEST_NAME)):
2344 print(
2345 "error: %s is not supported; put local manifests in `%s` "
2346 "instead"
2347 % (
2348 LOCAL_MANIFEST_NAME,
2349 os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME),
2350 ),
2351 file=sys.stderr,
2352 )
2353 sys.exit(1)
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002354
Gavin Makea2e3302023-03-11 06:46:20 +00002355 if manifest_file is None:
2356 manifest_file = os.path.join(prefix, MANIFEST_FILE_NAME)
2357 local_manifests = os.path.abspath(
2358 os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME)
2359 )
2360 super().__init__(
2361 repodir,
2362 manifest_file,
2363 local_manifests,
2364 submanifest_path=submanifest_path,
2365 **kwargs,
2366 )
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002367
Gavin Makea2e3302023-03-11 06:46:20 +00002368 # TODO: Completely separate manifest logic out of the client.
2369 self.manifest = self