summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLaMont Jones <lamontjones@google.com>2021-11-18 22:40:18 +0000
committerLaMont Jones <lamontjones@google.com>2022-02-17 21:57:55 +0000
commitcc879a97c3e2614d19b15b4661c3cab4d33139c9 (patch)
tree69d225e9f0e9d79fec8f423d9c40c275f0bf3b8c
parent87cce68b28c34fa86895baa8d7f48307382e6c75 (diff)
downloadgit-repo-2.22.tar.gz
Add multi-manifest support with <submanifest> elementv2.22
To be addressed in another change: - a partial `repo sync` (with a list of projects/paths to sync) requires `--this-tree-only`. Change-Id: I6c7400bf001540e9d7694fa70934f8f204cb5f57 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/322657 Tested-by: LaMont Jones <lamontjones@google.com> Reviewed-by: Mike Frysinger <vapier@google.com>
-rw-r--r--command.py84
-rw-r--r--docs/internal-fs-layout.md4
-rw-r--r--docs/manifest-format.md66
-rw-r--r--git_superproject.py3
-rwxr-xr-xmain.py43
-rw-r--r--manifest_xml.py454
-rw-r--r--project.py41
-rw-r--r--subcmds/abandon.py7
-rw-r--r--subcmds/branches.py14
-rw-r--r--subcmds/checkout.py2
-rw-r--r--subcmds/diff.py2
-rw-r--r--subcmds/diffmanifests.py3
-rw-r--r--subcmds/download.py10
-rw-r--r--subcmds/forall.py12
-rw-r--r--subcmds/gitc_init.py1
-rw-r--r--subcmds/grep.py15
-rw-r--r--subcmds/info.py16
-rw-r--r--subcmds/init.py12
-rw-r--r--subcmds/list.py7
-rw-r--r--subcmds/manifest.py72
-rw-r--r--subcmds/overview.py4
-rw-r--r--subcmds/prune.py4
-rw-r--r--subcmds/rebase.py9
-rw-r--r--subcmds/stage.py11
-rw-r--r--subcmds/start.py5
-rw-r--r--subcmds/status.py9
-rw-r--r--subcmds/sync.py8
-rw-r--r--subcmds/upload.py44
28 files changed, 797 insertions, 165 deletions
diff --git a/command.py b/command.py
index b972a0be..12fe4172 100644
--- a/command.py
+++ b/command.py
@@ -61,13 +61,21 @@ class Command(object):
61 # it is the number of parallel jobs to default to. 61 # it is the number of parallel jobs to default to.
62 PARALLEL_JOBS = None 62 PARALLEL_JOBS = None
63 63
64 # Whether this command supports Multi-manifest. If False, then main.py will
65 # iterate over the manifests and invoke the command once per (sub)manifest.
66 # This is only checked after calling ValidateOptions, so that partially
67 # migrated subcommands can set it to False.
68 MULTI_MANIFEST_SUPPORT = True
69
64 def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None, 70 def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None,
65 git_event_log=None): 71 git_event_log=None, outer_client=None, outer_manifest=None):
66 self.repodir = repodir 72 self.repodir = repodir
67 self.client = client 73 self.client = client
74 self.outer_client = outer_client or client
68 self.manifest = manifest 75 self.manifest = manifest
69 self.gitc_manifest = gitc_manifest 76 self.gitc_manifest = gitc_manifest
70 self.git_event_log = git_event_log 77 self.git_event_log = git_event_log
78 self.outer_manifest = outer_manifest
71 79
72 # Cache for the OptionParser property. 80 # Cache for the OptionParser property.
73 self._optparse = None 81 self._optparse = None
@@ -135,6 +143,18 @@ class Command(object):
135 type=int, default=self.PARALLEL_JOBS, 143 type=int, default=self.PARALLEL_JOBS,
136 help=f'number of jobs to run in parallel (default: {default})') 144 help=f'number of jobs to run in parallel (default: {default})')
137 145
146 m = p.add_option_group('Multi-manifest options')
147 m.add_option('--outer-manifest', action='store_true',
148 help='operate starting at the outermost manifest')
149 m.add_option('--no-outer-manifest', dest='outer_manifest',
150 action='store_false', default=None,
151 help='do not operate on outer manifests')
152 m.add_option('--this-manifest-only', action='store_true', default=None,
153 help='only operate on this (sub)manifest')
154 m.add_option('--no-this-manifest-only', '--all-manifests',
155 dest='this_manifest_only', action='store_false',
156 help='operate on this manifest and its submanifests')
157
138 def _Options(self, p): 158 def _Options(self, p):
139 """Initialize the option parser with subcommand-specific options.""" 159 """Initialize the option parser with subcommand-specific options."""
140 160
@@ -252,16 +272,19 @@ class Command(object):
252 return project 272 return project
253 273
254 def GetProjects(self, args, manifest=None, groups='', missing_ok=False, 274 def GetProjects(self, args, manifest=None, groups='', missing_ok=False,
255 submodules_ok=False): 275 submodules_ok=False, all_manifests=False):
256 """A list of projects that match the arguments. 276 """A list of projects that match the arguments.
257 """ 277 """
258 if not manifest: 278 if all_manifests:
259 manifest = self.manifest 279 if not manifest:
260 all_projects_list = manifest.projects 280 manifest = self.manifest.outer_client
281 all_projects_list = manifest.all_projects
282 else:
283 if not manifest:
284 manifest = self.manifest
285 all_projects_list = manifest.projects
261 result = [] 286 result = []
262 287
263 mp = manifest.manifestProject
264
265 if not groups: 288 if not groups:
266 groups = manifest.GetGroupsStr() 289 groups = manifest.GetGroupsStr()
267 groups = [x for x in re.split(r'[,\s]+', groups) if x] 290 groups = [x for x in re.split(r'[,\s]+', groups) if x]
@@ -282,12 +305,19 @@ class Command(object):
282 for arg in args: 305 for arg in args:
283 # We have to filter by manifest groups in case the requested project is 306 # We have to filter by manifest groups in case the requested project is
284 # checked out multiple times or differently based on them. 307 # checked out multiple times or differently based on them.
285 projects = [project for project in manifest.GetProjectsWithName(arg) 308 projects = [project for project in manifest.GetProjectsWithName(
309 arg, all_manifests=all_manifests)
286 if project.MatchesGroups(groups)] 310 if project.MatchesGroups(groups)]
287 311
288 if not projects: 312 if not projects:
289 path = os.path.abspath(arg).replace('\\', '/') 313 path = os.path.abspath(arg).replace('\\', '/')
290 project = self._GetProjectByPath(manifest, path) 314 tree = manifest
315 if all_manifests:
316 # Look for the deepest matching submanifest.
317 for tree in reversed(list(manifest.all_manifests)):
318 if path.startswith(tree.topdir):
319 break
320 project = self._GetProjectByPath(tree, path)
291 321
292 # If it's not a derived project, update path->project mapping and 322 # If it's not a derived project, update path->project mapping and
293 # search again, as arg might actually point to a derived subproject. 323 # search again, as arg might actually point to a derived subproject.
@@ -308,7 +338,8 @@ class Command(object):
308 338
309 for project in projects: 339 for project in projects:
310 if not missing_ok and not project.Exists: 340 if not missing_ok and not project.Exists:
311 raise NoSuchProjectError('%s (%s)' % (arg, project.relpath)) 341 raise NoSuchProjectError('%s (%s)' % (
342 arg, project.RelPath(local=not all_manifests)))
312 if not project.MatchesGroups(groups): 343 if not project.MatchesGroups(groups):
313 raise InvalidProjectGroupsError(arg) 344 raise InvalidProjectGroupsError(arg)
314 345
@@ -319,12 +350,22 @@ class Command(object):
319 result.sort(key=_getpath) 350 result.sort(key=_getpath)
320 return result 351 return result
321 352
322 def FindProjects(self, args, inverse=False): 353 def FindProjects(self, args, inverse=False, all_manifests=False):
354 """Find projects from command line arguments.
355
356 Args:
357 args: a list of (case-insensitive) strings, projects to search for.
358 inverse: a boolean, if True, then projects not matching any |args| are
359 returned.
360 all_manifests: a boolean, if True then all manifests and submanifests are
361 used. If False, then only the local (sub)manifest is used.
362 """
323 result = [] 363 result = []
324 patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args] 364 patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args]
325 for project in self.GetProjects(''): 365 for project in self.GetProjects('', all_manifests=all_manifests):
366 paths = [project.name, project.RelPath(local=not all_manifests)]
326 for pattern in patterns: 367 for pattern in patterns:
327 match = pattern.search(project.name) or pattern.search(project.relpath) 368 match = any(pattern.search(x) for x in paths)
328 if not inverse and match: 369 if not inverse and match:
329 result.append(project) 370 result.append(project)
330 break 371 break
@@ -333,9 +374,24 @@ class Command(object):
333 else: 374 else:
334 if inverse: 375 if inverse:
335 result.append(project) 376 result.append(project)
336 result.sort(key=lambda project: project.relpath) 377 result.sort(key=lambda project: (project.manifest.path_prefix,
378 project.relpath))
337 return result 379 return result
338 380
381 def ManifestList(self, opt):
382 """Yields all of the manifests to traverse.
383
384 Args:
385 opt: The command options.
386 """
387 top = self.outer_manifest
388 if opt.outer_manifest is False or opt.this_manifest_only:
389 top = self.manifest
390 yield top
391 if not opt.this_manifest_only:
392 for child in top.all_children:
393 yield child
394
339 395
340class InteractiveCommand(Command): 396class InteractiveCommand(Command):
341 """Command which requires user interaction on the tty and 397 """Command which requires user interaction on the tty and
diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md
index 0e830510..a9bd1d26 100644
--- a/docs/internal-fs-layout.md
+++ b/docs/internal-fs-layout.md
@@ -50,6 +50,10 @@ For example, if you want to change the manifest branch, you can simply run
50For more documentation on the manifest format, including the local_manifests 50For more documentation on the manifest format, including the local_manifests
51support, see the [manifest-format.md] file. 51support, see the [manifest-format.md] file.
52 52
53* `submanifests/{submanifest.path}/`: The path prefix to the manifest state of
54 a submanifest included in a multi-manifest checkout. The outermost manifest
55 manifest state is found adjacent to `submanifests/`.
56
53* `manifests/`: A git checkout of the manifest project. Its `.git/` state 57* `manifests/`: A git checkout of the manifest project. Its `.git/` state
54 points to the `manifest.git` bare checkout (see below). It tracks the git 58 points to the `manifest.git` bare checkout (see below). It tracks the git
55 branch specified at `repo init` time via `--manifest-branch`. 59 branch specified at `repo init` time via `--manifest-branch`.
diff --git a/docs/manifest-format.md b/docs/manifest-format.md
index 8e0049b3..7c0a7da9 100644
--- a/docs/manifest-format.md
+++ b/docs/manifest-format.md
@@ -26,6 +26,7 @@ following DTD:
26 remote*, 26 remote*,
27 default?, 27 default?,
28 manifest-server?, 28 manifest-server?,
29 submanifest*?,
29 remove-project*, 30 remove-project*,
30 project*, 31 project*,
31 extend-project*, 32 extend-project*,
@@ -57,6 +58,15 @@ following DTD:
57 <!ELEMENT manifest-server EMPTY> 58 <!ELEMENT manifest-server EMPTY>
58 <!ATTLIST manifest-server url CDATA #REQUIRED> 59 <!ATTLIST manifest-server url CDATA #REQUIRED>
59 60
61 <!ELEMENT submanifest EMPTY>
62 <!ATTLIST submanifest name ID #REQUIRED>
63 <!ATTLIST submanifest remote IDREF #IMPLIED>
64 <!ATTLIST submanifest project CDATA #IMPLIED>
65 <!ATTLIST submanifest manifest-name CDATA #IMPLIED>
66 <!ATTLIST submanifest revision CDATA #IMPLIED>
67 <!ATTLIST submanifest path CDATA #IMPLIED>
68 <!ATTLIST submanifest groups CDATA #IMPLIED>
69
60 <!ELEMENT project (annotation*, 70 <!ELEMENT project (annotation*,
61 project*, 71 project*,
62 copyfile*, 72 copyfile*,
@@ -236,6 +246,60 @@ the specified tag. This is used by repo sync when the --smart-tag option
236is given. 246is given.
237 247
238 248
249### Element submanifest
250
251One or more submanifest elements may be specified. Each element describes a
252single manifest to be checked out as a child.
253
254Attribute `name`: A unique name (within the current (sub)manifest) for this
255submanifest. It acts as a default for `revision` below. The same name can be
256used for submanifests with different parent (sub)manifests.
257
258Attribute `remote`: Name of a previously defined remote element.
259If not supplied the remote given by the default element is used.
260
261Attribute `project`: The manifest project name. The project's name is appended
262onto its remote's fetch URL to generate the actual URL to configure the Git
263remote with. The URL gets formed as:
264
265 ${remote_fetch}/${project_name}.git
266
267where ${remote_fetch} is the remote's fetch attribute and
268${project_name} is the project's name attribute. The suffix ".git"
269is always appended as repo assumes the upstream is a forest of
270bare Git repositories. If the project has a parent element, its
271name will be prefixed by the parent's.
272
273The project name must match the name Gerrit knows, if Gerrit is
274being used for code reviews.
275
276`project` must not be empty, and may not be an absolute path or use "." or ".."
277path components. It is always interpreted relative to the remote's fetch
278settings, so if a different base path is needed, declare a different remote
279with the new settings needed.
280
281If not supplied the remote and project for this manifest will be used: `remote`
282cannot be supplied.
283
284Attribute `manifest-name`: The manifest filename in the manifest project. If
285not supplied, `default.xml` is used.
286
287Attribute `revision`: Name of a Git branch (e.g. "main" or "refs/heads/main"),
288tag (e.g. "refs/tags/stable"), or a commit hash. If not supplied, `name` is
289used.
290
291Attribute `path`: An optional path relative to the top directory
292of the repo client where the submanifest repo client top directory
293should be placed. If not supplied, `revision` is used.
294
295`path` may not be an absolute path or use "." or ".." path components.
296
297Attribute `groups`: List of additional groups to which all projects
298in the included submanifest belong. This appends and recurses, meaning
299all projects in submanifests carry all parent submanifest groups.
300Same syntax as the corresponding element of `project`.
301
302
239### Element project 303### Element project
240 304
241One or more project elements may be specified. Each element 305One or more project elements may be specified. Each element
@@ -471,7 +535,7 @@ These restrictions are not enforced for [Local Manifests].
471 535
472Attribute `groups`: List of additional groups to which all projects 536Attribute `groups`: List of additional groups to which all projects
473in the included manifest belong. This appends and recurses, meaning 537in the included manifest belong. This appends and recurses, meaning
474all projects in sub-manifests carry all parent include groups. 538all projects in included manifests carry all parent include groups.
475Same syntax as the corresponding element of `project`. 539Same syntax as the corresponding element of `project`.
476 540
477## Local Manifests {#local-manifests} 541## Local Manifests {#local-manifests}
diff --git a/git_superproject.py b/git_superproject.py
index 237e57e1..299d2537 100644
--- a/git_superproject.py
+++ b/git_superproject.py
@@ -92,7 +92,8 @@ class Superproject(object):
92 self._branch = manifest.branch 92 self._branch = manifest.branch
93 self._repodir = os.path.abspath(repodir) 93 self._repodir = os.path.abspath(repodir)
94 self._superproject_dir = superproject_dir 94 self._superproject_dir = superproject_dir
95 self._superproject_path = os.path.join(self._repodir, superproject_dir) 95 self._superproject_path = manifest.SubmanifestInfoDir(manifest.path_prefix,
96 superproject_dir)
96 self._manifest_path = os.path.join(self._superproject_path, 97 self._manifest_path = os.path.join(self._superproject_path,
97 _SUPERPROJECT_MANIFEST_NAME) 98 _SUPERPROJECT_MANIFEST_NAME)
98 git_name = '' 99 git_name = ''
diff --git a/main.py b/main.py
index 2050cabb..6fb688c2 100755
--- a/main.py
+++ b/main.py
@@ -127,6 +127,8 @@ global_options.add_option('--event-log',
127 help='filename of event log to append timeline to') 127 help='filename of event log to append timeline to')
128global_options.add_option('--git-trace2-event-log', action='store', 128global_options.add_option('--git-trace2-event-log', action='store',
129 help='directory to write git trace2 event log to') 129 help='directory to write git trace2 event log to')
130global_options.add_option('--submanifest-path', action='store',
131 metavar='REL_PATH', help='submanifest path')
130 132
131 133
132class _Repo(object): 134class _Repo(object):
@@ -217,7 +219,12 @@ class _Repo(object):
217 SetDefaultColoring(gopts.color) 219 SetDefaultColoring(gopts.color)
218 220
219 git_trace2_event_log = EventLog() 221 git_trace2_event_log = EventLog()
220 repo_client = RepoClient(self.repodir) 222 outer_client = RepoClient(self.repodir)
223 repo_client = outer_client
224 if gopts.submanifest_path:
225 repo_client = RepoClient(self.repodir,
226 submanifest_path=gopts.submanifest_path,
227 outer_client=outer_client)
221 gitc_manifest = None 228 gitc_manifest = None
222 gitc_client_name = gitc_utils.parse_clientdir(os.getcwd()) 229 gitc_client_name = gitc_utils.parse_clientdir(os.getcwd())
223 if gitc_client_name: 230 if gitc_client_name:
@@ -229,6 +236,8 @@ class _Repo(object):
229 repodir=self.repodir, 236 repodir=self.repodir,
230 client=repo_client, 237 client=repo_client,
231 manifest=repo_client.manifest, 238 manifest=repo_client.manifest,
239 outer_client=outer_client,
240 outer_manifest=outer_client.manifest,
232 gitc_manifest=gitc_manifest, 241 gitc_manifest=gitc_manifest,
233 git_event_log=git_trace2_event_log) 242 git_event_log=git_trace2_event_log)
234 except KeyError: 243 except KeyError:
@@ -283,7 +292,37 @@ class _Repo(object):
283 try: 292 try:
284 cmd.CommonValidateOptions(copts, cargs) 293 cmd.CommonValidateOptions(copts, cargs)
285 cmd.ValidateOptions(copts, cargs) 294 cmd.ValidateOptions(copts, cargs)
286 result = cmd.Execute(copts, cargs) 295
296 this_manifest_only = copts.this_manifest_only
297 # If not specified, default to using the outer manifest.
298 outer_manifest = copts.outer_manifest is not False
299 if cmd.MULTI_MANIFEST_SUPPORT or this_manifest_only:
300 result = cmd.Execute(copts, cargs)
301 elif outer_manifest and repo_client.manifest.is_submanifest:
302 # The command does not support multi-manifest, we are using a
303 # submanifest, and the command line is for the outermost manifest.
304 # Re-run using the outermost manifest, which will recurse through the
305 # submanifests.
306 gopts.submanifest_path = ''
307 result = self._Run(name, gopts, argv)
308 else:
309 # No multi-manifest support. Run the command in the current
310 # (sub)manifest, and then any child submanifests.
311 result = cmd.Execute(copts, cargs)
312 for submanifest in repo_client.manifest.submanifests.values():
313 spec = submanifest.ToSubmanifestSpec(root=repo_client.outer_client)
314 gopts.submanifest_path = submanifest.repo_client.path_prefix
315 child_argv = argv[:]
316 child_argv.append('--no-outer-manifest')
317 # Not all subcommands support the 3 manifest options, so only add them
318 # if the original command includes them.
319 if hasattr(copts, 'manifest_url'):
320 child_argv.extend(['--manifest-url', spec.manifestUrl])
321 if hasattr(copts, 'manifest_name'):
322 child_argv.extend(['--manifest-name', spec.manifestName])
323 if hasattr(copts, 'manifest_branch'):
324 child_argv.extend(['--manifest-branch', spec.revision])
325 result = self._Run(name, gopts, child_argv) or result
287 except (DownloadError, ManifestInvalidRevisionError, 326 except (DownloadError, ManifestInvalidRevisionError,
288 NoManifestException) as e: 327 NoManifestException) as e:
289 print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)), 328 print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)),
diff --git a/manifest_xml.py b/manifest_xml.py
index 7c5906da..7a4eb1e8 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -33,6 +33,9 @@ from wrapper import Wrapper
33MANIFEST_FILE_NAME = 'manifest.xml' 33MANIFEST_FILE_NAME = 'manifest.xml'
34LOCAL_MANIFEST_NAME = 'local_manifest.xml' 34LOCAL_MANIFEST_NAME = 'local_manifest.xml'
35LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' 35LOCAL_MANIFESTS_DIR_NAME = 'local_manifests'
36SUBMANIFEST_DIR = 'submanifests'
37# Limit submanifests to an arbitrary depth for loop detection.
38MAX_SUBMANIFEST_DEPTH = 8
36 39
37# Add all projects from local manifest into a group. 40# Add all projects from local manifest into a group.
38LOCAL_MANIFEST_GROUP_PREFIX = 'local:' 41LOCAL_MANIFEST_GROUP_PREFIX = 'local:'
@@ -197,10 +200,122 @@ class _XmlRemote(object):
197 self.annotations.append(Annotation(name, value, keep)) 200 self.annotations.append(Annotation(name, value, keep))
198 201
199 202
203class _XmlSubmanifest:
204 """Manage the <submanifest> element specified in the manifest.
205
206 Attributes:
207 name: a string, the name for this submanifest.
208 remote: a string, the remote.name for this submanifest.
209 project: a string, the name of the manifest project.
210 revision: a string, the commitish.
211 manifestName: a string, the submanifest file name.
212 groups: a list of strings, the groups to add to all projects in the submanifest.
213 path: a string, the relative path for the submanifest checkout.
214 annotations: (derived) a list of annotations.
215 present: (derived) a boolean, whether the submanifest's manifest file is present.
216 """
217 def __init__(self,
218 name,
219 remote=None,
220 project=None,
221 revision=None,
222 manifestName=None,
223 groups=None,
224 path=None,
225 parent=None):
226 self.name = name
227 self.remote = remote
228 self.project = project
229 self.revision = revision
230 self.manifestName = manifestName
231 self.groups = groups
232 self.path = path
233 self.annotations = []
234 outer_client = parent._outer_client or parent
235 if self.remote and not self.project:
236 raise ManifestParseError(
237 f'Submanifest {name}: must specify project when remote is given.')
238 rc = self.repo_client = RepoClient(
239 parent.repodir, manifestName, parent_groups=','.join(groups) or '',
240 submanifest_path=self.relpath, outer_client=outer_client)
241
242 self.present = os.path.exists(os.path.join(self.repo_client.subdir,
243 MANIFEST_FILE_NAME))
244
245 def __eq__(self, other):
246 if not isinstance(other, _XmlSubmanifest):
247 return False
248 return (
249 self.name == other.name and
250 self.remote == other.remote and
251 self.project == other.project and
252 self.revision == other.revision and
253 self.manifestName == other.manifestName and
254 self.groups == other.groups and
255 self.path == other.path and
256 sorted(self.annotations) == sorted(other.annotations))
257
258 def __ne__(self, other):
259 return not self.__eq__(other)
260
261 def ToSubmanifestSpec(self, root):
262 """Return a SubmanifestSpec object, populating attributes"""
263 mp = root.manifestProject
264 remote = root.remotes[self.remote or root.default.remote.name]
265 # If a project was given, generate the url from the remote and project.
266 # If not, use this manifestProject's url.
267 if self.project:
268 manifestUrl = remote.ToRemoteSpec(self.project).url
269 else:
270 manifestUrl = mp.GetRemote(mp.remote.name).url
271 manifestName = self.manifestName or 'default.xml'
272 revision = self.revision or self.name
273 path = self.path or revision.split('/')[-1]
274 groups = self.groups or []
275
276 return SubmanifestSpec(self.name, manifestUrl, manifestName, revision, path,
277 groups)
278
279 @property
280 def relpath(self):
281 """The path of this submanifest relative to the parent manifest."""
282 revision = self.revision or self.name
283 return self.path or revision.split('/')[-1]
284
285 def GetGroupsStr(self):
286 """Returns the `groups` given for this submanifest."""
287 if self.groups:
288 return ','.join(self.groups)
289 return ''
290
291 def AddAnnotation(self, name, value, keep):
292 """Add annotations to the submanifest."""
293 self.annotations.append(Annotation(name, value, keep))
294
295
296class SubmanifestSpec:
297 """The submanifest element, with all fields expanded."""
298
299 def __init__(self,
300 name,
301 manifestUrl,
302 manifestName,
303 revision,
304 path,
305 groups):
306 self.name = name
307 self.manifestUrl = manifestUrl
308 self.manifestName = manifestName
309 self.revision = revision
310 self.path = path
311 self.groups = groups or []
312
313
200class XmlManifest(object): 314class XmlManifest(object):
201 """manages the repo configuration file""" 315 """manages the repo configuration file"""
202 316
203 def __init__(self, repodir, manifest_file, local_manifests=None): 317 def __init__(self, repodir, manifest_file, local_manifests=None,
318 outer_client=None, parent_groups='', submanifest_path=''):
204 """Initialize. 319 """Initialize.
205 320
206 Args: 321 Args:
@@ -210,23 +325,37 @@ class XmlManifest(object):
210 be |repodir|/|MANIFEST_FILE_NAME|. 325 be |repodir|/|MANIFEST_FILE_NAME|.
211 local_manifests: Full path to the directory of local override manifests. 326 local_manifests: Full path to the directory of local override manifests.
212 This will usually be |repodir|/|LOCAL_MANIFESTS_DIR_NAME|. 327 This will usually be |repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
328 outer_client: RepoClient of the outertree.
329 parent_groups: a string, the groups to apply to this projects.
330 submanifest_path: The submanifest root relative to the repo root.
213 """ 331 """
214 # TODO(vapier): Move this out of this class. 332 # TODO(vapier): Move this out of this class.
215 self.globalConfig = GitConfig.ForUser() 333 self.globalConfig = GitConfig.ForUser()
216 334
217 self.repodir = os.path.abspath(repodir) 335 self.repodir = os.path.abspath(repodir)
218 self.topdir = os.path.dirname(self.repodir) 336 self._CheckLocalPath(submanifest_path)
337 self.topdir = os.path.join(os.path.dirname(self.repodir), submanifest_path)
219 self.manifestFile = manifest_file 338 self.manifestFile = manifest_file
220 self.local_manifests = local_manifests 339 self.local_manifests = local_manifests
221 self._load_local_manifests = True 340 self._load_local_manifests = True
341 self.parent_groups = parent_groups
342
343 if outer_client and self.isGitcClient:
344 raise ManifestParseError('Multi-manifest is incompatible with `gitc-init`')
345
346 if submanifest_path and not outer_client:
347 # If passing a submanifest_path, there must be an outer_client.
348 raise ManifestParseError(f'Bad call to {self.__class__.__name__}')
349
350 # If self._outer_client is None, this is not a checkout that supports
351 # multi-tree.
352 self._outer_client = outer_client or self
222 353
223 self.repoProject = MetaProject(self, 'repo', 354 self.repoProject = MetaProject(self, 'repo',
224 gitdir=os.path.join(repodir, 'repo/.git'), 355 gitdir=os.path.join(repodir, 'repo/.git'),
225 worktree=os.path.join(repodir, 'repo')) 356 worktree=os.path.join(repodir, 'repo'))
226 357
227 mp = MetaProject(self, 'manifests', 358 mp = self.SubmanifestProject(self.path_prefix)
228 gitdir=os.path.join(repodir, 'manifests.git'),
229 worktree=os.path.join(repodir, 'manifests'))
230 self.manifestProject = mp 359 self.manifestProject = mp
231 360
232 # This is a bit hacky, but we're in a chicken & egg situation: all the 361 # This is a bit hacky, but we're in a chicken & egg situation: all the
@@ -311,6 +440,31 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
311 ae.setAttribute('value', a.value) 440 ae.setAttribute('value', a.value)
312 e.appendChild(ae) 441 e.appendChild(ae)
313 442
443 def _SubmanifestToXml(self, r, doc, root):
444 """Generate XML <submanifest/> node."""
445 e = doc.createElement('submanifest')
446 root.appendChild(e)
447 e.setAttribute('name', r.name)
448 if r.remote is not None:
449 e.setAttribute('remote', r.remote)
450 if r.project is not None:
451 e.setAttribute('project', r.project)
452 if r.manifestName is not None:
453 e.setAttribute('manifest-name', r.manifestName)
454 if r.revision is not None:
455 e.setAttribute('revision', r.revision)
456 if r.path is not None:
457 e.setAttribute('path', r.path)
458 if r.groups:
459 e.setAttribute('groups', r.GetGroupsStr())
460
461 for a in r.annotations:
462 if a.keep == 'true':
463 ae = doc.createElement('annotation')
464 ae.setAttribute('name', a.name)
465 ae.setAttribute('value', a.value)
466 e.appendChild(ae)
467
314 def _ParseList(self, field): 468 def _ParseList(self, field):
315 """Parse fields that contain flattened lists. 469 """Parse fields that contain flattened lists.
316 470
@@ -329,6 +483,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
329 483
330 doc = xml.dom.minidom.Document() 484 doc = xml.dom.minidom.Document()
331 root = doc.createElement('manifest') 485 root = doc.createElement('manifest')
486 if self.is_submanifest:
487 root.setAttribute('path', self.path_prefix)
332 doc.appendChild(root) 488 doc.appendChild(root)
333 489
334 # Save out the notice. There's a little bit of work here to give it the 490 # Save out the notice. There's a little bit of work here to give it the
@@ -383,6 +539,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
383 root.appendChild(e) 539 root.appendChild(e)
384 root.appendChild(doc.createTextNode('')) 540 root.appendChild(doc.createTextNode(''))
385 541
542 for r in sorted(self.submanifests):
543 self._SubmanifestToXml(self.submanifests[r], doc, root)
544 if self.submanifests:
545 root.appendChild(doc.createTextNode(''))
546
386 def output_projects(parent, parent_node, projects): 547 def output_projects(parent, parent_node, projects):
387 for project_name in projects: 548 for project_name in projects:
388 for project in self._projects[project_name]: 549 for project in self._projects[project_name]:
@@ -537,6 +698,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
537 'project', 698 'project',
538 'extend-project', 699 'extend-project',
539 'include', 700 'include',
701 'submanifest',
540 # These are children of 'project' nodes. 702 # These are children of 'project' nodes.
541 'annotation', 703 'annotation',
542 'project', 704 'project',
@@ -575,12 +737,74 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
575 """Manifests can modify e if they support extra project attributes.""" 737 """Manifests can modify e if they support extra project attributes."""
576 738
577 @property 739 @property
740 def is_multimanifest(self):
741 """Whether this is a multimanifest checkout"""
742 return bool(self.outer_client.submanifests)
743
744 @property
745 def is_submanifest(self):
746 """Whether this manifest is a submanifest"""
747 return self._outer_client and self._outer_client != self
748
749 @property
750 def outer_client(self):
751 """The instance of the outermost manifest client"""
752 self._Load()
753 return self._outer_client
754
755 @property
756 def all_manifests(self):
757 """Generator yielding all (sub)manifests."""
758 self._Load()
759 outer = self._outer_client
760 yield outer
761 for tree in outer.all_children:
762 yield tree
763
764 @property
765 def all_children(self):
766 """Generator yielding all child submanifests."""
767 self._Load()
768 for child in self._submanifests.values():
769 if child.repo_client:
770 yield child.repo_client
771 for tree in child.repo_client.all_children:
772 yield tree
773
774 @property
775 def path_prefix(self):
776 """The path of this submanifest, relative to the outermost manifest."""
777 if not self._outer_client or self == self._outer_client:
778 return ''
779 return os.path.relpath(self.topdir, self._outer_client.topdir)
780
781 @property
782 def all_paths(self):
783 """All project paths for all (sub)manifests. See `paths`."""
784 ret = {}
785 for tree in self.all_manifests:
786 prefix = tree.path_prefix
787 ret.update({os.path.join(prefix, k): v for k, v in tree.paths.items()})
788 return ret
789
790 @property
791 def all_projects(self):
792 """All projects for all (sub)manifests. See `projects`."""
793 return list(itertools.chain.from_iterable(x._paths.values() for x in self.all_manifests))
794
795 @property
578 def paths(self): 796 def paths(self):
797 """Return all paths for this manifest.
798
799 Return:
800 A dictionary of {path: Project()}. `path` is relative to this manifest.
801 """
579 self._Load() 802 self._Load()
580 return self._paths 803 return self._paths
581 804
582 @property 805 @property
583 def projects(self): 806 def projects(self):
807 """Return a list of all Projects in this manifest."""
584 self._Load() 808 self._Load()
585 return list(self._paths.values()) 809 return list(self._paths.values())
586 810
@@ -595,6 +819,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
595 return self._default 819 return self._default
596 820
597 @property 821 @property
822 def submanifests(self):
823 """All submanifests in this manifest."""
824 self._Load()
825 return self._submanifests
826
827 @property
598 def repo_hooks_project(self): 828 def repo_hooks_project(self):
599 self._Load() 829 self._Load()
600 return self._repo_hooks_project 830 return self._repo_hooks_project
@@ -651,8 +881,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
651 return self._load_local_manifests and self.local_manifests 881 return self._load_local_manifests and self.local_manifests
652 882
653 def IsFromLocalManifest(self, project): 883 def IsFromLocalManifest(self, project):
654 """Is the project from a local manifest? 884 """Is the project from a local manifest?"""
655 """
656 return any(x.startswith(LOCAL_MANIFEST_GROUP_PREFIX) 885 return any(x.startswith(LOCAL_MANIFEST_GROUP_PREFIX)
657 for x in project.groups) 886 for x in project.groups)
658 887
@@ -676,6 +905,50 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
676 def EnableGitLfs(self): 905 def EnableGitLfs(self):
677 return self.manifestProject.config.GetBoolean('repo.git-lfs') 906 return self.manifestProject.config.GetBoolean('repo.git-lfs')
678 907
908 def FindManifestByPath(self, path):
909 """Returns the manifest containing path."""
910 path = os.path.abspath(path)
911 manifest = self._outer_client or self
912 old = None
913 while manifest._submanifests and manifest != old:
914 old = manifest
915 for name in manifest._submanifests:
916 tree = manifest._submanifests[name]
917 if path.startswith(tree.repo_client.manifest.topdir):
918 manifest = tree.repo_client
919 break
920 return manifest
921
922 @property
923 def subdir(self):
924 """Returns the path for per-submanifest objects for this manifest."""
925 return self.SubmanifestInfoDir(self.path_prefix)
926
927 def SubmanifestInfoDir(self, submanifest_path, object_path=''):
928 """Return the path to submanifest-specific info for a submanifest.
929
930 Return the full path of the directory in which to put per-manifest objects.
931
932 Args:
933 submanifest_path: a string, the path of the submanifest, relative to the
934 outermost topdir. If empty, then repodir is returned.
935 object_path: a string, relative path to append to the submanifest info
936 directory path.
937 """
938 if submanifest_path:
939 return os.path.join(self.repodir, SUBMANIFEST_DIR, submanifest_path,
940 object_path)
941 else:
942 return os.path.join(self.repodir, object_path)
943
944 def SubmanifestProject(self, submanifest_path):
945 """Return a manifestProject for a submanifest."""
946 subdir = self.SubmanifestInfoDir(submanifest_path)
947 mp = MetaProject(self, 'manifests',
948 gitdir=os.path.join(subdir, 'manifests.git'),
949 worktree=os.path.join(subdir, 'manifests'))
950 return mp
951
679 def GetDefaultGroupsStr(self): 952 def GetDefaultGroupsStr(self):
680 """Returns the default group string for the platform.""" 953 """Returns the default group string for the platform."""
681 return 'default,platform-' + platform.system().lower() 954 return 'default,platform-' + platform.system().lower()
@@ -693,6 +966,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
693 self._paths = {} 966 self._paths = {}
694 self._remotes = {} 967 self._remotes = {}
695 self._default = None 968 self._default = None
969 self._submanifests = {}
696 self._repo_hooks_project = None 970 self._repo_hooks_project = None
697 self._superproject = {} 971 self._superproject = {}
698 self._contactinfo = ContactInfo(Wrapper().BUG_URL) 972 self._contactinfo = ContactInfo(Wrapper().BUG_URL)
@@ -700,20 +974,29 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
700 self.branch = None 974 self.branch = None
701 self._manifest_server = None 975 self._manifest_server = None
702 976
703 def _Load(self): 977 def _Load(self, initial_client=None, submanifest_depth=0):
978 if submanifest_depth > MAX_SUBMANIFEST_DEPTH:
979 raise ManifestParseError('maximum submanifest depth %d exceeded.' %
980 MAX_SUBMANIFEST_DEPTH)
704 if not self._loaded: 981 if not self._loaded:
982 if self._outer_client and self._outer_client != self:
983 # This will load all clients.
984 self._outer_client._Load(initial_client=self)
985
705 m = self.manifestProject 986 m = self.manifestProject
706 b = m.GetBranch(m.CurrentBranch).merge 987 b = m.GetBranch(m.CurrentBranch).merge
707 if b is not None and b.startswith(R_HEADS): 988 if b is not None and b.startswith(R_HEADS):
708 b = b[len(R_HEADS):] 989 b = b[len(R_HEADS):]
709 self.branch = b 990 self.branch = b
710 991
992 parent_groups = self.parent_groups
993
711 # The manifestFile was specified by the user which is why we allow include 994 # The manifestFile was specified by the user which is why we allow include
712 # paths to point anywhere. 995 # paths to point anywhere.
713 nodes = [] 996 nodes = []
714 nodes.append(self._ParseManifestXml( 997 nodes.append(self._ParseManifestXml(
715 self.manifestFile, self.manifestProject.worktree, 998 self.manifestFile, self.manifestProject.worktree,
716 restrict_includes=False)) 999 parent_groups=parent_groups, restrict_includes=False))
717 1000
718 if self._load_local_manifests and self.local_manifests: 1001 if self._load_local_manifests and self.local_manifests:
719 try: 1002 try:
@@ -722,9 +1005,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
722 local = os.path.join(self.local_manifests, local_file) 1005 local = os.path.join(self.local_manifests, local_file)
723 # Since local manifests are entirely managed by the user, allow 1006 # Since local manifests are entirely managed by the user, allow
724 # them to point anywhere the user wants. 1007 # them to point anywhere the user wants.
1008 local_group = f'{LOCAL_MANIFEST_GROUP_PREFIX}:{local_file[:-4]}'
725 nodes.append(self._ParseManifestXml( 1009 nodes.append(self._ParseManifestXml(
726 local, self.repodir, 1010 local, self.subdir,
727 parent_groups=f'{LOCAL_MANIFEST_GROUP_PREFIX}:{local_file[:-4]}', 1011 parent_groups=f'{local_group},{parent_groups}',
728 restrict_includes=False)) 1012 restrict_includes=False))
729 except OSError: 1013 except OSError:
730 pass 1014 pass
@@ -743,6 +1027,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
743 1027
744 self._loaded = True 1028 self._loaded = True
745 1029
1030 # Now that we have loaded this manifest, load any submanifest manifests
1031 # as well. We need to do this after self._loaded is set to avoid looping.
1032 if self._outer_client:
1033 for name in self._submanifests:
1034 tree = self._submanifests[name]
1035 spec = tree.ToSubmanifestSpec(self)
1036 present = os.path.exists(os.path.join(self.subdir, MANIFEST_FILE_NAME))
1037 if present and tree.present and not tree.repo_client:
1038 if initial_client and initial_client.topdir == self.topdir:
1039 tree.repo_client = self
1040 tree.present = present
1041 elif not os.path.exists(self.subdir):
1042 tree.present = False
1043 if tree.present:
1044 tree.repo_client._Load(initial_client=initial_client,
1045 submanifest_depth=submanifest_depth + 1)
1046
746 def _ParseManifestXml(self, path, include_root, parent_groups='', 1047 def _ParseManifestXml(self, path, include_root, parent_groups='',
747 restrict_includes=True): 1048 restrict_includes=True):
748 """Parse a manifest XML and return the computed nodes. 1049 """Parse a manifest XML and return the computed nodes.
@@ -832,6 +1133,20 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
832 if self._default is None: 1133 if self._default is None:
833 self._default = _Default() 1134 self._default = _Default()
834 1135
1136 submanifest_paths = set()
1137 for node in itertools.chain(*node_list):
1138 if node.nodeName == 'submanifest':
1139 submanifest = self._ParseSubmanifest(node)
1140 if submanifest:
1141 if submanifest.name in self._submanifests:
1142 if submanifest != self._submanifests[submanifest.name]:
1143 raise ManifestParseError(
1144 'submanifest %s already exists with different attributes' %
1145 (submanifest.name))
1146 else:
1147 self._submanifests[submanifest.name] = submanifest
1148 submanifest_paths.add(submanifest.relpath)
1149
835 for node in itertools.chain(*node_list): 1150 for node in itertools.chain(*node_list):
836 if node.nodeName == 'notice': 1151 if node.nodeName == 'notice':
837 if self._notice is not None: 1152 if self._notice is not None:
@@ -859,6 +1174,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
859 raise ManifestParseError( 1174 raise ManifestParseError(
860 'duplicate path %s in %s' % 1175 'duplicate path %s in %s' %
861 (project.relpath, self.manifestFile)) 1176 (project.relpath, self.manifestFile))
1177 for tree in submanifest_paths:
1178 if project.relpath.startswith(tree):
1179 raise ManifestParseError(
1180 'project %s conflicts with submanifest path %s' %
1181 (project.relpath, tree))
862 self._paths[project.relpath] = project 1182 self._paths[project.relpath] = project
863 projects.append(project) 1183 projects.append(project)
864 for subproject in project.subprojects: 1184 for subproject in project.subprojects:
@@ -883,8 +1203,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
883 if groups: 1203 if groups:
884 groups = self._ParseList(groups) 1204 groups = self._ParseList(groups)
885 revision = node.getAttribute('revision') 1205 revision = node.getAttribute('revision')
886 remote = node.getAttribute('remote') 1206 remote_name = node.getAttribute('remote')
887 if remote: 1207 if not remote_name:
1208 remote = self._default.remote
1209 else:
888 remote = self._get_remote(node) 1210 remote = self._get_remote(node)
889 1211
890 named_projects = self._projects[name] 1212 named_projects = self._projects[name]
@@ -899,12 +1221,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
899 if revision: 1221 if revision:
900 p.SetRevision(revision) 1222 p.SetRevision(revision)
901 1223
902 if remote: 1224 if remote_name:
903 p.remote = remote.ToRemoteSpec(name) 1225 p.remote = remote.ToRemoteSpec(name)
904 1226
905 if dest_path: 1227 if dest_path:
906 del self._paths[p.relpath] 1228 del self._paths[p.relpath]
907 relpath, worktree, gitdir, objdir, _ = self.GetProjectPaths(name, dest_path) 1229 relpath, worktree, gitdir, objdir, _ = self.GetProjectPaths(
1230 name, dest_path, remote.name)
908 p.UpdatePaths(relpath, worktree, gitdir, objdir) 1231 p.UpdatePaths(relpath, worktree, gitdir, objdir)
909 self._paths[p.relpath] = p 1232 self._paths[p.relpath] = p
910 1233
@@ -1109,6 +1432,53 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
1109 1432
1110 return '\n'.join(cleanLines) 1433 return '\n'.join(cleanLines)
1111 1434
1435 def _ParseSubmanifest(self, node):
1436 """Reads a <submanifest> element from the manifest file."""
1437 name = self._reqatt(node, 'name')
1438 remote = node.getAttribute('remote')
1439 if remote == '':
1440 remote = None
1441 project = node.getAttribute('project')
1442 if project == '':
1443 project = None
1444 revision = node.getAttribute('revision')
1445 if revision == '':
1446 revision = None
1447 manifestName = node.getAttribute('manifest-name')
1448 if manifestName == '':
1449 manifestName = None
1450 groups = ''
1451 if node.hasAttribute('groups'):
1452 groups = node.getAttribute('groups')
1453 groups = self._ParseList(groups)
1454 path = node.getAttribute('path')
1455 if path == '':
1456 path = None
1457 if revision:
1458 msg = self._CheckLocalPath(revision.split('/')[-1])
1459 if msg:
1460 raise ManifestInvalidPathError(
1461 '<submanifest> invalid "revision": %s: %s' % (revision, msg))
1462 else:
1463 msg = self._CheckLocalPath(name)
1464 if msg:
1465 raise ManifestInvalidPathError(
1466 '<submanifest> invalid "name": %s: %s' % (name, msg))
1467 else:
1468 msg = self._CheckLocalPath(path)
1469 if msg:
1470 raise ManifestInvalidPathError(
1471 '<submanifest> invalid "path": %s: %s' % (path, msg))
1472
1473 submanifest = _XmlSubmanifest(name, remote, project, revision, manifestName,
1474 groups, path, self)
1475
1476 for n in node.childNodes:
1477 if n.nodeName == 'annotation':
1478 self._ParseAnnotation(submanifest, n)
1479
1480 return submanifest
1481
1112 def _JoinName(self, parent_name, name): 1482 def _JoinName(self, parent_name, name):
1113 return os.path.join(parent_name, name) 1483 return os.path.join(parent_name, name)
1114 1484
@@ -1172,7 +1542,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
1172 1542
1173 if parent is None: 1543 if parent is None:
1174 relpath, worktree, gitdir, objdir, use_git_worktrees = \ 1544 relpath, worktree, gitdir, objdir, use_git_worktrees = \
1175 self.GetProjectPaths(name, path) 1545 self.GetProjectPaths(name, path, remote.name)
1176 else: 1546 else:
1177 use_git_worktrees = False 1547 use_git_worktrees = False
1178 relpath, worktree, gitdir, objdir = \ 1548 relpath, worktree, gitdir, objdir = \
@@ -1218,31 +1588,54 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
1218 1588
1219 return project 1589 return project
1220 1590
1221 def GetProjectPaths(self, name, path): 1591 def GetProjectPaths(self, name, path, remote):
1592 """Return the paths for a project.
1593
1594 Args:
1595 name: a string, the name of the project.
1596 path: a string, the path of the project.
1597 remote: a string, the remote.name of the project.
1598 """
1222 # The manifest entries might have trailing slashes. Normalize them to avoid 1599 # The manifest entries might have trailing slashes. Normalize them to avoid
1223 # unexpected filesystem behavior since we do string concatenation below. 1600 # unexpected filesystem behavior since we do string concatenation below.
1224 path = path.rstrip('/') 1601 path = path.rstrip('/')
1225 name = name.rstrip('/') 1602 name = name.rstrip('/')
1603 remote = remote.rstrip('/')
1226 use_git_worktrees = False 1604 use_git_worktrees = False
1605 use_remote_name = bool(self._outer_client._submanifests)
1227 relpath = path 1606 relpath = path
1228 if self.IsMirror: 1607 if self.IsMirror:
1229 worktree = None 1608 worktree = None
1230 gitdir = os.path.join(self.topdir, '%s.git' % name) 1609 gitdir = os.path.join(self.topdir, '%s.git' % name)
1231 objdir = gitdir 1610 objdir = gitdir
1232 else: 1611 else:
1612 if use_remote_name:
1613 namepath = os.path.join(remote, f'{name}.git')
1614 else:
1615 namepath = f'{name}.git'
1233 worktree = os.path.join(self.topdir, path).replace('\\', '/') 1616 worktree = os.path.join(self.topdir, path).replace('\\', '/')
1234 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path) 1617 gitdir = os.path.join(self.subdir, 'projects', '%s.git' % path)
1235 # We allow people to mix git worktrees & non-git worktrees for now. 1618 # We allow people to mix git worktrees & non-git worktrees for now.
1236 # This allows for in situ migration of repo clients. 1619 # This allows for in situ migration of repo clients.
1237 if os.path.exists(gitdir) or not self.UseGitWorktrees: 1620 if os.path.exists(gitdir) or not self.UseGitWorktrees:
1238 objdir = os.path.join(self.repodir, 'project-objects', '%s.git' % name) 1621 objdir = os.path.join(self.subdir, 'project-objects', namepath)
1239 else: 1622 else:
1240 use_git_worktrees = True 1623 use_git_worktrees = True
1241 gitdir = os.path.join(self.repodir, 'worktrees', '%s.git' % name) 1624 gitdir = os.path.join(self.repodir, 'worktrees', namepath)
1242 objdir = gitdir 1625 objdir = gitdir
1243 return relpath, worktree, gitdir, objdir, use_git_worktrees 1626 return relpath, worktree, gitdir, objdir, use_git_worktrees
1244 1627
1245 def GetProjectsWithName(self, name): 1628 def GetProjectsWithName(self, name, all_manifests=False):
1629 """All projects with |name|.
1630
1631 Args:
1632 name: a string, the name of the project.
1633 all_manifests: a boolean, if True, then all manifests are searched. If
1634 False, then only this manifest is searched.
1635 """
1636 if all_manifests:
1637 return list(itertools.chain.from_iterable(
1638 x._projects.get(name, []) for x in self.all_manifests))
1246 return self._projects.get(name, []) 1639 return self._projects.get(name, [])
1247 1640
1248 def GetSubprojectName(self, parent, submodule_path): 1641 def GetSubprojectName(self, parent, submodule_path):
@@ -1498,19 +1891,26 @@ class GitcManifest(XmlManifest):
1498class RepoClient(XmlManifest): 1891class RepoClient(XmlManifest):
1499 """Manages a repo client checkout.""" 1892 """Manages a repo client checkout."""
1500 1893
1501 def __init__(self, repodir, manifest_file=None): 1894 def __init__(self, repodir, manifest_file=None, submanifest_path='', **kwargs):
1502 self.isGitcClient = False 1895 self.isGitcClient = False
1896 submanifest_path = submanifest_path or ''
1897 if submanifest_path:
1898 self._CheckLocalPath(submanifest_path)
1899 prefix = os.path.join(repodir, SUBMANIFEST_DIR, submanifest_path)
1900 else:
1901 prefix = repodir
1503 1902
1504 if os.path.exists(os.path.join(repodir, LOCAL_MANIFEST_NAME)): 1903 if os.path.exists(os.path.join(prefix, LOCAL_MANIFEST_NAME)):
1505 print('error: %s is not supported; put local manifests in `%s` instead' % 1904 print('error: %s is not supported; put local manifests in `%s` instead' %
1506 (LOCAL_MANIFEST_NAME, os.path.join(repodir, LOCAL_MANIFESTS_DIR_NAME)), 1905 (LOCAL_MANIFEST_NAME, os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME)),
1507 file=sys.stderr) 1906 file=sys.stderr)
1508 sys.exit(1) 1907 sys.exit(1)
1509 1908
1510 if manifest_file is None: 1909 if manifest_file is None:
1511 manifest_file = os.path.join(repodir, MANIFEST_FILE_NAME) 1910 manifest_file = os.path.join(prefix, MANIFEST_FILE_NAME)
1512 local_manifests = os.path.abspath(os.path.join(repodir, LOCAL_MANIFESTS_DIR_NAME)) 1911 local_manifests = os.path.abspath(os.path.join(prefix, LOCAL_MANIFESTS_DIR_NAME))
1513 super().__init__(repodir, manifest_file, local_manifests) 1912 super().__init__(repodir, manifest_file, local_manifests,
1913 submanifest_path=submanifest_path, **kwargs)
1514 1914
1515 # TODO: Completely separate manifest logic out of the client. 1915 # TODO: Completely separate manifest logic out of the client.
1516 self.manifest = self 1916 self.manifest = self
diff --git a/project.py b/project.py
index ff91d189..480dac63 100644
--- a/project.py
+++ b/project.py
@@ -546,6 +546,18 @@ class Project(object):
546 # project containing repo hooks. 546 # project containing repo hooks.
547 self.enabled_repo_hooks = [] 547 self.enabled_repo_hooks = []
548 548
549 def RelPath(self, local=True):
550 """Return the path for the project relative to a manifest.
551
552 Args:
553 local: a boolean, if True, the path is relative to the local
554 (sub)manifest. If false, the path is relative to the
555 outermost manifest.
556 """
557 if local:
558 return self.relpath
559 return os.path.join(self.manifest.path_prefix, self.relpath)
560
549 def SetRevision(self, revisionExpr, revisionId=None): 561 def SetRevision(self, revisionExpr, revisionId=None):
550 """Set revisionId based on revision expression and id""" 562 """Set revisionId based on revision expression and id"""
551 self.revisionExpr = revisionExpr 563 self.revisionExpr = revisionExpr
@@ -2503,22 +2515,21 @@ class Project(object):
2503 mp = self.manifest.manifestProject 2515 mp = self.manifest.manifestProject
2504 ref_dir = mp.config.GetString('repo.reference') or '' 2516 ref_dir = mp.config.GetString('repo.reference') or ''
2505 2517
2518 def _expanded_ref_dirs():
2519 """Iterate through the possible git reference directory paths."""
2520 name = self.name + '.git'
2521 yield mirror_git or os.path.join(ref_dir, name)
2522 for prefix in '', self.remote.name:
2523 yield os.path.join(ref_dir, '.repo', 'project-objects', prefix, name)
2524 yield os.path.join(ref_dir, '.repo', 'worktrees', prefix, name)
2525
2506 if ref_dir or mirror_git: 2526 if ref_dir or mirror_git:
2507 if not mirror_git: 2527 found_ref_dir = None
2508 mirror_git = os.path.join(ref_dir, self.name + '.git') 2528 for path in _expanded_ref_dirs():
2509 repo_git = os.path.join(ref_dir, '.repo', 'project-objects', 2529 if os.path.exists(path):
2510 self.name + '.git') 2530 found_ref_dir = path
2511 worktrees_git = os.path.join(ref_dir, '.repo', 'worktrees', 2531 break
2512 self.name + '.git') 2532 ref_dir = found_ref_dir
2513
2514 if os.path.exists(mirror_git):
2515 ref_dir = mirror_git
2516 elif os.path.exists(repo_git):
2517 ref_dir = repo_git
2518 elif os.path.exists(worktrees_git):
2519 ref_dir = worktrees_git
2520 else:
2521 ref_dir = None
2522 2533
2523 if ref_dir: 2534 if ref_dir:
2524 if not os.path.isabs(ref_dir): 2535 if not os.path.isabs(ref_dir):
diff --git a/subcmds/abandon.py b/subcmds/abandon.py
index 85d85f5a..c3d2d5b7 100644
--- a/subcmds/abandon.py
+++ b/subcmds/abandon.py
@@ -69,7 +69,8 @@ It is equivalent to "git branch -D <branchname>".
69 nb = args[0] 69 nb = args[0]
70 err = defaultdict(list) 70 err = defaultdict(list)
71 success = defaultdict(list) 71 success = defaultdict(list)
72 all_projects = self.GetProjects(args[1:]) 72 all_projects = self.GetProjects(args[1:], all_manifests=not opt.this_manifest_only)
73 _RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
73 74
74 def _ProcessResults(_pool, pm, states): 75 def _ProcessResults(_pool, pm, states):
75 for (results, project) in states: 76 for (results, project) in states:
@@ -94,7 +95,7 @@ It is equivalent to "git branch -D <branchname>".
94 err_msg = "error: cannot abandon %s" % br 95 err_msg = "error: cannot abandon %s" % br
95 print(err_msg, file=sys.stderr) 96 print(err_msg, file=sys.stderr)
96 for proj in err[br]: 97 for proj in err[br]:
97 print(' ' * len(err_msg) + " | %s" % proj.relpath, file=sys.stderr) 98 print(' ' * len(err_msg) + " | %s" % _RelPath(proj), file=sys.stderr)
98 sys.exit(1) 99 sys.exit(1)
99 elif not success: 100 elif not success:
100 print('error: no project has local branch(es) : %s' % nb, 101 print('error: no project has local branch(es) : %s' % nb,
@@ -110,5 +111,5 @@ It is equivalent to "git branch -D <branchname>".
110 result = "all project" 111 result = "all project"
111 else: 112 else:
112 result = "%s" % ( 113 result = "%s" % (
113 ('\n' + ' ' * width + '| ').join(p.relpath for p in success[br])) 114 ('\n' + ' ' * width + '| ').join(_RelPath(p) for p in success[br]))
114 print("%s%s| %s\n" % (br, ' ' * (width - len(br)), result)) 115 print("%s%s| %s\n" % (br, ' ' * (width - len(br)), result))
diff --git a/subcmds/branches.py b/subcmds/branches.py
index 7b5decc6..b89cc2f8 100644
--- a/subcmds/branches.py
+++ b/subcmds/branches.py
@@ -98,7 +98,7 @@ is shown, then the branch appears in all projects.
98 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS 98 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
99 99
100 def Execute(self, opt, args): 100 def Execute(self, opt, args):
101 projects = self.GetProjects(args) 101 projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
102 out = BranchColoring(self.manifest.manifestProject.config) 102 out = BranchColoring(self.manifest.manifestProject.config)
103 all_branches = {} 103 all_branches = {}
104 project_cnt = len(projects) 104 project_cnt = len(projects)
@@ -147,6 +147,7 @@ is shown, then the branch appears in all projects.
147 hdr('%c%c %-*s' % (current, published, width, name)) 147 hdr('%c%c %-*s' % (current, published, width, name))
148 out.write(' |') 148 out.write(' |')
149 149
150 _RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
150 if in_cnt < project_cnt: 151 if in_cnt < project_cnt:
151 fmt = out.write 152 fmt = out.write
152 paths = [] 153 paths = []
@@ -154,19 +155,20 @@ is shown, then the branch appears in all projects.
154 if i.IsSplitCurrent or (in_cnt <= project_cnt - in_cnt): 155 if i.IsSplitCurrent or (in_cnt <= project_cnt - in_cnt):
155 in_type = 'in' 156 in_type = 'in'
156 for b in i.projects: 157 for b in i.projects:
158 relpath = b.project.relpath
157 if not i.IsSplitCurrent or b.current: 159 if not i.IsSplitCurrent or b.current:
158 paths.append(b.project.relpath) 160 paths.append(_RelPath(b.project))
159 else: 161 else:
160 non_cur_paths.append(b.project.relpath) 162 non_cur_paths.append(_RelPath(b.project))
161 else: 163 else:
162 fmt = out.notinproject 164 fmt = out.notinproject
163 in_type = 'not in' 165 in_type = 'not in'
164 have = set() 166 have = set()
165 for b in i.projects: 167 for b in i.projects:
166 have.add(b.project.relpath) 168 have.add(_RelPath(b.project))
167 for p in projects: 169 for p in projects:
168 if p.relpath not in have: 170 if _RelPath(p) not in have:
169 paths.append(p.relpath) 171 paths.append(_RelPath(p))
170 172
171 s = ' %s %s' % (in_type, ', '.join(paths)) 173 s = ' %s %s' % (in_type, ', '.join(paths))
172 if not i.IsSplitCurrent and (width + 7 + len(s) < 80): 174 if not i.IsSplitCurrent and (width + 7 + len(s) < 80):
diff --git a/subcmds/checkout.py b/subcmds/checkout.py
index 9b429489..768b6027 100644
--- a/subcmds/checkout.py
+++ b/subcmds/checkout.py
@@ -47,7 +47,7 @@ The command is equivalent to:
47 nb = args[0] 47 nb = args[0]
48 err = [] 48 err = []
49 success = [] 49 success = []
50 all_projects = self.GetProjects(args[1:]) 50 all_projects = self.GetProjects(args[1:], all_manifests=not opt.this_manifest_only)
51 51
52 def _ProcessResults(_pool, pm, results): 52 def _ProcessResults(_pool, pm, results):
53 for status, project in results: 53 for status, project in results:
diff --git a/subcmds/diff.py b/subcmds/diff.py
index 00a7ec29..a1f4ba88 100644
--- a/subcmds/diff.py
+++ b/subcmds/diff.py
@@ -50,7 +50,7 @@ to the Unix 'patch' command.
50 return (ret, buf.getvalue()) 50 return (ret, buf.getvalue())
51 51
52 def Execute(self, opt, args): 52 def Execute(self, opt, args):
53 all_projects = self.GetProjects(args) 53 all_projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
54 54
55 def _ProcessResults(_pool, _output, results): 55 def _ProcessResults(_pool, _output, results):
56 ret = 0 56 ret = 0
diff --git a/subcmds/diffmanifests.py b/subcmds/diffmanifests.py
index f6cc30a2..0e5f4108 100644
--- a/subcmds/diffmanifests.py
+++ b/subcmds/diffmanifests.py
@@ -179,6 +179,9 @@ synced and their revisions won't be found.
179 def ValidateOptions(self, opt, args): 179 def ValidateOptions(self, opt, args):
180 if not args or len(args) > 2: 180 if not args or len(args) > 2:
181 self.OptionParser.error('missing manifests to diff') 181 self.OptionParser.error('missing manifests to diff')
182 if opt.this_manifest_only is False:
183 raise self.OptionParser.error(
184 '`diffmanifest` only supports the current tree')
182 185
183 def Execute(self, opt, args): 186 def Execute(self, opt, args):
184 self.out = _Coloring(self.client.globalConfig) 187 self.out = _Coloring(self.client.globalConfig)
diff --git a/subcmds/download.py b/subcmds/download.py
index 523f25e0..15824843 100644
--- a/subcmds/download.py
+++ b/subcmds/download.py
@@ -48,7 +48,7 @@ If no project is specified try to use current directory as a project.
48 dest='ffonly', action='store_true', 48 dest='ffonly', action='store_true',
49 help="force fast-forward merge") 49 help="force fast-forward merge")
50 50
51 def _ParseChangeIds(self, args): 51 def _ParseChangeIds(self, opt, args):
52 if not args: 52 if not args:
53 self.Usage() 53 self.Usage()
54 54
@@ -77,7 +77,7 @@ If no project is specified try to use current directory as a project.
77 ps_id = max(int(match.group(1)), ps_id) 77 ps_id = max(int(match.group(1)), ps_id)
78 to_get.append((project, chg_id, ps_id)) 78 to_get.append((project, chg_id, ps_id))
79 else: 79 else:
80 projects = self.GetProjects([a]) 80 projects = self.GetProjects([a], all_manifests=not opt.this_manifest_only)
81 if len(projects) > 1: 81 if len(projects) > 1:
82 # If the cwd is one of the projects, assume they want that. 82 # If the cwd is one of the projects, assume they want that.
83 try: 83 try:
@@ -88,8 +88,8 @@ If no project is specified try to use current directory as a project.
88 print('error: %s matches too many projects; please re-run inside ' 88 print('error: %s matches too many projects; please re-run inside '
89 'the project checkout.' % (a,), file=sys.stderr) 89 'the project checkout.' % (a,), file=sys.stderr)
90 for project in projects: 90 for project in projects:
91 print(' %s/ @ %s' % (project.relpath, project.revisionExpr), 91 print(' %s/ @ %s' % (project.RelPath(local=opt.this_manifest_only),
92 file=sys.stderr) 92 project.revisionExpr), file=sys.stderr)
93 sys.exit(1) 93 sys.exit(1)
94 else: 94 else:
95 project = projects[0] 95 project = projects[0]
@@ -105,7 +105,7 @@ If no project is specified try to use current directory as a project.
105 self.OptionParser.error('-x and --ff are mutually exclusive options') 105 self.OptionParser.error('-x and --ff are mutually exclusive options')
106 106
107 def Execute(self, opt, args): 107 def Execute(self, opt, args):
108 for project, change_id, ps_id in self._ParseChangeIds(args): 108 for project, change_id, ps_id in self._ParseChangeIds(opt, args):
109 dl = project.DownloadPatchSet(change_id, ps_id) 109 dl = project.DownloadPatchSet(change_id, ps_id)
110 if not dl: 110 if not dl:
111 print('[%s] change %d/%d not found' 111 print('[%s] change %d/%d not found'
diff --git a/subcmds/forall.py b/subcmds/forall.py
index 7c1dea9e..cc578b52 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -168,6 +168,7 @@ without iterating through the remaining projects.
168 168
169 def Execute(self, opt, args): 169 def Execute(self, opt, args):
170 cmd = [opt.command[0]] 170 cmd = [opt.command[0]]
171 all_trees = not opt.this_manifest_only
171 172
172 shell = True 173 shell = True
173 if re.compile(r'^[a-z0-9A-Z_/\.-]+$').match(cmd[0]): 174 if re.compile(r'^[a-z0-9A-Z_/\.-]+$').match(cmd[0]):
@@ -213,11 +214,11 @@ without iterating through the remaining projects.
213 self.manifest.Override(smart_sync_manifest_path) 214 self.manifest.Override(smart_sync_manifest_path)
214 215
215 if opt.regex: 216 if opt.regex:
216 projects = self.FindProjects(args) 217 projects = self.FindProjects(args, all_manifests=all_trees)
217 elif opt.inverse_regex: 218 elif opt.inverse_regex:
218 projects = self.FindProjects(args, inverse=True) 219 projects = self.FindProjects(args, inverse=True, all_manifests=all_trees)
219 else: 220 else:
220 projects = self.GetProjects(args, groups=opt.groups) 221 projects = self.GetProjects(args, groups=opt.groups, all_manifests=all_trees)
221 222
222 os.environ['REPO_COUNT'] = str(len(projects)) 223 os.environ['REPO_COUNT'] = str(len(projects))
223 224
@@ -290,6 +291,7 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
290 291
291 setenv('REPO_PROJECT', project.name) 292 setenv('REPO_PROJECT', project.name)
292 setenv('REPO_PATH', project.relpath) 293 setenv('REPO_PATH', project.relpath)
294 setenv('REPO_OUTERPATH', project.RelPath(local=opt.this_manifest_only))
293 setenv('REPO_REMOTE', project.remote.name) 295 setenv('REPO_REMOTE', project.remote.name)
294 try: 296 try:
295 # If we aren't in a fully synced state and we don't have the ref the manifest 297 # If we aren't in a fully synced state and we don't have the ref the manifest
@@ -320,7 +322,7 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
320 output = '' 322 output = ''
321 if ((opt.project_header and opt.verbose) 323 if ((opt.project_header and opt.verbose)
322 or not opt.project_header): 324 or not opt.project_header):
323 output = 'skipping %s/' % project.relpath 325 output = 'skipping %s/' % project.RelPath(local=opt.this_manifest_only)
324 return (1, output) 326 return (1, output)
325 327
326 if opt.verbose: 328 if opt.verbose:
@@ -344,7 +346,7 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
344 if mirror: 346 if mirror:
345 project_header_path = project.name 347 project_header_path = project.name
346 else: 348 else:
347 project_header_path = project.relpath 349 project_header_path = project.RelPath(local=opt.this_manifest_only)
348 out.project('project %s/' % project_header_path) 350 out.project('project %s/' % project_header_path)
349 out.nl() 351 out.nl()
350 buf.write(output) 352 buf.write(output)
diff --git a/subcmds/gitc_init.py b/subcmds/gitc_init.py
index e705b613..1d81baf5 100644
--- a/subcmds/gitc_init.py
+++ b/subcmds/gitc_init.py
@@ -24,6 +24,7 @@ import wrapper
24 24
25class GitcInit(init.Init, GitcAvailableCommand): 25class GitcInit(init.Init, GitcAvailableCommand):
26 COMMON = True 26 COMMON = True
27 MULTI_MANIFEST_SUPPORT = False
27 helpSummary = "Initialize a GITC Client." 28 helpSummary = "Initialize a GITC Client."
28 helpUsage = """ 29 helpUsage = """
29%prog [options] [client name] 30%prog [options] [client name]
diff --git a/subcmds/grep.py b/subcmds/grep.py
index 8ac4ba14..93c9ae51 100644
--- a/subcmds/grep.py
+++ b/subcmds/grep.py
@@ -172,15 +172,16 @@ contain a line that matches both expressions:
172 return (project, p.Wait(), p.stdout, p.stderr) 172 return (project, p.Wait(), p.stdout, p.stderr)
173 173
174 @staticmethod 174 @staticmethod
175 def _ProcessResults(full_name, have_rev, _pool, out, results): 175 def _ProcessResults(full_name, have_rev, opt, _pool, out, results):
176 git_failed = False 176 git_failed = False
177 bad_rev = False 177 bad_rev = False
178 have_match = False 178 have_match = False
179 _RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
179 180
180 for project, rc, stdout, stderr in results: 181 for project, rc, stdout, stderr in results:
181 if rc < 0: 182 if rc < 0:
182 git_failed = True 183 git_failed = True
183 out.project('--- project %s ---' % project.relpath) 184 out.project('--- project %s ---' % _RelPath(project))
184 out.nl() 185 out.nl()
185 out.fail('%s', stderr) 186 out.fail('%s', stderr)
186 out.nl() 187 out.nl()
@@ -192,7 +193,7 @@ contain a line that matches both expressions:
192 if have_rev and 'fatal: ambiguous argument' in stderr: 193 if have_rev and 'fatal: ambiguous argument' in stderr:
193 bad_rev = True 194 bad_rev = True
194 else: 195 else:
195 out.project('--- project %s ---' % project.relpath) 196 out.project('--- project %s ---' % _RelPath(project))
196 out.nl() 197 out.nl()
197 out.fail('%s', stderr.strip()) 198 out.fail('%s', stderr.strip())
198 out.nl() 199 out.nl()
@@ -208,13 +209,13 @@ contain a line that matches both expressions:
208 rev, line = line.split(':', 1) 209 rev, line = line.split(':', 1)
209 out.write("%s", rev) 210 out.write("%s", rev)
210 out.write(':') 211 out.write(':')
211 out.project(project.relpath) 212 out.project(_RelPath(project))
212 out.write('/') 213 out.write('/')
213 out.write("%s", line) 214 out.write("%s", line)
214 out.nl() 215 out.nl()
215 elif full_name: 216 elif full_name:
216 for line in r: 217 for line in r:
217 out.project(project.relpath) 218 out.project(_RelPath(project))
218 out.write('/') 219 out.write('/')
219 out.write("%s", line) 220 out.write("%s", line)
220 out.nl() 221 out.nl()
@@ -239,7 +240,7 @@ contain a line that matches both expressions:
239 cmd_argv.append(args[0]) 240 cmd_argv.append(args[0])
240 args = args[1:] 241 args = args[1:]
241 242
242 projects = self.GetProjects(args) 243 projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
243 244
244 full_name = False 245 full_name = False
245 if len(projects) > 1: 246 if len(projects) > 1:
@@ -259,7 +260,7 @@ contain a line that matches both expressions:
259 opt.jobs, 260 opt.jobs,
260 functools.partial(self._ExecuteOne, cmd_argv), 261 functools.partial(self._ExecuteOne, cmd_argv),
261 projects, 262 projects,
262 callback=functools.partial(self._ProcessResults, full_name, have_rev), 263 callback=functools.partial(self._ProcessResults, full_name, have_rev, opt),
263 output=out, 264 output=out,
264 ordered=True) 265 ordered=True)
265 266
diff --git a/subcmds/info.py b/subcmds/info.py
index 6c1246ef..4bedf9d5 100644
--- a/subcmds/info.py
+++ b/subcmds/info.py
@@ -61,6 +61,8 @@ class Info(PagedCommand):
61 61
62 self.opt = opt 62 self.opt = opt
63 63
64 if not opt.this_manifest_only:
65 self.manifest = self.manifest.outer_client
64 manifestConfig = self.manifest.manifestProject.config 66 manifestConfig = self.manifest.manifestProject.config
65 mergeBranch = manifestConfig.GetBranch("default").merge 67 mergeBranch = manifestConfig.GetBranch("default").merge
66 manifestGroups = (manifestConfig.GetString('manifest.groups') 68 manifestGroups = (manifestConfig.GetString('manifest.groups')
@@ -80,17 +82,17 @@ class Info(PagedCommand):
80 self.printSeparator() 82 self.printSeparator()
81 83
82 if not opt.overview: 84 if not opt.overview:
83 self.printDiffInfo(args) 85 self._printDiffInfo(opt, args)
84 else: 86 else:
85 self.printCommitOverview(args) 87 self._printCommitOverview(opt, args)
86 88
87 def printSeparator(self): 89 def printSeparator(self):
88 self.text("----------------------------") 90 self.text("----------------------------")
89 self.out.nl() 91 self.out.nl()
90 92
91 def printDiffInfo(self, args): 93 def _printDiffInfo(self, opt, args):
92 # We let exceptions bubble up to main as they'll be well structured. 94 # We let exceptions bubble up to main as they'll be well structured.
93 projs = self.GetProjects(args) 95 projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
94 96
95 for p in projs: 97 for p in projs:
96 self.heading("Project: ") 98 self.heading("Project: ")
@@ -179,9 +181,9 @@ class Info(PagedCommand):
179 self.text(" ".join(split[1:])) 181 self.text(" ".join(split[1:]))
180 self.out.nl() 182 self.out.nl()
181 183
182 def printCommitOverview(self, args): 184 def _printCommitOverview(self, opt, args):
183 all_branches = [] 185 all_branches = []
184 for project in self.GetProjects(args): 186 for project in self.GetProjects(args, all_manifests=not opt.this_manifest_only):
185 br = [project.GetUploadableBranch(x) 187 br = [project.GetUploadableBranch(x)
186 for x in project.GetBranches()] 188 for x in project.GetBranches()]
187 br = [x for x in br if x] 189 br = [x for x in br if x]
@@ -200,7 +202,7 @@ class Info(PagedCommand):
200 if project != branch.project: 202 if project != branch.project:
201 project = branch.project 203 project = branch.project
202 self.out.nl() 204 self.out.nl()
203 self.headtext(project.relpath) 205 self.headtext(project.RelPath(local=opt.this_manifest_only))
204 self.out.nl() 206 self.out.nl()
205 207
206 commits = branch.commits 208 commits = branch.commits
diff --git a/subcmds/init.py b/subcmds/init.py
index 32c85f79..b9775a34 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -32,6 +32,7 @@ from wrapper import Wrapper
32 32
33class Init(InteractiveCommand, MirrorSafeCommand): 33class Init(InteractiveCommand, MirrorSafeCommand):
34 COMMON = True 34 COMMON = True
35 MULTI_MANIFEST_SUPPORT = False
35 helpSummary = "Initialize a repo client checkout in the current directory" 36 helpSummary = "Initialize a repo client checkout in the current directory"
36 helpUsage = """ 37 helpUsage = """
37%prog [options] [manifest url] 38%prog [options] [manifest url]
@@ -90,6 +91,17 @@ to update the working directory files.
90 91
91 def _Options(self, p, gitc_init=False): 92 def _Options(self, p, gitc_init=False):
92 Wrapper().InitParser(p, gitc_init=gitc_init) 93 Wrapper().InitParser(p, gitc_init=gitc_init)
94 m = p.add_option_group('Multi-manifest')
95 m.add_option('--outer-manifest', action='store_true',
96 help='operate starting at the outermost manifest')
97 m.add_option('--no-outer-manifest', dest='outer_manifest',
98 action='store_false', default=None,
99 help='do not operate on outer manifests')
100 m.add_option('--this-manifest-only', action='store_true', default=None,
101 help='only operate on this (sub)manifest')
102 m.add_option('--no-this-manifest-only', '--all-manifests',
103 dest='this_manifest_only', action='store_false',
104 help='operate on this manifest and its submanifests')
93 105
94 def _RegisteredEnvironmentOptions(self): 106 def _RegisteredEnvironmentOptions(self):
95 return {'REPO_MANIFEST_URL': 'manifest_url', 107 return {'REPO_MANIFEST_URL': 'manifest_url',
diff --git a/subcmds/list.py b/subcmds/list.py
index 6adf85b7..ad8036ee 100644
--- a/subcmds/list.py
+++ b/subcmds/list.py
@@ -77,16 +77,17 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
77 args: Positional args. Can be a list of projects to list, or empty. 77 args: Positional args. Can be a list of projects to list, or empty.
78 """ 78 """
79 if not opt.regex: 79 if not opt.regex:
80 projects = self.GetProjects(args, groups=opt.groups, missing_ok=opt.all) 80 projects = self.GetProjects(args, groups=opt.groups, missing_ok=opt.all,
81 all_manifests=not opt.this_manifest_only)
81 else: 82 else:
82 projects = self.FindProjects(args) 83 projects = self.FindProjects(args, all_manifests=not opt.this_manifest_only)
83 84
84 def _getpath(x): 85 def _getpath(x):
85 if opt.fullpath: 86 if opt.fullpath:
86 return x.worktree 87 return x.worktree
87 if opt.relative_to: 88 if opt.relative_to:
88 return os.path.relpath(x.worktree, opt.relative_to) 89 return os.path.relpath(x.worktree, opt.relative_to)
89 return x.relpath 90 return x.RelPath(local=opt.this_manifest_only)
90 91
91 lines = [] 92 lines = []
92 for project in projects: 93 for project in projects:
diff --git a/subcmds/manifest.py b/subcmds/manifest.py
index 0fbdeac0..08905cb4 100644
--- a/subcmds/manifest.py
+++ b/subcmds/manifest.py
@@ -15,6 +15,7 @@
15import json 15import json
16import os 16import os
17import sys 17import sys
18import optparse
18 19
19from command import PagedCommand 20from command import PagedCommand
20 21
@@ -75,7 +76,7 @@ to indicate the remote ref to push changes to via 'repo upload'.
75 p.add_option('-o', '--output-file', 76 p.add_option('-o', '--output-file',
76 dest='output_file', 77 dest='output_file',
77 default='-', 78 default='-',
78 help='file to save the manifest to', 79 help='file to save the manifest to. (Filename prefix for multi-tree.)',
79 metavar='-|NAME.xml') 80 metavar='-|NAME.xml')
80 81
81 def _Output(self, opt): 82 def _Output(self, opt):
@@ -83,36 +84,45 @@ to indicate the remote ref to push changes to via 'repo upload'.
83 if opt.manifest_name: 84 if opt.manifest_name:
84 self.manifest.Override(opt.manifest_name, False) 85 self.manifest.Override(opt.manifest_name, False)
85 86
86 if opt.output_file == '-': 87 for manifest in self.ManifestList(opt):
87 fd = sys.stdout 88 output_file = opt.output_file
88 else: 89 if output_file == '-':
89 fd = open(opt.output_file, 'w') 90 fd = sys.stdout
90 91 else:
91 self.manifest.SetUseLocalManifests(not opt.ignore_local_manifests) 92 if manifest.path_prefix:
92 93 output_file = f'{opt.output_file}:{manifest.path_prefix.replace("/", "%2f")}'
93 if opt.json: 94 fd = open(output_file, 'w')
94 print('warning: --json is experimental!', file=sys.stderr) 95
95 doc = self.manifest.ToDict(peg_rev=opt.peg_rev, 96 manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
96 peg_rev_upstream=opt.peg_rev_upstream, 97
97 peg_rev_dest_branch=opt.peg_rev_dest_branch) 98 if opt.json:
98 99 print('warning: --json is experimental!', file=sys.stderr)
99 json_settings = { 100 doc = manifest.ToDict(peg_rev=opt.peg_rev,
100 # JSON style guide says Uunicode characters are fully allowed. 101 peg_rev_upstream=opt.peg_rev_upstream,
101 'ensure_ascii': False, 102 peg_rev_dest_branch=opt.peg_rev_dest_branch)
102 # We use 2 space indent to match JSON style guide. 103
103 'indent': 2 if opt.pretty else None, 104 json_settings = {
104 'separators': (',', ': ') if opt.pretty else (',', ':'), 105 # JSON style guide says Uunicode characters are fully allowed.
105 'sort_keys': True, 106 'ensure_ascii': False,
106 } 107 # We use 2 space indent to match JSON style guide.
107 fd.write(json.dumps(doc, **json_settings)) 108 'indent': 2 if opt.pretty else None,
108 else: 109 'separators': (',', ': ') if opt.pretty else (',', ':'),
109 self.manifest.Save(fd, 110 'sort_keys': True,
110 peg_rev=opt.peg_rev, 111 }
111 peg_rev_upstream=opt.peg_rev_upstream, 112 fd.write(json.dumps(doc, **json_settings))
112 peg_rev_dest_branch=opt.peg_rev_dest_branch) 113 else:
113 fd.close() 114 manifest.Save(fd,
114 if opt.output_file != '-': 115 peg_rev=opt.peg_rev,
115 print('Saved manifest to %s' % opt.output_file, file=sys.stderr) 116 peg_rev_upstream=opt.peg_rev_upstream,
117 peg_rev_dest_branch=opt.peg_rev_dest_branch)
118 if output_file != '-':
119 fd.close()
120 if manifest.path_prefix:
121 print(f'Saved {manifest.path_prefix} submanifest to {output_file}',
122 file=sys.stderr)
123 else:
124 print(f'Saved manifest to {output_file}', file=sys.stderr)
125
116 126
117 def ValidateOptions(self, opt, args): 127 def ValidateOptions(self, opt, args):
118 if args: 128 if args:
diff --git a/subcmds/overview.py b/subcmds/overview.py
index 63f5a79e..11dba95f 100644
--- a/subcmds/overview.py
+++ b/subcmds/overview.py
@@ -47,7 +47,7 @@ are displayed.
47 47
48 def Execute(self, opt, args): 48 def Execute(self, opt, args):
49 all_branches = [] 49 all_branches = []
50 for project in self.GetProjects(args): 50 for project in self.GetProjects(args, all_manifests=not opt.this_manifest_only):
51 br = [project.GetUploadableBranch(x) 51 br = [project.GetUploadableBranch(x)
52 for x in project.GetBranches()] 52 for x in project.GetBranches()]
53 br = [x for x in br if x] 53 br = [x for x in br if x]
@@ -76,7 +76,7 @@ are displayed.
76 if project != branch.project: 76 if project != branch.project:
77 project = branch.project 77 project = branch.project
78 out.nl() 78 out.nl()
79 out.project('project %s/' % project.relpath) 79 out.project('project %s/' % project.RelPath(local=opt.this_manifest_only))
80 out.nl() 80 out.nl()
81 81
82 commits = branch.commits 82 commits = branch.commits
diff --git a/subcmds/prune.py b/subcmds/prune.py
index 584ee7ed..251accaa 100644
--- a/subcmds/prune.py
+++ b/subcmds/prune.py
@@ -31,7 +31,7 @@ class Prune(PagedCommand):
31 return project.PruneHeads() 31 return project.PruneHeads()
32 32
33 def Execute(self, opt, args): 33 def Execute(self, opt, args):
34 projects = self.GetProjects(args) 34 projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
35 35
36 # NB: Should be able to refactor this module to display summary as results 36 # NB: Should be able to refactor this module to display summary as results
37 # come back from children. 37 # come back from children.
@@ -63,7 +63,7 @@ class Prune(PagedCommand):
63 if project != branch.project: 63 if project != branch.project:
64 project = branch.project 64 project = branch.project
65 out.nl() 65 out.nl()
66 out.project('project %s/' % project.relpath) 66 out.project('project %s/' % project.RelPath(local=opt.this_manifest_only))
67 out.nl() 67 out.nl()
68 68
69 print('%s %-33s ' % ( 69 print('%s %-33s ' % (
diff --git a/subcmds/rebase.py b/subcmds/rebase.py
index 7c53eb7a..3d1a63e4 100644
--- a/subcmds/rebase.py
+++ b/subcmds/rebase.py
@@ -69,7 +69,7 @@ branch but need to incorporate new upstream changes "underneath" them.
69 'consistent if you previously synced to a manifest)') 69 'consistent if you previously synced to a manifest)')
70 70
71 def Execute(self, opt, args): 71 def Execute(self, opt, args):
72 all_projects = self.GetProjects(args) 72 all_projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
73 one_project = len(all_projects) == 1 73 one_project = len(all_projects) == 1
74 74
75 if opt.interactive and not one_project: 75 if opt.interactive and not one_project:
@@ -98,6 +98,7 @@ branch but need to incorporate new upstream changes "underneath" them.
98 config = self.manifest.manifestProject.config 98 config = self.manifest.manifestProject.config
99 out = RebaseColoring(config) 99 out = RebaseColoring(config)
100 out.redirect(sys.stdout) 100 out.redirect(sys.stdout)
101 _RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
101 102
102 ret = 0 103 ret = 0
103 for project in all_projects: 104 for project in all_projects:
@@ -107,7 +108,7 @@ branch but need to incorporate new upstream changes "underneath" them.
107 cb = project.CurrentBranch 108 cb = project.CurrentBranch
108 if not cb: 109 if not cb:
109 if one_project: 110 if one_project:
110 print("error: project %s has a detached HEAD" % project.relpath, 111 print("error: project %s has a detached HEAD" % _RelPath(project),
111 file=sys.stderr) 112 file=sys.stderr)
112 return 1 113 return 1
113 # ignore branches with detatched HEADs 114 # ignore branches with detatched HEADs
@@ -117,7 +118,7 @@ branch but need to incorporate new upstream changes "underneath" them.
117 if not upbranch.LocalMerge: 118 if not upbranch.LocalMerge:
118 if one_project: 119 if one_project:
119 print("error: project %s does not track any remote branches" 120 print("error: project %s does not track any remote branches"
120 % project.relpath, file=sys.stderr) 121 % _RelPath(project), file=sys.stderr)
121 return 1 122 return 1
122 # ignore branches without remotes 123 # ignore branches without remotes
123 continue 124 continue
@@ -130,7 +131,7 @@ branch but need to incorporate new upstream changes "underneath" them.
130 args.append(upbranch.LocalMerge) 131 args.append(upbranch.LocalMerge)
131 132
132 out.project('project %s: rebasing %s -> %s', 133 out.project('project %s: rebasing %s -> %s',
133 project.relpath, cb, upbranch.LocalMerge) 134 _RelPath(project), cb, upbranch.LocalMerge)
134 out.nl() 135 out.nl()
135 out.flush() 136 out.flush()
136 137
diff --git a/subcmds/stage.py b/subcmds/stage.py
index 0389a4ff..5f17cb64 100644
--- a/subcmds/stage.py
+++ b/subcmds/stage.py
@@ -50,7 +50,9 @@ The '%prog' command stages files to prepare the next commit.
50 self.Usage() 50 self.Usage()
51 51
52 def _Interactive(self, opt, args): 52 def _Interactive(self, opt, args):
53 all_projects = [p for p in self.GetProjects(args) if p.IsDirty()] 53 all_projects = [
54 p for p in self.GetProjects(args, all_manifests=not opt.this_manifest_only)
55 if p.IsDirty()]
54 if not all_projects: 56 if not all_projects:
55 print('no projects have uncommitted modifications', file=sys.stderr) 57 print('no projects have uncommitted modifications', file=sys.stderr)
56 return 58 return
@@ -62,7 +64,8 @@ The '%prog' command stages files to prepare the next commit.
62 64
63 for i in range(len(all_projects)): 65 for i in range(len(all_projects)):
64 project = all_projects[i] 66 project = all_projects[i]
65 out.write('%3d: %s', i + 1, project.relpath + '/') 67 out.write('%3d: %s', i + 1,
68 project.RelPath(local=opt.this_manifest_only) + '/')
66 out.nl() 69 out.nl()
67 out.nl() 70 out.nl()
68 71
@@ -99,7 +102,9 @@ The '%prog' command stages files to prepare the next commit.
99 _AddI(all_projects[a_index - 1]) 102 _AddI(all_projects[a_index - 1])
100 continue 103 continue
101 104
102 projects = [p for p in all_projects if a in [p.name, p.relpath]] 105 projects = [
106 p for p in all_projects
107 if a in [p.name, p.RelPath(local=opt.this_manifest_only)]]
103 if len(projects) == 1: 108 if len(projects) == 1:
104 _AddI(projects[0]) 109 _AddI(projects[0])
105 continue 110 continue
diff --git a/subcmds/start.py b/subcmds/start.py
index 2addaf2e..809df963 100644
--- a/subcmds/start.py
+++ b/subcmds/start.py
@@ -84,7 +84,8 @@ revision specified in the manifest.
84 projects = ['.'] # start it in the local project by default 84 projects = ['.'] # start it in the local project by default
85 85
86 all_projects = self.GetProjects(projects, 86 all_projects = self.GetProjects(projects,
87 missing_ok=bool(self.gitc_manifest)) 87 missing_ok=bool(self.gitc_manifest),
88 all_manifests=not opt.this_manifest_only)
88 89
89 # This must happen after we find all_projects, since GetProjects may need 90 # This must happen after we find all_projects, since GetProjects may need
90 # the local directory, which will disappear once we save the GITC manifest. 91 # the local directory, which will disappear once we save the GITC manifest.
@@ -137,6 +138,6 @@ revision specified in the manifest.
137 138
138 if err: 139 if err:
139 for p in err: 140 for p in err:
140 print("error: %s/: cannot start %s" % (p.relpath, nb), 141 print("error: %s/: cannot start %s" % (p.RelPath(local=opt.this_manifest_only), nb),
141 file=sys.stderr) 142 file=sys.stderr)
142 sys.exit(1) 143 sys.exit(1)
diff --git a/subcmds/status.py b/subcmds/status.py
index 5b669547..0aa4200f 100644
--- a/subcmds/status.py
+++ b/subcmds/status.py
@@ -117,7 +117,7 @@ the following meanings:
117 outstring.append(''.join([status_header, item, '/'])) 117 outstring.append(''.join([status_header, item, '/']))
118 118
119 def Execute(self, opt, args): 119 def Execute(self, opt, args):
120 all_projects = self.GetProjects(args) 120 all_projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
121 121
122 def _ProcessResults(_pool, _output, results): 122 def _ProcessResults(_pool, _output, results):
123 ret = 0 123 ret = 0
@@ -141,9 +141,10 @@ the following meanings:
141 if opt.orphans: 141 if opt.orphans:
142 proj_dirs = set() 142 proj_dirs = set()
143 proj_dirs_parents = set() 143 proj_dirs_parents = set()
144 for project in self.GetProjects(None, missing_ok=True): 144 for project in self.GetProjects(None, missing_ok=True, all_manifests=not opt.this_manifest_only):
145 proj_dirs.add(project.relpath) 145 relpath = project.RelPath(local=opt.this_manifest_only)
146 (head, _tail) = os.path.split(project.relpath) 146 proj_dirs.add(relpath)
147 (head, _tail) = os.path.split(relpath)
147 while head != "": 148 while head != "":
148 proj_dirs_parents.add(head) 149 proj_dirs_parents.add(head)
149 (head, _tail) = os.path.split(head) 150 (head, _tail) = os.path.split(head)
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 707c5bbd..f5584dc8 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -66,6 +66,7 @@ _ONE_DAY_S = 24 * 60 * 60
66class Sync(Command, MirrorSafeCommand): 66class Sync(Command, MirrorSafeCommand):
67 jobs = 1 67 jobs = 1
68 COMMON = True 68 COMMON = True
69 MULTI_MANIFEST_SUPPORT = False
69 helpSummary = "Update working tree to the latest revision" 70 helpSummary = "Update working tree to the latest revision"
70 helpUsage = """ 71 helpUsage = """
71%prog [<project>...] 72%prog [<project>...]
@@ -704,7 +705,7 @@ later is required to fix a server side protocol bug.
704 if project.relpath: 705 if project.relpath:
705 new_project_paths.append(project.relpath) 706 new_project_paths.append(project.relpath)
706 file_name = 'project.list' 707 file_name = 'project.list'
707 file_path = os.path.join(self.repodir, file_name) 708 file_path = os.path.join(self.manifest.subdir, file_name)
708 old_project_paths = [] 709 old_project_paths = []
709 710
710 if os.path.exists(file_path): 711 if os.path.exists(file_path):
@@ -760,7 +761,7 @@ later is required to fix a server side protocol bug.
760 } 761 }
761 762
762 copylinkfile_name = 'copy-link-files.json' 763 copylinkfile_name = 'copy-link-files.json'
763 copylinkfile_path = os.path.join(self.manifest.repodir, copylinkfile_name) 764 copylinkfile_path = os.path.join(self.manifest.subdir, copylinkfile_name)
764 old_copylinkfile_paths = {} 765 old_copylinkfile_paths = {}
765 766
766 if os.path.exists(copylinkfile_path): 767 if os.path.exists(copylinkfile_path):
@@ -932,6 +933,9 @@ later is required to fix a server side protocol bug.
932 if opt.prune is None: 933 if opt.prune is None:
933 opt.prune = True 934 opt.prune = True
934 935
936 if self.manifest.is_multimanifest and not opt.this_manifest_only and args:
937 self.OptionParser.error('partial syncs must use --this-manifest-only')
938
935 def Execute(self, opt, args): 939 def Execute(self, opt, args):
936 if opt.jobs: 940 if opt.jobs:
937 self.jobs = opt.jobs 941 self.jobs = opt.jobs
diff --git a/subcmds/upload.py b/subcmds/upload.py
index c48deab6..ef3d8e9d 100644
--- a/subcmds/upload.py
+++ b/subcmds/upload.py
@@ -226,7 +226,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
226 226
227 destination = opt.dest_branch or project.dest_branch or project.revisionExpr 227 destination = opt.dest_branch or project.dest_branch or project.revisionExpr
228 print('Upload project %s/ to remote branch %s%s:' % 228 print('Upload project %s/ to remote branch %s%s:' %
229 (project.relpath, destination, ' (private)' if opt.private else '')) 229 (project.RelPath(local=opt.this_manifest_only), destination,
230 ' (private)' if opt.private else ''))
230 print(' branch %s (%2d commit%s, %s):' % ( 231 print(' branch %s (%2d commit%s, %s):' % (
231 name, 232 name,
232 len(commit_list), 233 len(commit_list),
@@ -262,7 +263,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
262 script.append('# Uncomment the branches to upload:') 263 script.append('# Uncomment the branches to upload:')
263 for project, avail in pending: 264 for project, avail in pending:
264 script.append('#') 265 script.append('#')
265 script.append('# project %s/:' % project.relpath) 266 script.append('# project %s/:' % project.RelPath(local=opt.this_manifest_only))
266 267
267 b = {} 268 b = {}
268 for branch in avail: 269 for branch in avail:
@@ -285,7 +286,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
285 script.append('# %s' % commit) 286 script.append('# %s' % commit)
286 b[name] = branch 287 b[name] = branch
287 288
288 projects[project.relpath] = project 289 projects[project.RelPath(local=opt.this_manifest_only)] = project
289 branches[project.name] = b 290 branches[project.name] = b
290 script.append('') 291 script.append('')
291 292
@@ -313,7 +314,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
313 _die('project for branch %s not in script', name) 314 _die('project for branch %s not in script', name)
314 branch = branches[project.name].get(name) 315 branch = branches[project.name].get(name)
315 if not branch: 316 if not branch:
316 _die('branch %s not in %s', name, project.relpath) 317 _die('branch %s not in %s', name, project.RelPath(local=opt.this_manifest_only))
317 todo.append(branch) 318 todo.append(branch)
318 if not todo: 319 if not todo:
319 _die("nothing uncommented for upload") 320 _die("nothing uncommented for upload")
@@ -481,7 +482,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
481 else: 482 else:
482 fmt = '\n (%s)' 483 fmt = '\n (%s)'
483 print(('[FAILED] %-15s %-15s' + fmt) % ( 484 print(('[FAILED] %-15s %-15s' + fmt) % (
484 branch.project.relpath + '/', 485 branch.project.RelPath(local=opt.this_manifest_only) + '/',
485 branch.name, 486 branch.name,
486 str(branch.error)), 487 str(branch.error)),
487 file=sys.stderr) 488 file=sys.stderr)
@@ -490,7 +491,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
490 for branch in todo: 491 for branch in todo:
491 if branch.uploaded: 492 if branch.uploaded:
492 print('[OK ] %-15s %s' % ( 493 print('[OK ] %-15s %s' % (
493 branch.project.relpath + '/', 494 branch.project.RelPath(local=opt.this_manifest_only) + '/',
494 branch.name), 495 branch.name),
495 file=sys.stderr) 496 file=sys.stderr)
496 497
@@ -524,7 +525,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
524 return (project, avail) 525 return (project, avail)
525 526
526 def Execute(self, opt, args): 527 def Execute(self, opt, args):
527 projects = self.GetProjects(args) 528 projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
528 529
529 def _ProcessResults(_pool, _out, results): 530 def _ProcessResults(_pool, _out, results):
530 pending = [] 531 pending = []
@@ -534,7 +535,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
534 print('repo: error: %s: Unable to upload branch "%s". ' 535 print('repo: error: %s: Unable to upload branch "%s". '
535 'You might be able to fix the branch by running:\n' 536 'You might be able to fix the branch by running:\n'
536 ' git branch --set-upstream-to m/%s' % 537 ' git branch --set-upstream-to m/%s' %
537 (project.relpath, project.CurrentBranch, self.manifest.branch), 538 (project.RelPath(local=opt.this_manifest_only), project.CurrentBranch,
539 project.manifest.branch),
538 file=sys.stderr) 540 file=sys.stderr)
539 elif avail: 541 elif avail:
540 pending.append(result) 542 pending.append(result)
@@ -554,15 +556,23 @@ Gerrit Code Review: https://www.gerritcodereview.com/
554 (opt.branch,), file=sys.stderr) 556 (opt.branch,), file=sys.stderr)
555 return 1 557 return 1
556 558
557 pending_proj_names = [project.name for (project, available) in pending] 559 manifests = {project.manifest.topdir: project.manifest
558 pending_worktrees = [project.worktree for (project, available) in pending] 560 for (project, available) in pending}
559 hook = RepoHook.FromSubcmd( 561 ret = 0
560 hook_type='pre-upload', manifest=self.manifest, 562 for manifest in manifests.values():
561 opt=opt, abort_if_user_denies=True) 563 pending_proj_names = [project.name for (project, available) in pending
562 if not hook.Run( 564 if project.manifest.topdir == manifest.topdir]
563 project_list=pending_proj_names, 565 pending_worktrees = [project.worktree for (project, available) in pending
564 worktree_list=pending_worktrees): 566 if project.manifest.topdir == manifest.topdir]
565 return 1 567 hook = RepoHook.FromSubcmd(
568 hook_type='pre-upload', manifest=manifest,
569 opt=opt, abort_if_user_denies=True)
570 if not hook.Run(
571 project_list=pending_proj_names,
572 worktree_list=pending_worktrees):
573 ret = 1
574 if ret:
575 return ret
566 576
567 reviewers = _SplitEmails(opt.reviewers) if opt.reviewers else [] 577 reviewers = _SplitEmails(opt.reviewers) if opt.reviewers else []
568 cc = _SplitEmails(opt.cc) if opt.cc else [] 578 cc = _SplitEmails(opt.cc) if opt.cc else []