blob: 61b130cf951bdd37dd3f003563ee2f40ca390202 [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
Mike Frysingerd4aee652023-10-19 05:13:32 -0400120class _Default:
Gavin Makea2e3302023-03-11 06:46:20 +0000121 """Project defaults within the manifest."""
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700122
Gavin Makea2e3302023-03-11 06:46:20 +0000123 revisionExpr = None
124 destBranchExpr = None
125 upstreamExpr = None
126 remote = None
127 sync_j = None
128 sync_c = False
129 sync_s = False
130 sync_tags = True
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700131
Gavin Makea2e3302023-03-11 06:46:20 +0000132 def __eq__(self, other):
133 if not isinstance(other, _Default):
134 return False
135 return self.__dict__ == other.__dict__
Julien Campergue74879922013-10-09 14:38:46 +0200136
Gavin Makea2e3302023-03-11 06:46:20 +0000137 def __ne__(self, other):
138 if not isinstance(other, _Default):
139 return True
140 return self.__dict__ != other.__dict__
Julien Campergue74879922013-10-09 14:38:46 +0200141
David Pursehouse819827a2020-02-12 15:20:19 +0900142
Mike Frysingerd4aee652023-10-19 05:13:32 -0400143class _XmlRemote:
Gavin Makea2e3302023-03-11 06:46:20 +0000144 def __init__(
145 self,
146 name,
147 alias=None,
148 fetch=None,
149 pushUrl=None,
150 manifestUrl=None,
151 review=None,
152 revision=None,
153 ):
154 self.name = name
155 self.fetchUrl = fetch
156 self.pushUrl = pushUrl
157 self.manifestUrl = manifestUrl
158 self.remoteAlias = alias
159 self.reviewUrl = review
160 self.revision = revision
161 self.resolvedFetchUrl = self._resolveFetchUrl()
162 self.annotations = []
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700163
Gavin Makea2e3302023-03-11 06:46:20 +0000164 def __eq__(self, other):
165 if not isinstance(other, _XmlRemote):
166 return False
167 return (
168 sorted(self.annotations) == sorted(other.annotations)
169 and self.name == other.name
170 and self.fetchUrl == other.fetchUrl
171 and self.pushUrl == other.pushUrl
172 and self.remoteAlias == other.remoteAlias
173 and self.reviewUrl == other.reviewUrl
174 and self.revision == other.revision
175 )
David Pursehouse717ece92012-11-13 08:49:16 +0900176
Gavin Makea2e3302023-03-11 06:46:20 +0000177 def __ne__(self, other):
178 return not self.__eq__(other)
David Pursehouse717ece92012-11-13 08:49:16 +0900179
Gavin Makea2e3302023-03-11 06:46:20 +0000180 def _resolveFetchUrl(self):
181 if self.fetchUrl is None:
182 return ""
183 url = self.fetchUrl.rstrip("/")
184 manifestUrl = self.manifestUrl.rstrip("/")
185 # urljoin will gets confused over quite a few things. The ones we care
186 # about here are:
187 # * no scheme in the base url, like <hostname:port>
188 # We handle no scheme by replacing it with an obscure protocol, gopher
189 # and then replacing it with the original when we are done.
Anthony Kingcb07ba72015-03-28 23:26:04 +0000190
Gavin Makea2e3302023-03-11 06:46:20 +0000191 if manifestUrl.find(":") != manifestUrl.find("/") - 1:
192 url = urllib.parse.urljoin("gopher://" + manifestUrl, url)
193 url = re.sub(r"^gopher://", "", url)
194 else:
195 url = urllib.parse.urljoin(manifestUrl, url)
196 return url
Conley Owensceea3682011-10-20 10:45:47 -0700197
Gavin Makea2e3302023-03-11 06:46:20 +0000198 def ToRemoteSpec(self, projectName):
199 fetchUrl = self.resolvedFetchUrl.rstrip("/")
200 url = fetchUrl + "/" + projectName
201 remoteName = self.name
202 if self.remoteAlias:
203 remoteName = self.remoteAlias
204 return RemoteSpec(
205 remoteName,
206 url=url,
207 pushUrl=self.pushUrl,
208 review=self.reviewUrl,
209 orig_name=self.name,
210 fetchUrl=self.fetchUrl,
211 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700212
Gavin Makea2e3302023-03-11 06:46:20 +0000213 def AddAnnotation(self, name, value, keep):
214 self.annotations.append(Annotation(name, value, keep))
Jack Neus6ea0cae2021-07-20 20:52:33 +0000215
David Pursehouse819827a2020-02-12 15:20:19 +0900216
LaMont Jonescc879a92021-11-18 22:40:18 +0000217class _XmlSubmanifest:
Gavin Makea2e3302023-03-11 06:46:20 +0000218 """Manage the <submanifest> element specified in the manifest.
LaMont Jonescc879a92021-11-18 22:40:18 +0000219
Gavin Makea2e3302023-03-11 06:46:20 +0000220 Attributes:
221 name: a string, the name for this submanifest.
222 remote: a string, the remote.name for this submanifest.
223 project: a string, the name of the manifest project.
224 revision: a string, the commitish.
225 manifestName: a string, the submanifest file name.
226 groups: a list of strings, the groups to add to all projects in the
227 submanifest.
228 default_groups: a list of strings, the default groups to sync.
229 path: a string, the relative path for the submanifest checkout.
230 parent: an XmlManifest, the parent manifest.
231 annotations: (derived) a list of annotations.
232 present: (derived) a boolean, whether the sub manifest file is present.
233 """
LaMont Jonescc879a92021-11-18 22:40:18 +0000234
Gavin Makea2e3302023-03-11 06:46:20 +0000235 def __init__(
236 self,
237 name,
238 remote=None,
239 project=None,
240 revision=None,
241 manifestName=None,
242 groups=None,
243 default_groups=None,
244 path=None,
245 parent=None,
246 ):
247 self.name = name
248 self.remote = remote
249 self.project = project
250 self.revision = revision
251 self.manifestName = manifestName
252 self.groups = groups
253 self.default_groups = default_groups
254 self.path = path
255 self.parent = parent
256 self.annotations = []
257 outer_client = parent._outer_client or parent
258 if self.remote and not self.project:
259 raise ManifestParseError(
260 f"Submanifest {name}: must specify project when remote is "
261 "given."
262 )
263 # Construct the absolute path to the manifest file using the parent's
264 # method, so that we can correctly create our repo_client.
265 manifestFile = parent.SubmanifestInfoDir(
266 os.path.join(parent.path_prefix, self.relpath),
267 os.path.join("manifests", manifestName or "default.xml"),
268 )
269 linkFile = parent.SubmanifestInfoDir(
270 os.path.join(parent.path_prefix, self.relpath), MANIFEST_FILE_NAME
271 )
272 self.repo_client = RepoClient(
273 parent.repodir,
274 linkFile,
275 parent_groups=",".join(groups) or "",
276 submanifest_path=self.relpath,
277 outer_client=outer_client,
278 default_groups=default_groups,
279 )
LaMont Jonescc879a92021-11-18 22:40:18 +0000280
Gavin Makea2e3302023-03-11 06:46:20 +0000281 self.present = os.path.exists(manifestFile)
LaMont Jonescc879a92021-11-18 22:40:18 +0000282
Gavin Makea2e3302023-03-11 06:46:20 +0000283 def __eq__(self, other):
284 if not isinstance(other, _XmlSubmanifest):
285 return False
286 return (
287 self.name == other.name
288 and self.remote == other.remote
289 and self.project == other.project
290 and self.revision == other.revision
291 and self.manifestName == other.manifestName
292 and self.groups == other.groups
293 and self.default_groups == other.default_groups
294 and self.path == other.path
295 and sorted(self.annotations) == sorted(other.annotations)
296 )
LaMont Jonescc879a92021-11-18 22:40:18 +0000297
Gavin Makea2e3302023-03-11 06:46:20 +0000298 def __ne__(self, other):
299 return not self.__eq__(other)
LaMont Jonescc879a92021-11-18 22:40:18 +0000300
Gavin Makea2e3302023-03-11 06:46:20 +0000301 def ToSubmanifestSpec(self):
302 """Return a SubmanifestSpec object, populating attributes"""
303 mp = self.parent.manifestProject
304 remote = self.parent.remotes[
305 self.remote or self.parent.default.remote.name
306 ]
307 # If a project was given, generate the url from the remote and project.
308 # If not, use this manifestProject's url.
309 if self.project:
310 manifestUrl = remote.ToRemoteSpec(self.project).url
311 else:
312 manifestUrl = mp.GetRemote().url
313 manifestName = self.manifestName or "default.xml"
314 revision = self.revision or self.name
315 path = self.path or revision.split("/")[-1]
316 groups = self.groups or []
LaMont Jonescc879a92021-11-18 22:40:18 +0000317
Gavin Makea2e3302023-03-11 06:46:20 +0000318 return SubmanifestSpec(
319 self.name, manifestUrl, manifestName, revision, path, groups
320 )
LaMont Jonescc879a92021-11-18 22:40:18 +0000321
Gavin Makea2e3302023-03-11 06:46:20 +0000322 @property
323 def relpath(self):
324 """The path of this submanifest relative to the parent manifest."""
325 revision = self.revision or self.name
326 return self.path or revision.split("/")[-1]
LaMont Jonescc879a92021-11-18 22:40:18 +0000327
Gavin Makea2e3302023-03-11 06:46:20 +0000328 def GetGroupsStr(self):
329 """Returns the `groups` given for this submanifest."""
330 if self.groups:
331 return ",".join(self.groups)
332 return ""
LaMont Jones501733c2022-04-20 16:42:32 +0000333
Gavin Makea2e3302023-03-11 06:46:20 +0000334 def GetDefaultGroupsStr(self):
335 """Returns the `default-groups` given for this submanifest."""
336 return ",".join(self.default_groups or [])
337
338 def AddAnnotation(self, name, value, keep):
339 """Add annotations to the submanifest."""
340 self.annotations.append(Annotation(name, value, keep))
LaMont Jonescc879a92021-11-18 22:40:18 +0000341
342
343class SubmanifestSpec:
Gavin Makea2e3302023-03-11 06:46:20 +0000344 """The submanifest element, with all fields expanded."""
LaMont Jonescc879a92021-11-18 22:40:18 +0000345
Gavin Makea2e3302023-03-11 06:46:20 +0000346 def __init__(self, name, manifestUrl, manifestName, revision, path, groups):
347 self.name = name
348 self.manifestUrl = manifestUrl
349 self.manifestName = manifestName
350 self.revision = revision
351 self.path = path
352 self.groups = groups or []
LaMont Jonescc879a92021-11-18 22:40:18 +0000353
354
Mike Frysingerd4aee652023-10-19 05:13:32 -0400355class XmlManifest:
Gavin Makea2e3302023-03-11 06:46:20 +0000356 """manages the repo configuration file"""
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700357
Gavin Makea2e3302023-03-11 06:46:20 +0000358 def __init__(
359 self,
360 repodir,
361 manifest_file,
362 local_manifests=None,
363 outer_client=None,
364 parent_groups="",
365 submanifest_path="",
366 default_groups=None,
367 ):
368 """Initialize.
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -0400369
Gavin Makea2e3302023-03-11 06:46:20 +0000370 Args:
371 repodir: Path to the .repo/ dir for holding all internal checkout
372 state. It must be in the top directory of the repo client
373 checkout.
374 manifest_file: Full path to the manifest file to parse. This will
375 usually be |repodir|/|MANIFEST_FILE_NAME|.
376 local_manifests: Full path to the directory of local override
377 manifests. This will usually be
378 |repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
379 outer_client: RepoClient of the outer manifest.
380 parent_groups: a string, the groups to apply to this projects.
381 submanifest_path: The submanifest root relative to the repo root.
382 default_groups: a string, the default manifest groups to use.
383 """
384 # TODO(vapier): Move this out of this class.
385 self.globalConfig = GitConfig.ForUser()
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -0400386
Gavin Makea2e3302023-03-11 06:46:20 +0000387 self.repodir = os.path.abspath(repodir)
388 self._CheckLocalPath(submanifest_path)
389 self.topdir = os.path.dirname(self.repodir)
390 if submanifest_path:
391 # This avoids a trailing os.path.sep when submanifest_path is empty.
392 self.topdir = os.path.join(self.topdir, submanifest_path)
393 if manifest_file != os.path.abspath(manifest_file):
394 raise ManifestParseError("manifest_file must be abspath")
395 self.manifestFile = manifest_file
396 if not outer_client or outer_client == self:
397 # manifestFileOverrides only exists in the outer_client's manifest,
398 # since that is the only instance left when Unload() is called on
399 # the outer manifest.
400 self.manifestFileOverrides = {}
401 self.local_manifests = local_manifests
402 self._load_local_manifests = True
403 self.parent_groups = parent_groups
404 self.default_groups = default_groups
LaMont Jonescc879a92021-11-18 22:40:18 +0000405
Gavin Makea2e3302023-03-11 06:46:20 +0000406 if outer_client and self.isGitcClient:
407 raise ManifestParseError(
408 "Multi-manifest is incompatible with `gitc-init`"
409 )
LaMont Jonescc879a92021-11-18 22:40:18 +0000410
Gavin Makea2e3302023-03-11 06:46:20 +0000411 if submanifest_path and not outer_client:
412 # If passing a submanifest_path, there must be an outer_client.
413 raise ManifestParseError(f"Bad call to {self.__class__.__name__}")
LaMont Jonescc879a92021-11-18 22:40:18 +0000414
Gavin Makea2e3302023-03-11 06:46:20 +0000415 # If self._outer_client is None, this is not a checkout that supports
416 # multi-tree.
417 self._outer_client = outer_client or self
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700418
Gavin Makea2e3302023-03-11 06:46:20 +0000419 self.repoProject = RepoProject(
420 self,
421 "repo",
422 gitdir=os.path.join(repodir, "repo/.git"),
423 worktree=os.path.join(repodir, "repo"),
424 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700425
Gavin Makea2e3302023-03-11 06:46:20 +0000426 mp = self.SubmanifestProject(self.path_prefix)
427 self.manifestProject = mp
Mike Frysinger979d5bd2020-02-09 02:28:34 -0500428
Gavin Makea2e3302023-03-11 06:46:20 +0000429 # This is a bit hacky, but we're in a chicken & egg situation: all the
430 # normal repo settings live in the manifestProject which we just setup
431 # above, so we couldn't easily query before that. We assume Project()
432 # init doesn't care if this changes afterwards.
433 if os.path.exists(mp.gitdir) and mp.use_worktree:
434 mp.use_git_worktrees = True
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700435
Gavin Makea2e3302023-03-11 06:46:20 +0000436 self.Unload()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700437
Gavin Makea2e3302023-03-11 06:46:20 +0000438 def Override(self, name, load_local_manifests=True):
439 """Use a different manifest, just for the current instantiation."""
440 path = None
Basil Gelloc7453502018-05-25 20:23:52 +0300441
Gavin Makea2e3302023-03-11 06:46:20 +0000442 # Look for a manifest by path in the filesystem (including the cwd).
443 if not load_local_manifests:
444 local_path = os.path.abspath(name)
445 if os.path.isfile(local_path):
446 path = local_path
Basil Gelloc7453502018-05-25 20:23:52 +0300447
Gavin Makea2e3302023-03-11 06:46:20 +0000448 # Look for manifests by name from the manifests repo.
449 if path is None:
450 path = os.path.join(self.manifestProject.worktree, name)
451 if not os.path.isfile(path):
452 raise ManifestParseError("manifest %s not found" % name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700453
Gavin Makea2e3302023-03-11 06:46:20 +0000454 self._load_local_manifests = load_local_manifests
455 self._outer_client.manifestFileOverrides[self.path_prefix] = path
456 self.Unload()
457 self._Load()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700458
Gavin Makea2e3302023-03-11 06:46:20 +0000459 def Link(self, name):
460 """Update the repo metadata to use a different manifest."""
461 self.Override(name)
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700462
Gavin Makea2e3302023-03-11 06:46:20 +0000463 # Old versions of repo would generate symlinks we need to clean up.
464 platform_utils.remove(self.manifestFile, missing_ok=True)
465 # This file is interpreted as if it existed inside the manifest repo.
466 # That allows us to use <include> with the relative file name.
467 with open(self.manifestFile, "w") as fp:
468 fp.write(
469 """<?xml version="1.0" encoding="UTF-8"?>
Mike Frysingera269b1c2020-02-21 00:49:41 -0500470<!--
471DO NOT EDIT THIS FILE! It is generated by repo and changes will be discarded.
472If you want to use a different manifest, use `repo init -m <file>` instead.
473
474If you want to customize your checkout by overriding manifest settings, use
475the local_manifests/ directory instead.
476
477For more information on repo manifests, check out:
478https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
479-->
480<manifest>
481 <include name="%s" />
482</manifest>
Gavin Makea2e3302023-03-11 06:46:20 +0000483"""
484 % (name,)
485 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700486
Gavin Makea2e3302023-03-11 06:46:20 +0000487 def _RemoteToXml(self, r, doc, root):
488 e = doc.createElement("remote")
489 root.appendChild(e)
490 e.setAttribute("name", r.name)
491 e.setAttribute("fetch", r.fetchUrl)
492 if r.pushUrl is not None:
493 e.setAttribute("pushurl", r.pushUrl)
494 if r.remoteAlias is not None:
495 e.setAttribute("alias", r.remoteAlias)
496 if r.reviewUrl is not None:
497 e.setAttribute("review", r.reviewUrl)
498 if r.revision is not None:
499 e.setAttribute("revision", r.revision)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800500
Gavin Makea2e3302023-03-11 06:46:20 +0000501 for a in r.annotations:
502 if a.keep == "true":
503 ae = doc.createElement("annotation")
504 ae.setAttribute("name", a.name)
505 ae.setAttribute("value", a.value)
506 e.appendChild(ae)
Jack Neus6ea0cae2021-07-20 20:52:33 +0000507
Gavin Makea2e3302023-03-11 06:46:20 +0000508 def _SubmanifestToXml(self, r, doc, root):
509 """Generate XML <submanifest/> node."""
510 e = doc.createElement("submanifest")
511 root.appendChild(e)
512 e.setAttribute("name", r.name)
513 if r.remote is not None:
514 e.setAttribute("remote", r.remote)
515 if r.project is not None:
516 e.setAttribute("project", r.project)
517 if r.manifestName is not None:
518 e.setAttribute("manifest-name", r.manifestName)
519 if r.revision is not None:
520 e.setAttribute("revision", r.revision)
521 if r.path is not None:
522 e.setAttribute("path", r.path)
523 if r.groups:
524 e.setAttribute("groups", r.GetGroupsStr())
525 if r.default_groups:
526 e.setAttribute("default-groups", r.GetDefaultGroupsStr())
LaMont Jonescc879a92021-11-18 22:40:18 +0000527
Gavin Makea2e3302023-03-11 06:46:20 +0000528 for a in r.annotations:
529 if a.keep == "true":
530 ae = doc.createElement("annotation")
531 ae.setAttribute("name", a.name)
532 ae.setAttribute("value", a.value)
533 e.appendChild(ae)
LaMont Jonescc879a92021-11-18 22:40:18 +0000534
Gavin Makea2e3302023-03-11 06:46:20 +0000535 def _ParseList(self, field):
536 """Parse fields that contain flattened lists.
Mike Frysinger51e39d52020-12-04 05:32:06 -0500537
Gavin Makea2e3302023-03-11 06:46:20 +0000538 These are whitespace & comma separated. Empty elements will be
539 discarded.
540 """
541 return [x for x in re.split(r"[,\s]+", field) if x]
Josh Triplett884a3872014-06-12 14:57:29 -0700542
Gavin Makea2e3302023-03-11 06:46:20 +0000543 def ToXml(
544 self,
545 peg_rev=False,
546 peg_rev_upstream=True,
547 peg_rev_dest_branch=True,
548 groups=None,
549 omit_local=False,
550 ):
551 """Return the current manifest XML."""
552 mp = self.manifestProject
Colin Cross5acde752012-03-28 20:15:45 -0700553
Gavin Makea2e3302023-03-11 06:46:20 +0000554 if groups is None:
555 groups = mp.manifest_groups
556 if groups:
557 groups = self._ParseList(groups)
Colin Cross5acde752012-03-28 20:15:45 -0700558
Gavin Makea2e3302023-03-11 06:46:20 +0000559 doc = xml.dom.minidom.Document()
560 root = doc.createElement("manifest")
561 if self.is_submanifest:
562 root.setAttribute("path", self.path_prefix)
563 doc.appendChild(root)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800564
Gavin Makea2e3302023-03-11 06:46:20 +0000565 # Save out the notice. There's a little bit of work here to give it the
566 # right whitespace, which assumes that the notice is automatically
567 # indented by 4 by minidom.
568 if self.notice:
569 notice_element = root.appendChild(doc.createElement("notice"))
570 notice_lines = self.notice.splitlines()
571 indented_notice = (
572 "\n".join(" " * 4 + line for line in notice_lines)
573 )[4:]
574 notice_element.appendChild(doc.createTextNode(indented_notice))
Doug Anderson2b8db3c2010-11-01 15:08:06 -0700575
Gavin Makea2e3302023-03-11 06:46:20 +0000576 d = self.default
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800577
Gavin Makea2e3302023-03-11 06:46:20 +0000578 for r in sorted(self.remotes):
579 self._RemoteToXml(self.remotes[r], doc, root)
580 if self.remotes:
581 root.appendChild(doc.createTextNode(""))
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800582
Gavin Makea2e3302023-03-11 06:46:20 +0000583 have_default = False
584 e = doc.createElement("default")
585 if d.remote:
586 have_default = True
587 e.setAttribute("remote", d.remote.name)
588 if d.revisionExpr:
589 have_default = True
590 e.setAttribute("revision", d.revisionExpr)
591 if d.destBranchExpr:
592 have_default = True
593 e.setAttribute("dest-branch", d.destBranchExpr)
594 if d.upstreamExpr:
595 have_default = True
596 e.setAttribute("upstream", d.upstreamExpr)
597 if d.sync_j is not None:
598 have_default = True
599 e.setAttribute("sync-j", "%d" % d.sync_j)
600 if d.sync_c:
601 have_default = True
602 e.setAttribute("sync-c", "true")
603 if d.sync_s:
604 have_default = True
605 e.setAttribute("sync-s", "true")
606 if not d.sync_tags:
607 have_default = True
608 e.setAttribute("sync-tags", "false")
609 if have_default:
610 root.appendChild(e)
611 root.appendChild(doc.createTextNode(""))
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800612
Gavin Makea2e3302023-03-11 06:46:20 +0000613 if self._manifest_server:
614 e = doc.createElement("manifest-server")
615 e.setAttribute("url", self._manifest_server)
616 root.appendChild(e)
617 root.appendChild(doc.createTextNode(""))
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700618
Gavin Makea2e3302023-03-11 06:46:20 +0000619 for r in sorted(self.submanifests):
620 self._SubmanifestToXml(self.submanifests[r], doc, root)
621 if self.submanifests:
622 root.appendChild(doc.createTextNode(""))
LaMont Jonescc879a92021-11-18 22:40:18 +0000623
Gavin Makea2e3302023-03-11 06:46:20 +0000624 def output_projects(parent, parent_node, projects):
625 for project_name in projects:
626 for project in self._projects[project_name]:
627 output_project(parent, parent_node, project)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800628
Gavin Makea2e3302023-03-11 06:46:20 +0000629 def output_project(parent, parent_node, p):
630 if not p.MatchesGroups(groups):
631 return
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800632
Gavin Makea2e3302023-03-11 06:46:20 +0000633 if omit_local and self.IsFromLocalManifest(p):
634 return
LaMont Jonesa8cf5752022-07-15 20:31:33 +0000635
Gavin Makea2e3302023-03-11 06:46:20 +0000636 name = p.name
637 relpath = p.relpath
638 if parent:
639 name = self._UnjoinName(parent.name, name)
640 relpath = self._UnjoinRelpath(parent.relpath, relpath)
Colin Cross5acde752012-03-28 20:15:45 -0700641
Gavin Makea2e3302023-03-11 06:46:20 +0000642 e = doc.createElement("project")
643 parent_node.appendChild(e)
644 e.setAttribute("name", name)
645 if relpath != name:
646 e.setAttribute("path", relpath)
647 remoteName = None
648 if d.remote:
649 remoteName = d.remote.name
650 if not d.remote or p.remote.orig_name != remoteName:
651 remoteName = p.remote.orig_name
652 e.setAttribute("remote", remoteName)
653 if peg_rev:
654 if self.IsMirror:
655 value = p.bare_git.rev_parse(p.revisionExpr + "^0")
656 else:
657 value = p.work_git.rev_parse(HEAD + "^0")
658 e.setAttribute("revision", value)
659 if peg_rev_upstream:
660 if p.upstream:
661 e.setAttribute("upstream", p.upstream)
662 elif value != p.revisionExpr:
663 # Only save the origin if the origin is not a sha1, and
664 # the default isn't our value
665 e.setAttribute("upstream", p.revisionExpr)
666
667 if peg_rev_dest_branch:
668 if p.dest_branch:
669 e.setAttribute("dest-branch", p.dest_branch)
670 elif value != p.revisionExpr:
671 e.setAttribute("dest-branch", p.revisionExpr)
672
673 else:
674 revision = (
675 self.remotes[p.remote.orig_name].revision or d.revisionExpr
676 )
677 if not revision or revision != p.revisionExpr:
678 e.setAttribute("revision", p.revisionExpr)
679 elif p.revisionId:
680 e.setAttribute("revision", p.revisionId)
681 if p.upstream and (
682 p.upstream != p.revisionExpr or p.upstream != d.upstreamExpr
683 ):
684 e.setAttribute("upstream", p.upstream)
685
686 if p.dest_branch and p.dest_branch != d.destBranchExpr:
687 e.setAttribute("dest-branch", p.dest_branch)
688
689 for c in p.copyfiles:
690 ce = doc.createElement("copyfile")
691 ce.setAttribute("src", c.src)
692 ce.setAttribute("dest", c.dest)
693 e.appendChild(ce)
694
695 for lf in p.linkfiles:
696 le = doc.createElement("linkfile")
697 le.setAttribute("src", lf.src)
698 le.setAttribute("dest", lf.dest)
699 e.appendChild(le)
700
701 default_groups = ["all", "name:%s" % p.name, "path:%s" % p.relpath]
702 egroups = [g for g in p.groups if g not in default_groups]
703 if egroups:
704 e.setAttribute("groups", ",".join(egroups))
705
706 for a in p.annotations:
707 if a.keep == "true":
708 ae = doc.createElement("annotation")
709 ae.setAttribute("name", a.name)
710 ae.setAttribute("value", a.value)
711 e.appendChild(ae)
712
713 if p.sync_c:
714 e.setAttribute("sync-c", "true")
715
716 if p.sync_s:
717 e.setAttribute("sync-s", "true")
718
719 if not p.sync_tags:
720 e.setAttribute("sync-tags", "false")
721
722 if p.clone_depth:
723 e.setAttribute("clone-depth", str(p.clone_depth))
724
725 self._output_manifest_project_extras(p, e)
726
727 if p.subprojects:
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545728 subprojects = {subp.name for subp in p.subprojects}
Gavin Makea2e3302023-03-11 06:46:20 +0000729 output_projects(p, e, list(sorted(subprojects)))
730
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545731 projects = {p.name for p in self._paths.values() if not p.parent}
Gavin Makea2e3302023-03-11 06:46:20 +0000732 output_projects(None, root, list(sorted(projects)))
733
734 if self._repo_hooks_project:
735 root.appendChild(doc.createTextNode(""))
736 e = doc.createElement("repo-hooks")
737 e.setAttribute("in-project", self._repo_hooks_project.name)
738 e.setAttribute(
739 "enabled-list",
740 " ".join(self._repo_hooks_project.enabled_repo_hooks),
741 )
742 root.appendChild(e)
743
744 if self._superproject:
745 root.appendChild(doc.createTextNode(""))
746 e = doc.createElement("superproject")
747 e.setAttribute("name", self._superproject.name)
748 remoteName = None
749 if d.remote:
750 remoteName = d.remote.name
751 remote = self._superproject.remote
752 if not d.remote or remote.orig_name != remoteName:
753 remoteName = remote.orig_name
754 e.setAttribute("remote", remoteName)
755 revision = remote.revision or d.revisionExpr
756 if not revision or revision != self._superproject.revision:
757 e.setAttribute("revision", self._superproject.revision)
758 root.appendChild(e)
759
760 if self._contactinfo.bugurl != Wrapper().BUG_URL:
761 root.appendChild(doc.createTextNode(""))
762 e = doc.createElement("contactinfo")
763 e.setAttribute("bugurl", self._contactinfo.bugurl)
764 root.appendChild(e)
765
766 return doc
767
768 def ToDict(self, **kwargs):
769 """Return the current manifest as a dictionary."""
770 # Elements that may only appear once.
771 SINGLE_ELEMENTS = {
772 "notice",
773 "default",
774 "manifest-server",
775 "repo-hooks",
776 "superproject",
777 "contactinfo",
778 }
779 # Elements that may be repeated.
780 MULTI_ELEMENTS = {
781 "remote",
782 "remove-project",
783 "project",
784 "extend-project",
785 "include",
786 "submanifest",
787 # These are children of 'project' nodes.
788 "annotation",
789 "project",
790 "copyfile",
791 "linkfile",
792 }
793
794 doc = self.ToXml(**kwargs)
795 ret = {}
796
797 def append_children(ret, node):
798 for child in node.childNodes:
799 if child.nodeType == xml.dom.Node.ELEMENT_NODE:
800 attrs = child.attributes
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545801 element = {
802 attrs.item(i).localName: attrs.item(i).value
Gavin Makea2e3302023-03-11 06:46:20 +0000803 for i in range(attrs.length)
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545804 }
Gavin Makea2e3302023-03-11 06:46:20 +0000805 if child.nodeName in SINGLE_ELEMENTS:
806 ret[child.nodeName] = element
807 elif child.nodeName in MULTI_ELEMENTS:
808 ret.setdefault(child.nodeName, []).append(element)
809 else:
810 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400811 f'Unhandled element "{child.nodeName}"'
Gavin Makea2e3302023-03-11 06:46:20 +0000812 )
813
814 append_children(element, child)
815
816 append_children(ret, doc.firstChild)
817
818 return ret
819
820 def Save(self, fd, **kwargs):
821 """Write the current manifest out to the given file descriptor."""
822 doc = self.ToXml(**kwargs)
823 doc.writexml(fd, "", " ", "\n", "UTF-8")
824
825 def _output_manifest_project_extras(self, p, e):
826 """Manifests can modify e if they support extra project attributes."""
827
828 @property
829 def is_multimanifest(self):
830 """Whether this is a multimanifest checkout.
831
832 This is safe to use as long as the outermost manifest XML has been
833 parsed.
834 """
835 return bool(self._outer_client._submanifests)
836
837 @property
838 def is_submanifest(self):
839 """Whether this manifest is a submanifest.
840
841 This is safe to use as long as the outermost manifest XML has been
842 parsed.
843 """
844 return self._outer_client and self._outer_client != self
845
846 @property
847 def outer_client(self):
848 """The instance of the outermost manifest client."""
849 self._Load()
850 return self._outer_client
851
852 @property
853 def all_manifests(self):
854 """Generator yielding all (sub)manifests, in depth-first order."""
855 self._Load()
856 outer = self._outer_client
857 yield outer
Jason R. Coombs8dd85212023-10-20 06:48:20 -0400858 yield from outer.all_children
Gavin Makea2e3302023-03-11 06:46:20 +0000859
860 @property
861 def all_children(self):
862 """Generator yielding all (present) child submanifests."""
863 self._Load()
864 for child in self._submanifests.values():
865 if child.repo_client:
866 yield child.repo_client
Jason R. Coombs8dd85212023-10-20 06:48:20 -0400867 yield from child.repo_client.all_children
Gavin Makea2e3302023-03-11 06:46:20 +0000868
869 @property
870 def path_prefix(self):
871 """The path of this submanifest, relative to the outermost manifest."""
872 if not self._outer_client or self == self._outer_client:
873 return ""
874 return os.path.relpath(self.topdir, self._outer_client.topdir)
875
876 @property
877 def all_paths(self):
878 """All project paths for all (sub)manifests.
879
880 See also `paths`.
881
882 Returns:
883 A dictionary of {path: Project()}. `path` is relative to the outer
884 manifest.
885 """
886 ret = {}
887 for tree in self.all_manifests:
888 prefix = tree.path_prefix
889 ret.update(
890 {os.path.join(prefix, k): v for k, v in tree.paths.items()}
891 )
892 return ret
893
894 @property
895 def all_projects(self):
896 """All projects for all (sub)manifests. See `projects`."""
897 return list(
898 itertools.chain.from_iterable(
899 x._paths.values() for x in self.all_manifests
900 )
901 )
902
903 @property
904 def paths(self):
905 """Return all paths for this manifest.
906
907 Returns:
908 A dictionary of {path: Project()}. `path` is relative to this
909 manifest.
910 """
911 self._Load()
912 return self._paths
913
914 @property
915 def projects(self):
916 """Return a list of all Projects in this manifest."""
917 self._Load()
918 return list(self._paths.values())
919
920 @property
921 def remotes(self):
922 """Return a list of remotes for this manifest."""
923 self._Load()
924 return self._remotes
925
926 @property
927 def default(self):
928 """Return default values for this manifest."""
929 self._Load()
930 return self._default
931
932 @property
933 def submanifests(self):
934 """All submanifests in this manifest."""
935 self._Load()
936 return self._submanifests
937
938 @property
939 def repo_hooks_project(self):
940 self._Load()
941 return self._repo_hooks_project
942
943 @property
944 def superproject(self):
945 self._Load()
946 return self._superproject
947
948 @property
949 def contactinfo(self):
950 self._Load()
951 return self._contactinfo
952
953 @property
954 def notice(self):
955 self._Load()
956 return self._notice
957
958 @property
959 def manifest_server(self):
960 self._Load()
961 return self._manifest_server
962
963 @property
964 def CloneBundle(self):
965 clone_bundle = self.manifestProject.clone_bundle
966 if clone_bundle is None:
967 return False if self.manifestProject.partial_clone else True
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800968 else:
Gavin Makea2e3302023-03-11 06:46:20 +0000969 return clone_bundle
Sean McAllisteraf908cb2020-04-20 08:41:58 -0600970
Gavin Makea2e3302023-03-11 06:46:20 +0000971 @property
972 def CloneFilter(self):
973 if self.manifestProject.partial_clone:
974 return self.manifestProject.clone_filter
975 return None
Sean McAllisteraf908cb2020-04-20 08:41:58 -0600976
Gavin Makea2e3302023-03-11 06:46:20 +0000977 @property
Jason Chang17833322023-05-23 13:06:55 -0700978 def CloneFilterForDepth(self):
979 if self.manifestProject.clone_filter_for_depth:
980 return self.manifestProject.clone_filter_for_depth
981 return None
982
983 @property
Gavin Makea2e3302023-03-11 06:46:20 +0000984 def PartialCloneExclude(self):
985 exclude = self.manifest.manifestProject.partial_clone_exclude or ""
Jason R. Coombs0bcffd82023-10-20 23:29:42 +0545986 return {x.strip() for x in exclude.split(",")}
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800987
Gavin Makea2e3302023-03-11 06:46:20 +0000988 def SetManifestOverride(self, path):
989 """Override manifestFile. The caller must call Unload()"""
990 self._outer_client.manifest.manifestFileOverrides[
991 self.path_prefix
992 ] = path
Simon Ruggier7e59de22015-07-24 12:50:06 +0200993
Gavin Makea2e3302023-03-11 06:46:20 +0000994 @property
995 def UseLocalManifests(self):
996 return self._load_local_manifests
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800997
Gavin Makea2e3302023-03-11 06:46:20 +0000998 def SetUseLocalManifests(self, value):
999 self._load_local_manifests = value
Jeff Hamiltone0df2322014-04-21 17:10:59 -05001000
Gavin Makea2e3302023-03-11 06:46:20 +00001001 @property
1002 def HasLocalManifests(self):
1003 return self._load_local_manifests and self.local_manifests
Colin Cross5acde752012-03-28 20:15:45 -07001004
Gavin Makea2e3302023-03-11 06:46:20 +00001005 def IsFromLocalManifest(self, project):
1006 """Is the project from a local manifest?"""
1007 return any(
1008 x.startswith(LOCAL_MANIFEST_GROUP_PREFIX) for x in project.groups
1009 )
James W. Mills24c13082012-04-12 15:04:13 -05001010
Gavin Makea2e3302023-03-11 06:46:20 +00001011 @property
1012 def IsMirror(self):
1013 return self.manifestProject.mirror
Anatol Pomazau79770d22012-04-20 14:41:59 -07001014
Gavin Makea2e3302023-03-11 06:46:20 +00001015 @property
1016 def UseGitWorktrees(self):
1017 return self.manifestProject.use_worktree
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001018
Gavin Makea2e3302023-03-11 06:46:20 +00001019 @property
1020 def IsArchive(self):
1021 return self.manifestProject.archive
YOUNG HO CHAa32c92c2018-02-14 16:57:31 +09001022
Gavin Makea2e3302023-03-11 06:46:20 +00001023 @property
1024 def HasSubmodules(self):
1025 return self.manifestProject.submodules
Dan Willemsen88409222015-08-17 15:29:10 -07001026
Gavin Makea2e3302023-03-11 06:46:20 +00001027 @property
1028 def EnableGitLfs(self):
1029 return self.manifestProject.git_lfs
Simran Basib9a1b732015-08-20 12:19:28 -07001030
Gavin Makea2e3302023-03-11 06:46:20 +00001031 def FindManifestByPath(self, path):
1032 """Returns the manifest containing path."""
1033 path = os.path.abspath(path)
1034 manifest = self._outer_client or self
1035 old = None
1036 while manifest._submanifests and manifest != old:
1037 old = manifest
1038 for name in manifest._submanifests:
1039 tree = manifest._submanifests[name]
1040 if path.startswith(tree.repo_client.manifest.topdir):
1041 manifest = tree.repo_client
1042 break
1043 return manifest
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001044
Gavin Makea2e3302023-03-11 06:46:20 +00001045 @property
1046 def subdir(self):
1047 """Returns the path for per-submanifest objects for this manifest."""
1048 return self.SubmanifestInfoDir(self.path_prefix)
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001049
Gavin Makea2e3302023-03-11 06:46:20 +00001050 def SubmanifestInfoDir(self, submanifest_path, object_path=""):
1051 """Return the path to submanifest-specific info for a submanifest.
Doug Anderson37282b42011-03-04 11:54:18 -08001052
Gavin Makea2e3302023-03-11 06:46:20 +00001053 Return the full path of the directory in which to put per-manifest
1054 objects.
Raman Tenneti1bb4fb22021-01-07 16:50:45 -08001055
Gavin Makea2e3302023-03-11 06:46:20 +00001056 Args:
1057 submanifest_path: a string, the path of the submanifest, relative to
1058 the outermost topdir. If empty, then repodir is returned.
1059 object_path: a string, relative path to append to the submanifest
1060 info directory path.
1061 """
1062 if submanifest_path:
1063 return os.path.join(
1064 self.repodir, SUBMANIFEST_DIR, submanifest_path, object_path
1065 )
1066 else:
1067 return os.path.join(self.repodir, object_path)
Raman Tenneti1c3f57e2021-05-04 12:32:13 -07001068
Gavin Makea2e3302023-03-11 06:46:20 +00001069 def SubmanifestProject(self, submanifest_path):
1070 """Return a manifestProject for a submanifest."""
1071 subdir = self.SubmanifestInfoDir(submanifest_path)
1072 mp = ManifestProject(
1073 self,
1074 "manifests",
1075 gitdir=os.path.join(subdir, "manifests.git"),
1076 worktree=os.path.join(subdir, "manifests"),
1077 )
1078 return mp
Mike Frysinger23411d32020-09-02 04:31:10 -04001079
Gavin Makea2e3302023-03-11 06:46:20 +00001080 def GetDefaultGroupsStr(self, with_platform=True):
1081 """Returns the default group string to use.
Mike Frysinger23411d32020-09-02 04:31:10 -04001082
Gavin Makea2e3302023-03-11 06:46:20 +00001083 Args:
1084 with_platform: a boolean, whether to include the group for the
1085 underlying platform.
1086 """
1087 groups = ",".join(self.default_groups or ["default"])
1088 if with_platform:
1089 groups += f",platform-{platform.system().lower()}"
1090 return groups
Mike Frysinger23411d32020-09-02 04:31:10 -04001091
Gavin Makea2e3302023-03-11 06:46:20 +00001092 def GetGroupsStr(self):
1093 """Returns the manifest group string that should be synced."""
1094 return (
1095 self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
1096 )
Mike Frysinger23411d32020-09-02 04:31:10 -04001097
Gavin Makea2e3302023-03-11 06:46:20 +00001098 def Unload(self):
1099 """Unload the manifest.
Mike Frysinger23411d32020-09-02 04:31:10 -04001100
Gavin Makea2e3302023-03-11 06:46:20 +00001101 If the manifest files have been changed since Load() was called, this
1102 will cause the new/updated manifest to be used.
Mike Frysinger23411d32020-09-02 04:31:10 -04001103
Gavin Makea2e3302023-03-11 06:46:20 +00001104 """
1105 self._loaded = False
1106 self._projects = {}
1107 self._paths = {}
1108 self._remotes = {}
1109 self._default = None
1110 self._submanifests = {}
1111 self._repo_hooks_project = None
1112 self._superproject = None
1113 self._contactinfo = ContactInfo(Wrapper().BUG_URL)
1114 self._notice = None
1115 self.branch = None
1116 self._manifest_server = None
Mike Frysinger23411d32020-09-02 04:31:10 -04001117
Gavin Makea2e3302023-03-11 06:46:20 +00001118 def Load(self):
1119 """Read the manifest into memory."""
1120 # Do not expose internal arguments.
1121 self._Load()
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -08001122
Gavin Makea2e3302023-03-11 06:46:20 +00001123 def _Load(self, initial_client=None, submanifest_depth=0):
1124 if submanifest_depth > MAX_SUBMANIFEST_DEPTH:
1125 raise ManifestParseError(
1126 "maximum submanifest depth %d exceeded." % MAX_SUBMANIFEST_DEPTH
1127 )
1128 if not self._loaded:
1129 if self._outer_client and self._outer_client != self:
1130 # This will load all clients.
1131 self._outer_client._Load(initial_client=self)
Simran Basib9a1b732015-08-20 12:19:28 -07001132
Gavin Makea2e3302023-03-11 06:46:20 +00001133 savedManifestFile = self.manifestFile
1134 override = self._outer_client.manifestFileOverrides.get(
1135 self.path_prefix
1136 )
1137 if override:
1138 self.manifestFile = override
Mike Frysinger1d00a7e2021-12-21 00:40:31 -05001139
Gavin Makea2e3302023-03-11 06:46:20 +00001140 try:
1141 m = self.manifestProject
1142 b = m.GetBranch(m.CurrentBranch).merge
1143 if b is not None and b.startswith(R_HEADS):
1144 b = b[len(R_HEADS) :]
1145 self.branch = b
LaMont Jonescc879a92021-11-18 22:40:18 +00001146
Gavin Makea2e3302023-03-11 06:46:20 +00001147 parent_groups = self.parent_groups
1148 if self.path_prefix:
1149 parent_groups = (
1150 f"{SUBMANIFEST_GROUP_PREFIX}:path:"
1151 f"{self.path_prefix},{parent_groups}"
1152 )
LaMont Jonesff6b1da2022-06-01 21:03:34 +00001153
Gavin Makea2e3302023-03-11 06:46:20 +00001154 # The manifestFile was specified by the user which is why we
1155 # allow include paths to point anywhere.
1156 nodes = []
1157 nodes.append(
1158 self._ParseManifestXml(
1159 self.manifestFile,
1160 self.manifestProject.worktree,
1161 parent_groups=parent_groups,
1162 restrict_includes=False,
1163 )
1164 )
LaMont Jonescc879a92021-11-18 22:40:18 +00001165
Gavin Makea2e3302023-03-11 06:46:20 +00001166 if self._load_local_manifests and self.local_manifests:
1167 try:
1168 for local_file in sorted(
1169 platform_utils.listdir(self.local_manifests)
1170 ):
1171 if local_file.endswith(".xml"):
1172 local = os.path.join(
1173 self.local_manifests, local_file
1174 )
1175 # Since local manifests are entirely managed by
1176 # the user, allow them to point anywhere the
1177 # user wants.
1178 local_group = (
1179 f"{LOCAL_MANIFEST_GROUP_PREFIX}:"
1180 f"{local_file[:-4]}"
1181 )
1182 nodes.append(
1183 self._ParseManifestXml(
1184 local,
1185 self.subdir,
1186 parent_groups=(
1187 f"{local_group},{parent_groups}"
1188 ),
1189 restrict_includes=False,
1190 )
1191 )
1192 except OSError:
1193 pass
Raman Tenneti080877e2021-03-09 15:19:06 -08001194
Gavin Makea2e3302023-03-11 06:46:20 +00001195 try:
1196 self._ParseManifest(nodes)
1197 except ManifestParseError as e:
1198 # There was a problem parsing, unload ourselves in case they
1199 # catch this error and try again later, we will show the
1200 # correct error
1201 self.Unload()
1202 raise e
Raman Tenneti080877e2021-03-09 15:19:06 -08001203
Gavin Makea2e3302023-03-11 06:46:20 +00001204 if self.IsMirror:
1205 self._AddMetaProjectMirror(self.repoProject)
1206 self._AddMetaProjectMirror(self.manifestProject)
LaMont Jonesa2ff20d2022-04-07 16:49:06 +00001207
Gavin Makea2e3302023-03-11 06:46:20 +00001208 self._loaded = True
1209 finally:
1210 if override:
1211 self.manifestFile = savedManifestFile
LaMont Jonesa2ff20d2022-04-07 16:49:06 +00001212
Gavin Makea2e3302023-03-11 06:46:20 +00001213 # Now that we have loaded this manifest, load any submanifests as
1214 # well. We need to do this after self._loaded is set to avoid
1215 # looping.
1216 for name in self._submanifests:
1217 tree = self._submanifests[name]
1218 tree.ToSubmanifestSpec()
1219 present = os.path.exists(
1220 os.path.join(self.subdir, MANIFEST_FILE_NAME)
1221 )
1222 if present and tree.present and not tree.repo_client:
1223 if initial_client and initial_client.topdir == self.topdir:
1224 tree.repo_client = self
1225 tree.present = present
1226 elif not os.path.exists(self.subdir):
1227 tree.present = False
1228 if present and tree.present:
1229 tree.repo_client._Load(
1230 initial_client=initial_client,
1231 submanifest_depth=submanifest_depth + 1,
1232 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001233
Gavin Makea2e3302023-03-11 06:46:20 +00001234 def _ParseManifestXml(
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001235 self,
1236 path,
1237 include_root,
1238 parent_groups="",
1239 restrict_includes=True,
1240 parent_node=None,
Gavin Makea2e3302023-03-11 06:46:20 +00001241 ):
1242 """Parse a manifest XML and return the computed nodes.
LaMont Jonesa2ff20d2022-04-07 16:49:06 +00001243
Gavin Makea2e3302023-03-11 06:46:20 +00001244 Args:
1245 path: The XML file to read & parse.
1246 include_root: The path to interpret include "name"s relative to.
1247 parent_groups: The groups to apply to this projects.
1248 restrict_includes: Whether to constrain the "name" attribute of
1249 includes.
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001250 parent_node: The parent include node, to apply attribute to this
1251 projects.
LaMont Jonescc879a92021-11-18 22:40:18 +00001252
Gavin Makea2e3302023-03-11 06:46:20 +00001253 Returns:
1254 List of XML nodes.
1255 """
1256 try:
1257 root = xml.dom.minidom.parse(path)
1258 except (OSError, xml.parsers.expat.ExpatError) as e:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001259 raise ManifestParseError(f"error parsing manifest {path}: {e}")
David Pursehouse2d5a0df2012-11-13 02:50:36 +09001260
Gavin Makea2e3302023-03-11 06:46:20 +00001261 if not root or not root.childNodes:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001262 raise ManifestParseError(f"no root node in {path}")
Shawn O. Pearce5cc66792008-10-23 16:19:27 -07001263
Gavin Makea2e3302023-03-11 06:46:20 +00001264 for manifest in root.childNodes:
Chris Allen7393f6b2023-10-20 16:35:39 +01001265 if (
1266 manifest.nodeType == manifest.ELEMENT_NODE
1267 and manifest.nodeName == "manifest"
1268 ):
Gavin Makea2e3302023-03-11 06:46:20 +00001269 break
1270 else:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001271 raise ManifestParseError(f"no <manifest> in {path}")
Shawn O. Pearcee284ad12008-11-04 07:37:10 -08001272
LaMont Jonesb90a4222022-04-14 15:00:09 +00001273 nodes = []
Gavin Makea2e3302023-03-11 06:46:20 +00001274 for node in manifest.childNodes:
1275 if node.nodeName == "include":
1276 name = self._reqatt(node, "name")
1277 if restrict_includes:
1278 msg = self._CheckLocalPath(name)
1279 if msg:
1280 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001281 f'<include> invalid "name": {name}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001282 )
1283 include_groups = ""
1284 if parent_groups:
1285 include_groups = parent_groups
1286 if node.hasAttribute("groups"):
1287 include_groups = (
1288 node.getAttribute("groups") + "," + include_groups
1289 )
1290 fp = os.path.join(include_root, name)
1291 if not os.path.isfile(fp):
1292 raise ManifestParseError(
1293 "include [%s/]%s doesn't exist or isn't a file"
1294 % (include_root, name)
1295 )
1296 try:
1297 nodes.extend(
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001298 self._ParseManifestXml(
1299 fp, include_root, include_groups, parent_node=node
1300 )
Gavin Makea2e3302023-03-11 06:46:20 +00001301 )
1302 # should isolate this to the exact exception, but that's
1303 # tricky. actual parsing implementation may vary.
1304 except (
1305 KeyboardInterrupt,
1306 RuntimeError,
1307 SystemExit,
1308 ManifestParseError,
1309 ):
1310 raise
1311 except Exception as e:
1312 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001313 f"failed parsing included manifest {name}: {e}"
Gavin Makea2e3302023-03-11 06:46:20 +00001314 )
1315 else:
1316 if parent_groups and node.nodeName == "project":
1317 nodeGroups = parent_groups
1318 if node.hasAttribute("groups"):
1319 nodeGroups = (
1320 node.getAttribute("groups") + "," + nodeGroups
1321 )
1322 node.setAttribute("groups", nodeGroups)
Shuchuan Zeng3e3340d2023-04-18 10:36:50 +08001323 if (
1324 parent_node
1325 and node.nodeName == "project"
1326 and not node.hasAttribute("revision")
1327 ):
1328 node.setAttribute(
1329 "revision", parent_node.getAttribute("revision")
1330 )
Gavin Makea2e3302023-03-11 06:46:20 +00001331 nodes.append(node)
1332 return nodes
LaMont Jonesb90a4222022-04-14 15:00:09 +00001333
Gavin Makea2e3302023-03-11 06:46:20 +00001334 def _ParseManifest(self, node_list):
1335 for node in itertools.chain(*node_list):
1336 if node.nodeName == "remote":
1337 remote = self._ParseRemote(node)
1338 if remote:
1339 if remote.name in self._remotes:
1340 if remote != self._remotes[remote.name]:
1341 raise ManifestParseError(
1342 "remote %s already exists with different "
1343 "attributes" % (remote.name)
1344 )
1345 else:
1346 self._remotes[remote.name] = remote
LaMont Jonesb90a4222022-04-14 15:00:09 +00001347
Gavin Makea2e3302023-03-11 06:46:20 +00001348 for node in itertools.chain(*node_list):
1349 if node.nodeName == "default":
1350 new_default = self._ParseDefault(node)
1351 emptyDefault = (
1352 not node.hasAttributes() and not node.hasChildNodes()
1353 )
1354 if self._default is None:
1355 self._default = new_default
1356 elif not emptyDefault and new_default != self._default:
1357 raise ManifestParseError(
1358 "duplicate default in %s" % (self.manifestFile)
1359 )
LaMont Jonesb90a4222022-04-14 15:00:09 +00001360
Julien Campergue74879922013-10-09 14:38:46 +02001361 if self._default is None:
Gavin Makea2e3302023-03-11 06:46:20 +00001362 self._default = _Default()
Julien Campergue74879922013-10-09 14:38:46 +02001363
Gavin Makea2e3302023-03-11 06:46:20 +00001364 submanifest_paths = set()
1365 for node in itertools.chain(*node_list):
1366 if node.nodeName == "submanifest":
1367 submanifest = self._ParseSubmanifest(node)
1368 if submanifest:
1369 if submanifest.name in self._submanifests:
1370 if submanifest != self._submanifests[submanifest.name]:
1371 raise ManifestParseError(
1372 "submanifest %s already exists with different "
1373 "attributes" % (submanifest.name)
1374 )
1375 else:
1376 self._submanifests[submanifest.name] = submanifest
1377 submanifest_paths.add(submanifest.relpath)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001378
Gavin Makea2e3302023-03-11 06:46:20 +00001379 for node in itertools.chain(*node_list):
1380 if node.nodeName == "notice":
1381 if self._notice is not None:
1382 raise ManifestParseError(
1383 "duplicate notice in %s" % (self.manifestFile)
1384 )
1385 self._notice = self._ParseNotice(node)
LaMont Jonescc879a92021-11-18 22:40:18 +00001386
Gavin Makea2e3302023-03-11 06:46:20 +00001387 for node in itertools.chain(*node_list):
1388 if node.nodeName == "manifest-server":
1389 url = self._reqatt(node, "url")
1390 if self._manifest_server is not None:
1391 raise ManifestParseError(
1392 "duplicate manifest-server in %s" % (self.manifestFile)
1393 )
1394 self._manifest_server = url
Doug Anderson2b8db3c2010-11-01 15:08:06 -07001395
Gavin Makea2e3302023-03-11 06:46:20 +00001396 def recursively_add_projects(project):
1397 projects = self._projects.setdefault(project.name, [])
1398 if project.relpath is None:
1399 raise ManifestParseError(
1400 "missing path for %s in %s"
1401 % (project.name, self.manifestFile)
1402 )
1403 if project.relpath in self._paths:
1404 raise ManifestParseError(
1405 "duplicate path %s in %s"
1406 % (project.relpath, self.manifestFile)
1407 )
1408 for tree in submanifest_paths:
1409 if project.relpath.startswith(tree):
1410 raise ManifestParseError(
1411 "project %s conflicts with submanifest path %s"
1412 % (project.relpath, tree)
1413 )
1414 self._paths[project.relpath] = project
1415 projects.append(project)
1416 for subproject in project.subprojects:
1417 recursively_add_projects(subproject)
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -07001418
Gavin Makea2e3302023-03-11 06:46:20 +00001419 repo_hooks_project = None
1420 enabled_repo_hooks = None
1421 for node in itertools.chain(*node_list):
1422 if node.nodeName == "project":
1423 project = self._ParseProject(node)
1424 recursively_add_projects(project)
1425 if node.nodeName == "extend-project":
1426 name = self._reqatt(node, "name")
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +08001427
Gavin Makea2e3302023-03-11 06:46:20 +00001428 if name not in self._projects:
1429 raise ManifestParseError(
1430 "extend-project element specifies non-existent "
1431 "project: %s" % name
1432 )
1433
1434 path = node.getAttribute("path")
1435 dest_path = node.getAttribute("dest-path")
1436 groups = node.getAttribute("groups")
1437 if groups:
1438 groups = self._ParseList(groups)
1439 revision = node.getAttribute("revision")
1440 remote_name = node.getAttribute("remote")
1441 if not remote_name:
1442 remote = self._default.remote
1443 else:
1444 remote = self._get_remote(node)
1445 dest_branch = node.getAttribute("dest-branch")
1446 upstream = node.getAttribute("upstream")
1447
1448 named_projects = self._projects[name]
1449 if dest_path and not path and len(named_projects) > 1:
1450 raise ManifestParseError(
1451 "extend-project cannot use dest-path when "
1452 "matching multiple projects: %s" % name
1453 )
1454 for p in self._projects[name]:
1455 if path and p.relpath != path:
1456 continue
1457 if groups:
1458 p.groups.extend(groups)
1459 if revision:
1460 p.SetRevision(revision)
1461
1462 if remote_name:
1463 p.remote = remote.ToRemoteSpec(name)
1464 if dest_branch:
1465 p.dest_branch = dest_branch
1466 if upstream:
1467 p.upstream = upstream
1468
1469 if dest_path:
1470 del self._paths[p.relpath]
1471 (
1472 relpath,
1473 worktree,
1474 gitdir,
1475 objdir,
1476 _,
1477 ) = self.GetProjectPaths(name, dest_path, remote.name)
1478 p.UpdatePaths(relpath, worktree, gitdir, objdir)
1479 self._paths[p.relpath] = p
1480
1481 if node.nodeName == "repo-hooks":
1482 # Only one project can be the hooks project
1483 if repo_hooks_project is not None:
1484 raise ManifestParseError(
1485 "duplicate repo-hooks in %s" % (self.manifestFile)
1486 )
1487
1488 # Get the name of the project and the (space-separated) list of
1489 # enabled.
1490 repo_hooks_project = self._reqatt(node, "in-project")
1491 enabled_repo_hooks = self._ParseList(
1492 self._reqatt(node, "enabled-list")
1493 )
1494 if node.nodeName == "superproject":
1495 name = self._reqatt(node, "name")
1496 # There can only be one superproject.
1497 if self._superproject:
1498 raise ManifestParseError(
1499 "duplicate superproject in %s" % (self.manifestFile)
1500 )
1501 remote_name = node.getAttribute("remote")
1502 if not remote_name:
1503 remote = self._default.remote
1504 else:
1505 remote = self._get_remote(node)
1506 if remote is None:
1507 raise ManifestParseError(
1508 "no remote for superproject %s within %s"
1509 % (name, self.manifestFile)
1510 )
1511 revision = node.getAttribute("revision") or remote.revision
1512 if not revision:
1513 revision = self._default.revisionExpr
1514 if not revision:
1515 raise ManifestParseError(
1516 "no revision for superproject %s within %s"
1517 % (name, self.manifestFile)
1518 )
1519 self._superproject = Superproject(
1520 self,
1521 name=name,
1522 remote=remote.ToRemoteSpec(name),
1523 revision=revision,
1524 )
1525 if node.nodeName == "contactinfo":
1526 bugurl = self._reqatt(node, "bugurl")
1527 # This element can be repeated, later entries will clobber
1528 # earlier ones.
1529 self._contactinfo = ContactInfo(bugurl)
1530
1531 if node.nodeName == "remove-project":
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001532 name = node.getAttribute("name")
1533 path = node.getAttribute("path")
Gavin Makea2e3302023-03-11 06:46:20 +00001534
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001535 # Name or path needed.
1536 if not name and not path:
1537 raise ManifestParseError(
1538 "remove-project must have name and/or path"
1539 )
Gavin Makea2e3302023-03-11 06:46:20 +00001540
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001541 removed_project = ""
1542
1543 # Find and remove projects based on name and/or path.
1544 for projname, projects in list(self._projects.items()):
1545 for p in projects:
1546 if name == projname and not path:
1547 del self._paths[p.relpath]
1548 if not removed_project:
1549 del self._projects[name]
1550 removed_project = name
1551 elif path == p.relpath and (
1552 name == projname or not name
1553 ):
1554 self._projects[projname].remove(p)
1555 del self._paths[p.relpath]
1556 removed_project = p.name
1557
1558 # If the manifest removes the hooks project, treat it as if
1559 # it deleted the repo-hooks element too.
1560 if (
1561 removed_project
1562 and removed_project not in self._projects
1563 and repo_hooks_project == removed_project
1564 ):
1565 repo_hooks_project = None
1566
1567 if not removed_project and not XmlBool(node, "optional", False):
Gavin Makea2e3302023-03-11 06:46:20 +00001568 raise ManifestParseError(
1569 "remove-project element specifies non-existent "
Fredrik de Grootbe71c2f2023-05-31 16:56:34 +02001570 "project: %s" % node.toxml()
Gavin Makea2e3302023-03-11 06:46:20 +00001571 )
1572
1573 # Store repo hooks project information.
1574 if repo_hooks_project:
1575 # Store a reference to the Project.
1576 try:
1577 repo_hooks_projects = self._projects[repo_hooks_project]
1578 except KeyError:
1579 raise ManifestParseError(
1580 "project %s not found for repo-hooks" % (repo_hooks_project)
1581 )
1582
1583 if len(repo_hooks_projects) != 1:
1584 raise ManifestParseError(
1585 "internal error parsing repo-hooks in %s"
1586 % (self.manifestFile)
1587 )
1588 self._repo_hooks_project = repo_hooks_projects[0]
1589 # Store the enabled hooks in the Project object.
1590 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
1591
1592 def _AddMetaProjectMirror(self, m):
1593 name = None
1594 m_url = m.GetRemote().url
1595 if m_url.endswith("/.git"):
1596 raise ManifestParseError("refusing to mirror %s" % m_url)
1597
1598 if self._default and self._default.remote:
1599 url = self._default.remote.resolvedFetchUrl
1600 if not url.endswith("/"):
1601 url += "/"
1602 if m_url.startswith(url):
1603 remote = self._default.remote
1604 name = m_url[len(url) :]
1605
1606 if name is None:
1607 s = m_url.rindex("/") + 1
1608 manifestUrl = self.manifestProject.config.GetString(
1609 "remote.origin.url"
1610 )
1611 remote = _XmlRemote(
1612 "origin", fetch=m_url[:s], manifestUrl=manifestUrl
1613 )
1614 name = m_url[s:]
1615
1616 if name.endswith(".git"):
1617 name = name[:-4]
Josh Triplett884a3872014-06-12 14:57:29 -07001618
1619 if name not in self._projects:
Gavin Makea2e3302023-03-11 06:46:20 +00001620 m.PreSync()
1621 gitdir = os.path.join(self.topdir, "%s.git" % name)
1622 project = Project(
1623 manifest=self,
1624 name=name,
1625 remote=remote.ToRemoteSpec(name),
1626 gitdir=gitdir,
1627 objdir=gitdir,
1628 worktree=None,
1629 relpath=name or None,
1630 revisionExpr=m.revisionExpr,
1631 revisionId=None,
1632 )
1633 self._projects[project.name] = [project]
1634 self._paths[project.relpath] = project
Josh Triplett884a3872014-06-12 14:57:29 -07001635
Gavin Makea2e3302023-03-11 06:46:20 +00001636 def _ParseRemote(self, node):
1637 """
1638 reads a <remote> element from the manifest file
1639 """
1640 name = self._reqatt(node, "name")
1641 alias = node.getAttribute("alias")
1642 if alias == "":
1643 alias = None
1644 fetch = self._reqatt(node, "fetch")
1645 pushUrl = node.getAttribute("pushurl")
1646 if pushUrl == "":
1647 pushUrl = None
1648 review = node.getAttribute("review")
1649 if review == "":
1650 review = None
1651 revision = node.getAttribute("revision")
1652 if revision == "":
1653 revision = None
1654 manifestUrl = self.manifestProject.config.GetString("remote.origin.url")
1655
1656 remote = _XmlRemote(
1657 name, alias, fetch, pushUrl, manifestUrl, review, revision
1658 )
1659
1660 for n in node.childNodes:
1661 if n.nodeName == "annotation":
1662 self._ParseAnnotation(remote, n)
1663
1664 return remote
1665
1666 def _ParseDefault(self, node):
1667 """
1668 reads a <default> element from the manifest file
1669 """
1670 d = _Default()
1671 d.remote = self._get_remote(node)
1672 d.revisionExpr = node.getAttribute("revision")
1673 if d.revisionExpr == "":
1674 d.revisionExpr = None
1675
1676 d.destBranchExpr = node.getAttribute("dest-branch") or None
1677 d.upstreamExpr = node.getAttribute("upstream") or None
1678
1679 d.sync_j = XmlInt(node, "sync-j", None)
1680 if d.sync_j is not None and d.sync_j <= 0:
1681 raise ManifestParseError(
1682 '%s: sync-j must be greater than 0, not "%s"'
1683 % (self.manifestFile, d.sync_j)
1684 )
1685
1686 d.sync_c = XmlBool(node, "sync-c", False)
1687 d.sync_s = XmlBool(node, "sync-s", False)
1688 d.sync_tags = XmlBool(node, "sync-tags", True)
1689 return d
1690
1691 def _ParseNotice(self, node):
1692 """
1693 reads a <notice> element from the manifest file
1694
1695 The <notice> element is distinct from other tags in the XML in that the
1696 data is conveyed between the start and end tag (it's not an
1697 empty-element tag).
1698
1699 The white space (carriage returns, indentation) for the notice element
1700 is relevant and is parsed in a way that is based on how python
1701 docstrings work. In fact, the code is remarkably similar to here:
1702 http://www.python.org/dev/peps/pep-0257/
1703 """
1704 # Get the data out of the node...
1705 notice = node.childNodes[0].data
1706
1707 # Figure out minimum indentation, skipping the first line (the same line
1708 # as the <notice> tag)...
1709 minIndent = sys.maxsize
1710 lines = notice.splitlines()
1711 for line in lines[1:]:
1712 lstrippedLine = line.lstrip()
1713 if lstrippedLine:
1714 indent = len(line) - len(lstrippedLine)
1715 minIndent = min(indent, minIndent)
1716
1717 # Strip leading / trailing blank lines and also indentation.
1718 cleanLines = [lines[0].strip()]
1719 for line in lines[1:]:
1720 cleanLines.append(line[minIndent:].rstrip())
1721
1722 # Clear completely blank lines from front and back...
1723 while cleanLines and not cleanLines[0]:
1724 del cleanLines[0]
1725 while cleanLines and not cleanLines[-1]:
1726 del cleanLines[-1]
1727
1728 return "\n".join(cleanLines)
1729
1730 def _ParseSubmanifest(self, node):
1731 """Reads a <submanifest> element from the manifest file."""
1732 name = self._reqatt(node, "name")
1733 remote = node.getAttribute("remote")
1734 if remote == "":
1735 remote = None
1736 project = node.getAttribute("project")
1737 if project == "":
1738 project = None
1739 revision = node.getAttribute("revision")
1740 if revision == "":
1741 revision = None
1742 manifestName = node.getAttribute("manifest-name")
1743 if manifestName == "":
1744 manifestName = None
1745 groups = ""
1746 if node.hasAttribute("groups"):
1747 groups = node.getAttribute("groups")
1748 groups = self._ParseList(groups)
1749 default_groups = self._ParseList(node.getAttribute("default-groups"))
1750 path = node.getAttribute("path")
1751 if path == "":
1752 path = None
1753 if revision:
1754 msg = self._CheckLocalPath(revision.split("/")[-1])
1755 if msg:
1756 raise ManifestInvalidPathError(
1757 '<submanifest> invalid "revision": %s: %s'
1758 % (revision, msg)
1759 )
1760 else:
1761 msg = self._CheckLocalPath(name)
1762 if msg:
1763 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001764 f'<submanifest> invalid "name": {name}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001765 )
LaMont Jonescc879a92021-11-18 22:40:18 +00001766 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001767 msg = self._CheckLocalPath(path)
1768 if msg:
1769 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001770 f'<submanifest> invalid "path": {path}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001771 )
Josh Triplett884a3872014-06-12 14:57:29 -07001772
Gavin Makea2e3302023-03-11 06:46:20 +00001773 submanifest = _XmlSubmanifest(
1774 name,
1775 remote,
1776 project,
1777 revision,
1778 manifestName,
1779 groups,
1780 default_groups,
1781 path,
1782 self,
1783 )
Michael Kelly2f3c3312020-07-21 19:40:38 -07001784
Gavin Makea2e3302023-03-11 06:46:20 +00001785 for n in node.childNodes:
1786 if n.nodeName == "annotation":
1787 self._ParseAnnotation(submanifest, n)
Michael Kelly2f3c3312020-07-21 19:40:38 -07001788
Gavin Makea2e3302023-03-11 06:46:20 +00001789 return submanifest
Michael Kelly37c21c22020-06-13 02:10:40 -07001790
Gavin Makea2e3302023-03-11 06:46:20 +00001791 def _JoinName(self, parent_name, name):
1792 return os.path.join(parent_name, name)
Doug Anderson37282b42011-03-04 11:54:18 -08001793
Gavin Makea2e3302023-03-11 06:46:20 +00001794 def _UnjoinName(self, parent_name, name):
1795 return os.path.relpath(name, parent_name)
1796
1797 def _ParseProject(self, node, parent=None, **extra_proj_attrs):
1798 """
1799 reads a <project> element from the manifest file
1800 """
1801 name = self._reqatt(node, "name")
1802 msg = self._CheckLocalPath(name, dir_ok=True)
1803 if msg:
1804 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001805 f'<project> invalid "name": {name}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001806 )
1807 if parent:
1808 name = self._JoinName(parent.name, name)
1809
1810 remote = self._get_remote(node)
Raman Tenneti1bb4fb22021-01-07 16:50:45 -08001811 if remote is None:
Gavin Makea2e3302023-03-11 06:46:20 +00001812 remote = self._default.remote
1813 if remote is None:
1814 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001815 f"no remote for project {name} within {self.manifestFile}"
Gavin Makea2e3302023-03-11 06:46:20 +00001816 )
Raman Tenneti993af5e2021-05-12 12:00:31 -07001817
Gavin Makea2e3302023-03-11 06:46:20 +00001818 revisionExpr = node.getAttribute("revision") or remote.revision
1819 if not revisionExpr:
1820 revisionExpr = self._default.revisionExpr
1821 if not revisionExpr:
1822 raise ManifestParseError(
1823 "no revision for project %s within %s"
1824 % (name, self.manifestFile)
1825 )
David Jamesb8433df2014-01-30 10:11:17 -08001826
Gavin Makea2e3302023-03-11 06:46:20 +00001827 path = node.getAttribute("path")
1828 if not path:
1829 path = name
Julien Camperguedd654222014-01-09 16:21:37 +01001830 else:
Gavin Makea2e3302023-03-11 06:46:20 +00001831 # NB: The "." project is handled specially in
1832 # Project.Sync_LocalHalf.
1833 msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
1834 if msg:
1835 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04001836 f'<project> invalid "path": {path}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00001837 )
Julien Camperguedd654222014-01-09 16:21:37 +01001838
Gavin Makea2e3302023-03-11 06:46:20 +00001839 rebase = XmlBool(node, "rebase", True)
1840 sync_c = XmlBool(node, "sync-c", False)
1841 sync_s = XmlBool(node, "sync-s", self._default.sync_s)
1842 sync_tags = XmlBool(node, "sync-tags", self._default.sync_tags)
Julien Camperguedd654222014-01-09 16:21:37 +01001843
Gavin Makea2e3302023-03-11 06:46:20 +00001844 clone_depth = XmlInt(node, "clone-depth")
1845 if clone_depth is not None and clone_depth <= 0:
1846 raise ManifestParseError(
1847 '%s: clone-depth must be greater than 0, not "%s"'
1848 % (self.manifestFile, clone_depth)
1849 )
1850
1851 dest_branch = (
1852 node.getAttribute("dest-branch") or self._default.destBranchExpr
1853 )
1854
1855 upstream = node.getAttribute("upstream") or self._default.upstreamExpr
1856
1857 groups = ""
1858 if node.hasAttribute("groups"):
1859 groups = node.getAttribute("groups")
1860 groups = self._ParseList(groups)
1861
1862 if parent is None:
1863 (
1864 relpath,
1865 worktree,
1866 gitdir,
1867 objdir,
1868 use_git_worktrees,
1869 ) = self.GetProjectPaths(name, path, remote.name)
1870 else:
1871 use_git_worktrees = False
1872 relpath, worktree, gitdir, objdir = self.GetSubprojectPaths(
1873 parent, name, path
1874 )
1875
1876 default_groups = ["all", "name:%s" % name, "path:%s" % relpath]
1877 groups.extend(set(default_groups).difference(groups))
1878
1879 if self.IsMirror and node.hasAttribute("force-path"):
1880 if XmlBool(node, "force-path", False):
1881 gitdir = os.path.join(self.topdir, "%s.git" % path)
1882
1883 project = Project(
1884 manifest=self,
1885 name=name,
1886 remote=remote.ToRemoteSpec(name),
1887 gitdir=gitdir,
1888 objdir=objdir,
1889 worktree=worktree,
1890 relpath=relpath,
1891 revisionExpr=revisionExpr,
1892 revisionId=None,
1893 rebase=rebase,
1894 groups=groups,
1895 sync_c=sync_c,
1896 sync_s=sync_s,
1897 sync_tags=sync_tags,
1898 clone_depth=clone_depth,
1899 upstream=upstream,
1900 parent=parent,
1901 dest_branch=dest_branch,
1902 use_git_worktrees=use_git_worktrees,
1903 **extra_proj_attrs,
1904 )
1905
1906 for n in node.childNodes:
1907 if n.nodeName == "copyfile":
1908 self._ParseCopyFile(project, n)
1909 if n.nodeName == "linkfile":
1910 self._ParseLinkFile(project, n)
1911 if n.nodeName == "annotation":
1912 self._ParseAnnotation(project, n)
1913 if n.nodeName == "project":
1914 project.subprojects.append(
1915 self._ParseProject(n, parent=project)
1916 )
1917
1918 return project
1919
1920 def GetProjectPaths(self, name, path, remote):
1921 """Return the paths for a project.
1922
1923 Args:
1924 name: a string, the name of the project.
1925 path: a string, the path of the project.
1926 remote: a string, the remote.name of the project.
1927
1928 Returns:
1929 A tuple of (relpath, worktree, gitdir, objdir, use_git_worktrees)
1930 for the project with |name| and |path|.
1931 """
1932 # The manifest entries might have trailing slashes. Normalize them to
1933 # avoid unexpected filesystem behavior since we do string concatenation
1934 # below.
1935 path = path.rstrip("/")
1936 name = name.rstrip("/")
1937 remote = remote.rstrip("/")
1938 use_git_worktrees = False
1939 use_remote_name = self.is_multimanifest
1940 relpath = path
1941 if self.IsMirror:
1942 worktree = None
1943 gitdir = os.path.join(self.topdir, "%s.git" % name)
1944 objdir = gitdir
1945 else:
1946 if use_remote_name:
1947 namepath = os.path.join(remote, f"{name}.git")
1948 else:
1949 namepath = f"{name}.git"
1950 worktree = os.path.join(self.topdir, path).replace("\\", "/")
1951 gitdir = os.path.join(self.subdir, "projects", "%s.git" % path)
1952 # We allow people to mix git worktrees & non-git worktrees for now.
1953 # This allows for in situ migration of repo clients.
1954 if os.path.exists(gitdir) or not self.UseGitWorktrees:
1955 objdir = os.path.join(self.repodir, "project-objects", namepath)
1956 else:
1957 use_git_worktrees = True
1958 gitdir = os.path.join(self.repodir, "worktrees", namepath)
1959 objdir = gitdir
1960 return relpath, worktree, gitdir, objdir, use_git_worktrees
1961
1962 def GetProjectsWithName(self, name, all_manifests=False):
1963 """All projects with |name|.
1964
1965 Args:
1966 name: a string, the name of the project.
1967 all_manifests: a boolean, if True, then all manifests are searched.
1968 If False, then only this manifest is searched.
1969
1970 Returns:
1971 A list of Project instances with name |name|.
1972 """
1973 if all_manifests:
1974 return list(
1975 itertools.chain.from_iterable(
1976 x._projects.get(name, []) for x in self.all_manifests
1977 )
1978 )
1979 return self._projects.get(name, [])
1980
1981 def GetSubprojectName(self, parent, submodule_path):
1982 return os.path.join(parent.name, submodule_path)
1983
1984 def _JoinRelpath(self, parent_relpath, relpath):
1985 return os.path.join(parent_relpath, relpath)
1986
1987 def _UnjoinRelpath(self, parent_relpath, relpath):
1988 return os.path.relpath(relpath, parent_relpath)
1989
1990 def GetSubprojectPaths(self, parent, name, path):
1991 # The manifest entries might have trailing slashes. Normalize them to
1992 # avoid unexpected filesystem behavior since we do string concatenation
1993 # below.
1994 path = path.rstrip("/")
1995 name = name.rstrip("/")
1996 relpath = self._JoinRelpath(parent.relpath, path)
1997 gitdir = os.path.join(parent.gitdir, "subprojects", "%s.git" % path)
1998 objdir = os.path.join(
1999 parent.gitdir, "subproject-objects", "%s.git" % name
2000 )
2001 if self.IsMirror:
2002 worktree = None
2003 else:
2004 worktree = os.path.join(parent.worktree, path).replace("\\", "/")
2005 return relpath, worktree, gitdir, objdir
2006
2007 @staticmethod
2008 def _CheckLocalPath(path, dir_ok=False, cwd_dot_ok=False):
2009 """Verify |path| is reasonable for use in filesystem paths.
2010
2011 Used with <copyfile> & <linkfile> & <project> elements.
2012
2013 This only validates the |path| in isolation: it does not check against
2014 the current filesystem state. Thus it is suitable as a first-past in a
2015 parser.
2016
2017 It enforces a number of constraints:
2018 * No empty paths.
2019 * No "~" in paths.
2020 * No Unicode codepoints that filesystems might elide when normalizing.
2021 * No relative path components like "." or "..".
2022 * No absolute paths.
2023 * No ".git" or ".repo*" path components.
2024
2025 Args:
2026 path: The path name to validate.
2027 dir_ok: Whether |path| may force a directory (e.g. end in a /).
2028 cwd_dot_ok: Whether |path| may be just ".".
2029
2030 Returns:
2031 None if |path| is OK, a failure message otherwise.
2032 """
2033 if not path:
2034 return "empty paths not allowed"
2035
2036 if "~" in path:
2037 return "~ not allowed (due to 8.3 filenames on Windows filesystems)"
2038
2039 path_codepoints = set(path)
2040
2041 # Some filesystems (like Apple's HFS+) try to normalize Unicode
2042 # codepoints which means there are alternative names for ".git". Reject
2043 # paths with these in it as there shouldn't be any reasonable need for
2044 # them here. The set of codepoints here was cribbed from jgit's
2045 # implementation:
2046 # https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
2047 BAD_CODEPOINTS = {
2048 "\u200C", # ZERO WIDTH NON-JOINER
2049 "\u200D", # ZERO WIDTH JOINER
2050 "\u200E", # LEFT-TO-RIGHT MARK
2051 "\u200F", # RIGHT-TO-LEFT MARK
2052 "\u202A", # LEFT-TO-RIGHT EMBEDDING
2053 "\u202B", # RIGHT-TO-LEFT EMBEDDING
2054 "\u202C", # POP DIRECTIONAL FORMATTING
2055 "\u202D", # LEFT-TO-RIGHT OVERRIDE
2056 "\u202E", # RIGHT-TO-LEFT OVERRIDE
2057 "\u206A", # INHIBIT SYMMETRIC SWAPPING
2058 "\u206B", # ACTIVATE SYMMETRIC SWAPPING
2059 "\u206C", # INHIBIT ARABIC FORM SHAPING
2060 "\u206D", # ACTIVATE ARABIC FORM SHAPING
2061 "\u206E", # NATIONAL DIGIT SHAPES
2062 "\u206F", # NOMINAL DIGIT SHAPES
2063 "\uFEFF", # ZERO WIDTH NO-BREAK SPACE
2064 }
2065 if BAD_CODEPOINTS & path_codepoints:
2066 # This message is more expansive than reality, but should be fine.
2067 return "Unicode combining characters not allowed"
2068
2069 # Reject newlines as there shouldn't be any legitmate use for them,
2070 # they'll be confusing to users, and they can easily break tools that
2071 # expect to be able to iterate over newline delimited lists. This even
2072 # applies to our own code like .repo/project.list.
2073 if {"\r", "\n"} & path_codepoints:
2074 return "Newlines not allowed"
2075
2076 # Assume paths might be used on case-insensitive filesystems.
2077 path = path.lower()
2078
2079 # Split up the path by its components. We can't use os.path.sep
2080 # exclusively as some platforms (like Windows) will convert / to \ and
2081 # that bypasses all our constructed logic here. Especially since
2082 # manifest authors only use / in their paths.
2083 resep = re.compile(r"[/%s]" % re.escape(os.path.sep))
2084 # Strip off trailing slashes as those only produce '' elements, and we
2085 # use parts to look for individual bad components.
2086 parts = resep.split(path.rstrip("/"))
2087
2088 # Some people use src="." to create stable links to projects. Lets
2089 # allow that but reject all other uses of "." to keep things simple.
2090 if not cwd_dot_ok or parts != ["."]:
2091 for part in set(parts):
2092 if part in {".", "..", ".git"} or part.startswith(".repo"):
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002093 return f"bad component: {part}"
Gavin Makea2e3302023-03-11 06:46:20 +00002094
2095 if not dir_ok and resep.match(path[-1]):
2096 return "dirs not allowed"
2097
2098 # NB: The two abspath checks here are to handle platforms with multiple
2099 # filesystem path styles (e.g. Windows).
2100 norm = os.path.normpath(path)
2101 if (
2102 norm == ".."
2103 or (
2104 len(norm) >= 3
2105 and norm.startswith("..")
2106 and resep.match(norm[0])
2107 )
2108 or os.path.isabs(norm)
2109 or norm.startswith("/")
2110 ):
2111 return "path cannot be outside"
2112
2113 @classmethod
2114 def _ValidateFilePaths(cls, element, src, dest):
2115 """Verify |src| & |dest| are reasonable for <copyfile> & <linkfile>.
2116
2117 We verify the path independent of any filesystem state as we won't have
2118 a checkout available to compare to. i.e. This is for parsing validation
2119 purposes only.
2120
2121 We'll do full/live sanity checking before we do the actual filesystem
2122 modifications in _CopyFile/_LinkFile/etc...
2123 """
2124 # |dest| is the file we write to or symlink we create.
2125 # It is relative to the top of the repo client checkout.
2126 msg = cls._CheckLocalPath(dest)
2127 if msg:
2128 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002129 f'<{element}> invalid "dest": {dest}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00002130 )
2131
2132 # |src| is the file we read from or path we point to for symlinks.
2133 # It is relative to the top of the git project checkout.
2134 is_linkfile = element == "linkfile"
2135 msg = cls._CheckLocalPath(
2136 src, dir_ok=is_linkfile, cwd_dot_ok=is_linkfile
2137 )
2138 if msg:
2139 raise ManifestInvalidPathError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002140 f'<{element}> invalid "src": {src}: {msg}'
Gavin Makea2e3302023-03-11 06:46:20 +00002141 )
2142
2143 def _ParseCopyFile(self, project, node):
2144 src = self._reqatt(node, "src")
2145 dest = self._reqatt(node, "dest")
2146 if not self.IsMirror:
2147 # src is project relative;
2148 # dest is relative to the top of the tree.
2149 # We only validate paths if we actually plan to process them.
2150 self._ValidateFilePaths("copyfile", src, dest)
2151 project.AddCopyFile(src, dest, self.topdir)
2152
2153 def _ParseLinkFile(self, project, node):
2154 src = self._reqatt(node, "src")
2155 dest = self._reqatt(node, "dest")
2156 if not self.IsMirror:
2157 # src is project relative;
2158 # dest is relative to the top of the tree.
2159 # We only validate paths if we actually plan to process them.
2160 self._ValidateFilePaths("linkfile", src, dest)
2161 project.AddLinkFile(src, dest, self.topdir)
2162
2163 def _ParseAnnotation(self, element, node):
2164 name = self._reqatt(node, "name")
2165 value = self._reqatt(node, "value")
2166 try:
2167 keep = self._reqatt(node, "keep").lower()
2168 except ManifestParseError:
2169 keep = "true"
2170 if keep != "true" and keep != "false":
2171 raise ManifestParseError(
2172 'optional "keep" attribute must be ' '"true" or "false"'
2173 )
2174 element.AddAnnotation(name, value, keep)
2175
2176 def _get_remote(self, node):
2177 name = node.getAttribute("remote")
2178 if not name:
2179 return None
2180
2181 v = self._remotes.get(name)
2182 if not v:
2183 raise ManifestParseError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -04002184 f"remote {name} not defined in {self.manifestFile}"
Gavin Makea2e3302023-03-11 06:46:20 +00002185 )
2186 return v
2187
2188 def _reqatt(self, node, attname):
2189 """
2190 reads a required attribute from the node.
2191 """
2192 v = node.getAttribute(attname)
2193 if not v:
2194 raise ManifestParseError(
2195 "no %s in <%s> within %s"
2196 % (attname, node.nodeName, self.manifestFile)
2197 )
2198 return v
2199
2200 def projectsDiff(self, manifest):
2201 """return the projects differences between two manifests.
2202
2203 The diff will be from self to given manifest.
2204
2205 """
2206 fromProjects = self.paths
2207 toProjects = manifest.paths
2208
2209 fromKeys = sorted(fromProjects.keys())
Sylvain25d6c7c2023-08-19 23:21:49 +02002210 toKeys = set(toProjects.keys())
Gavin Makea2e3302023-03-11 06:46:20 +00002211
2212 diff = {
2213 "added": [],
2214 "removed": [],
2215 "missing": [],
2216 "changed": [],
2217 "unreachable": [],
2218 }
2219
2220 for proj in fromKeys:
Sylvain25d6c7c2023-08-19 23:21:49 +02002221 fromProj = fromProjects[proj]
Gavin Makea2e3302023-03-11 06:46:20 +00002222 if proj not in toKeys:
Sylvain25d6c7c2023-08-19 23:21:49 +02002223 diff["removed"].append(fromProj)
2224 elif not fromProj.Exists:
Gavin Makea2e3302023-03-11 06:46:20 +00002225 diff["missing"].append(toProjects[proj])
2226 toKeys.remove(proj)
2227 else:
Gavin Makea2e3302023-03-11 06:46:20 +00002228 toProj = toProjects[proj]
2229 try:
2230 fromRevId = fromProj.GetCommitRevisionId()
2231 toRevId = toProj.GetCommitRevisionId()
2232 except ManifestInvalidRevisionError:
2233 diff["unreachable"].append((fromProj, toProj))
2234 else:
2235 if fromRevId != toRevId:
2236 diff["changed"].append((fromProj, toProj))
2237 toKeys.remove(proj)
2238
Sylvain25d6c7c2023-08-19 23:21:49 +02002239 diff["added"].extend(toProjects[proj] for proj in sorted(toKeys))
Gavin Makea2e3302023-03-11 06:46:20 +00002240
2241 return diff
Simran Basib9a1b732015-08-20 12:19:28 -07002242
2243
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002244class RepoClient(XmlManifest):
Gavin Makea2e3302023-03-11 06:46:20 +00002245 """Manages a repo client checkout."""
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002246
Gavin Makea2e3302023-03-11 06:46:20 +00002247 def __init__(
2248 self, repodir, manifest_file=None, submanifest_path="", **kwargs
2249 ):
2250 """Initialize.
LaMont Jonesff6b1da2022-06-01 21:03:34 +00002251
Gavin Makea2e3302023-03-11 06:46:20 +00002252 Args:
2253 repodir: Path to the .repo/ dir for holding all internal checkout
2254 state. It must be in the top directory of the repo client
2255 checkout.
2256 manifest_file: Full path to the manifest file to parse. This will
2257 usually be |repodir|/|MANIFEST_FILE_NAME|.
2258 submanifest_path: The submanifest root relative to the repo root.
2259 **kwargs: Additional keyword arguments, passed to XmlManifest.
2260 """
2261 self.isGitcClient = False
2262 submanifest_path = submanifest_path or ""
2263 if submanifest_path:
2264 self._CheckLocalPath(submanifest_path)
2265 prefix = os.path.join(repodir, SUBMANIFEST_DIR, submanifest_path)
2266 else:
2267 prefix = repodir
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002268
Gavin Makea2e3302023-03-11 06:46:20 +00002269 if os.path.exists(os.path.join(prefix, LOCAL_MANIFEST_NAME)):
2270 print(
2271 "error: %s is not supported; put local manifests in `%s` "
2272 "instead"
2273 % (
2274 LOCAL_MANIFEST_NAME,
2275 os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME),
2276 ),
2277 file=sys.stderr,
2278 )
2279 sys.exit(1)
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002280
Gavin Makea2e3302023-03-11 06:46:20 +00002281 if manifest_file is None:
2282 manifest_file = os.path.join(prefix, MANIFEST_FILE_NAME)
2283 local_manifests = os.path.abspath(
2284 os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME)
2285 )
2286 super().__init__(
2287 repodir,
2288 manifest_file,
2289 local_manifests,
2290 submanifest_path=submanifest_path,
2291 **kwargs,
2292 )
Mike Frysinger8c1e9cb2020-09-06 14:53:18 -04002293
Gavin Makea2e3302023-03-11 06:46:20 +00002294 # TODO: Completely separate manifest logic out of the client.
2295 self.manifest = self