blob: 7c68ebc5f725fc82e6345c996a41b19b76dffc0b [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Mike Frysingerb5d075d2021-03-01 00:56:38 -050015import multiprocessing
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070016import os
17import optparse
Colin Cross5acde752012-03-28 20:15:45 -070018import re
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070019import sys
20
David Rileye0684ad2017-04-05 00:02:59 -070021from event_log import EventLog
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070022from error import NoSuchProjectError
Colin Cross5acde752012-03-28 20:15:45 -070023from error import InvalidProjectGroupsError
Mike Frysingerb5d075d2021-03-01 00:56:38 -050024import progress
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070025
David Pursehouseb148ac92012-11-16 09:33:39 +090026
Mike Frysingerdf8b1cb2021-07-26 15:59:20 -040027# Are we generating man-pages?
28GENERATE_MANPAGES = os.environ.get('_REPO_GENERATE_MANPAGES_') == ' indeed! '
29
30
Mike Frysinger7c871162021-02-16 01:45:39 -050031# Number of projects to submit to a single worker process at a time.
32# This number represents a tradeoff between the overhead of IPC and finer
33# grained opportunity for parallelism. This particular value was chosen by
34# iterating through powers of two until the overall performance no longer
35# improved. The performance of this batch size is not a function of the
36# number of cores on the system.
37WORKER_BATCH_SIZE = 32
38
39
Mike Frysinger6a2400a2021-02-16 01:43:31 -050040# How many jobs to run in parallel by default? This assumes the jobs are
41# largely I/O bound and do not hit the network.
42DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8)
43
44
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070045class Command(object):
46 """Base class for any command line action in repo.
47 """
48
Mike Frysingerd88b3692021-06-14 16:09:29 -040049 # Singleton for all commands to track overall repo command execution and
50 # provide event summary to callers. Only used by sync subcommand currently.
51 #
52 # NB: This is being replaced by git trace2 events. See git_trace2_event_log.
53 event_log = EventLog()
54
Mike Frysinger4f210542021-06-14 16:05:19 -040055 # Whether this command is a "common" one, i.e. whether the user would commonly
56 # use it or it's a more uncommon command. This is used by the help command to
57 # show short-vs-full summaries.
58 COMMON = False
59
Mike Frysinger6a2400a2021-02-16 01:43:31 -050060 # Whether this command supports running in parallel. If greater than 0,
61 # it is the number of parallel jobs to default to.
62 PARALLEL_JOBS = None
63
LaMont Jonescc879a92021-11-18 22:40:18 +000064 # 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
Raman Tenneti784e16f2021-06-11 17:29:45 -070070 def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None,
LaMont Jonescc879a92021-11-18 22:40:18 +000071 git_event_log=None, outer_client=None, outer_manifest=None):
Mike Frysingerd58d0dd2021-06-14 16:17:27 -040072 self.repodir = repodir
73 self.client = client
LaMont Jonescc879a92021-11-18 22:40:18 +000074 self.outer_client = outer_client or client
Mike Frysingerd58d0dd2021-06-14 16:17:27 -040075 self.manifest = manifest
76 self.gitc_manifest = gitc_manifest
Raman Tenneti784e16f2021-06-11 17:29:45 -070077 self.git_event_log = git_event_log
LaMont Jonescc879a92021-11-18 22:40:18 +000078 self.outer_manifest = outer_manifest
Mike Frysingerd58d0dd2021-06-14 16:17:27 -040079
80 # Cache for the OptionParser property.
81 self._optparse = None
82
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -070083 def WantPager(self, _opt):
Shawn O. Pearcedb45da12009-04-18 13:49:13 -070084 return False
85
David Pursehouseb148ac92012-11-16 09:33:39 +090086 def ReadEnvironmentOptions(self, opts):
87 """ Set options from environment variables. """
88
89 env_options = self._RegisteredEnvironmentOptions()
90
91 for env_key, opt_key in env_options.items():
92 # Get the user-set option value if any
93 opt_value = getattr(opts, opt_key)
94
95 # If the value is set, it means the user has passed it as a command
96 # line option, and we should use that. Otherwise we can try to set it
97 # with the value from the corresponding environment variable.
98 if opt_value is not None:
99 continue
100
101 env_value = os.environ.get(env_key)
102 if env_value is not None:
103 setattr(opts, opt_key, env_value)
104
105 return opts
106
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700107 @property
108 def OptionParser(self):
109 if self._optparse is None:
110 try:
111 me = 'repo %s' % self.NAME
112 usage = self.helpUsage.strip().replace('%prog', me)
113 except AttributeError:
114 usage = 'repo %s' % self.NAME
Mike Frysinger72ebf192020-02-19 01:20:18 -0500115 epilog = 'Run `repo help %s` to view the detailed manual.' % self.NAME
116 self._optparse = optparse.OptionParser(usage=usage, epilog=epilog)
Mike Frysinger9180a072021-04-13 14:57:40 -0400117 self._CommonOptions(self._optparse)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700118 self._Options(self._optparse)
119 return self._optparse
120
Mike Frysinger9180a072021-04-13 14:57:40 -0400121 def _CommonOptions(self, p, opt_v=True):
122 """Initialize the option parser with common options.
123
124 These will show up for *all* subcommands, so use sparingly.
125 NB: Keep in sync with repo:InitParser().
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700126 """
Mike Frysinger9180a072021-04-13 14:57:40 -0400127 g = p.add_option_group('Logging options')
128 opts = ['-v'] if opt_v else []
129 g.add_option(*opts, '--verbose',
130 dest='output_mode', action='store_true',
131 help='show all output')
132 g.add_option('-q', '--quiet',
133 dest='output_mode', action='store_false',
134 help='only show errors')
135
Mike Frysinger6a2400a2021-02-16 01:43:31 -0500136 if self.PARALLEL_JOBS is not None:
Mike Frysingerdf8b1cb2021-07-26 15:59:20 -0400137 default = 'based on number of CPU cores'
138 if not GENERATE_MANPAGES:
139 # Only include active cpu count if we aren't generating man pages.
140 default = f'%default; {default}'
Mike Frysinger6a2400a2021-02-16 01:43:31 -0500141 p.add_option(
142 '-j', '--jobs',
143 type=int, default=self.PARALLEL_JOBS,
Mike Frysingerdf8b1cb2021-07-26 15:59:20 -0400144 help=f'number of jobs to run in parallel (default: {default})')
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700145
LaMont Jonescc879a92021-11-18 22:40:18 +0000146 m = p.add_option_group('Multi-manifest options')
LaMont Jonesbdcba7d2022-04-11 22:50:11 +0000147 m.add_option('--outer-manifest', action='store_true', default=None,
LaMont Jonescc879a92021-11-18 22:40:18 +0000148 help='operate starting at the outermost manifest')
149 m.add_option('--no-outer-manifest', dest='outer_manifest',
LaMont Jonesbdcba7d2022-04-11 22:50:11 +0000150 action='store_false', help='do not operate on outer manifests')
LaMont Jonescc879a92021-11-18 22:40:18 +0000151 m.add_option('--this-manifest-only', action='store_true', default=None,
152 help='only operate on this (sub)manifest')
153 m.add_option('--no-this-manifest-only', '--all-manifests',
154 dest='this_manifest_only', action='store_false',
155 help='operate on this manifest and its submanifests')
156
Mike Frysinger9180a072021-04-13 14:57:40 -0400157 def _Options(self, p):
158 """Initialize the option parser with subcommand-specific options."""
159
David Pursehouseb148ac92012-11-16 09:33:39 +0900160 def _RegisteredEnvironmentOptions(self):
161 """Get options that can be set from environment variables.
162
163 Return a dictionary mapping environment variable name
164 to option key name that it can override.
165
166 Example: {'REPO_MY_OPTION': 'my_option'}
167
168 Will allow the option with key value 'my_option' to be set
169 from the value in the environment variable named 'REPO_MY_OPTION'.
170
171 Note: This does not work properly for options that are explicitly
172 set to None by the user, or options that are defined with a
173 default value other than None.
174
175 """
176 return {}
177
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700178 def Usage(self):
179 """Display usage and terminate.
180 """
181 self.OptionParser.print_usage()
182 sys.exit(1)
183
Mike Frysinger9180a072021-04-13 14:57:40 -0400184 def CommonValidateOptions(self, opt, args):
185 """Validate common options."""
186 opt.quiet = opt.output_mode is False
187 opt.verbose = opt.output_mode is True
LaMont Jonesbdcba7d2022-04-11 22:50:11 +0000188 if opt.outer_manifest is None:
189 # By default, treat multi-manifest instances as a single manifest from
190 # the user's perspective.
191 opt.outer_manifest = True
Mike Frysinger9180a072021-04-13 14:57:40 -0400192
Mike Frysingerae6cb082019-08-27 01:10:59 -0400193 def ValidateOptions(self, opt, args):
194 """Validate the user options & arguments before executing.
195
196 This is meant to help break the code up into logical steps. Some tips:
197 * Use self.OptionParser.error to display CLI related errors.
198 * Adjust opt member defaults as makes sense.
199 * Adjust the args list, but do so inplace so the caller sees updates.
200 * Try to avoid updating self state. Leave that to Execute.
201 """
202
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700203 def Execute(self, opt, args):
204 """Perform the action, after option parsing is complete.
205 """
206 raise NotImplementedError
Conley Owens971de8e2012-04-16 10:36:08 -0700207
Mike Frysingerb5d075d2021-03-01 00:56:38 -0500208 @staticmethod
209 def ExecuteInParallel(jobs, func, inputs, callback, output=None, ordered=False):
210 """Helper for managing parallel execution boiler plate.
211
212 For subcommands that can easily split their work up.
213
214 Args:
215 jobs: How many parallel processes to use.
216 func: The function to apply to each of the |inputs|. Usually a
217 functools.partial for wrapping additional arguments. It will be run
218 in a separate process, so it must be pickalable, so nested functions
219 won't work. Methods on the subcommand Command class should work.
220 inputs: The list of items to process. Must be a list.
221 callback: The function to pass the results to for processing. It will be
222 executed in the main thread and process the results of |func| as they
223 become available. Thus it may be a local nested function. Its return
224 value is passed back directly. It takes three arguments:
225 - The processing pool (or None with one job).
226 - The |output| argument.
227 - An iterator for the results.
228 output: An output manager. May be progress.Progess or color.Coloring.
229 ordered: Whether the jobs should be processed in order.
230
231 Returns:
232 The |callback| function's results are returned.
233 """
234 try:
235 # NB: Multiprocessing is heavy, so don't spin it up for one job.
236 if len(inputs) == 1 or jobs == 1:
237 return callback(None, output, (func(x) for x in inputs))
238 else:
239 with multiprocessing.Pool(jobs) as pool:
240 submit = pool.imap if ordered else pool.imap_unordered
241 return callback(pool, output, submit(func, inputs, chunksize=WORKER_BATCH_SIZE))
242 finally:
243 if isinstance(output, progress.Progress):
244 output.end()
245
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800246 def _ResetPathToProjectMap(self, projects):
247 self._by_path = dict((p.worktree, p) for p in projects)
248
249 def _UpdatePathToProjectMap(self, project):
250 self._by_path[project.worktree] = project
251
Simran Basib9a1b732015-08-20 12:19:28 -0700252 def _GetProjectByPath(self, manifest, path):
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800253 project = None
254 if os.path.exists(path):
255 oldpath = None
David Pursehouse5a2517f2020-02-12 14:55:01 +0900256 while (path and
257 path != oldpath and
258 path != manifest.topdir):
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800259 try:
260 project = self._by_path[path]
261 break
262 except KeyError:
263 oldpath = path
264 path = os.path.dirname(path)
Mark E. Hamiltonf9fe3e12016-02-23 18:10:42 -0700265 if not project and path == manifest.topdir:
266 try:
267 project = self._by_path[path]
268 except KeyError:
269 pass
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800270 else:
271 try:
272 project = self._by_path[path]
273 except KeyError:
274 pass
275 return project
276
Simran Basib9a1b732015-08-20 12:19:28 -0700277 def GetProjects(self, args, manifest=None, groups='', missing_ok=False,
LaMont Jonescc879a92021-11-18 22:40:18 +0000278 submodules_ok=False, all_manifests=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700279 """A list of projects that match the arguments.
LaMont Jonesff6b1da2022-06-01 21:03:34 +0000280
281 Args:
282 args: a list of (case-insensitive) strings, projects to search for.
283 manifest: an XmlManifest, the manifest to use, or None for default.
284 groups: a string, the manifest groups in use.
285 missing_ok: a boolean, whether to allow missing projects.
286 submodules_ok: a boolean, whether to allow submodules.
287 all_manifests: a boolean, if True then all manifests and submanifests are
288 used. If False, then only the local (sub)manifest is used.
289
290 Returns:
291 A list of matching Project instances.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700292 """
LaMont Jonescc879a92021-11-18 22:40:18 +0000293 if all_manifests:
294 if not manifest:
295 manifest = self.manifest.outer_client
296 all_projects_list = manifest.all_projects
297 else:
298 if not manifest:
299 manifest = self.manifest
300 all_projects_list = manifest.projects
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700301 result = []
302
Graham Christensen0369a062015-07-29 17:02:54 -0500303 if not groups:
Raman Tenneti080877e2021-03-09 15:19:06 -0800304 groups = manifest.GetGroupsStr()
David Pursehouse1d947b32012-10-25 12:23:11 +0900305 groups = [x for x in re.split(r'[,\s]+', groups) if x]
Colin Cross5acde752012-03-28 20:15:45 -0700306
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700307 if not args:
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800308 derived_projects = {}
309 for project in all_projects_list:
310 if submodules_ok or project.sync_s:
311 derived_projects.update((p.name, p)
312 for p in project.GetDerivedSubprojects())
313 all_projects_list.extend(derived_projects.values())
314 for project in all_projects_list:
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700315 if (missing_ok or project.Exists) and project.MatchesGroups(groups):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700316 result.append(project)
317 else:
David James8d201162013-10-11 17:03:19 -0700318 self._ResetPathToProjectMap(all_projects_list)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700319
320 for arg in args:
Mike Frysingere778e572019-10-04 14:21:41 -0400321 # We have to filter by manifest groups in case the requested project is
322 # checked out multiple times or differently based on them.
LaMont Jonescc879a92021-11-18 22:40:18 +0000323 projects = [project for project in manifest.GetProjectsWithName(
324 arg, all_manifests=all_manifests)
Mike Frysingere778e572019-10-04 14:21:41 -0400325 if project.MatchesGroups(groups)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700326
David James8d201162013-10-11 17:03:19 -0700327 if not projects:
Anthony Newnamdf14a702011-01-09 17:31:57 -0800328 path = os.path.abspath(arg).replace('\\', '/')
LaMont Jonescc879a92021-11-18 22:40:18 +0000329 tree = manifest
330 if all_manifests:
331 # Look for the deepest matching submanifest.
332 for tree in reversed(list(manifest.all_manifests)):
333 if path.startswith(tree.topdir):
334 break
335 project = self._GetProjectByPath(tree, path)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700336
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800337 # If it's not a derived project, update path->project mapping and
338 # search again, as arg might actually point to a derived subproject.
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700339 if (project and not project.Derived and (submodules_ok or
340 project.sync_s)):
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800341 search_again = False
342 for subproject in project.GetDerivedSubprojects():
343 self._UpdatePathToProjectMap(subproject)
344 search_again = True
345 if search_again:
Simran Basib9a1b732015-08-20 12:19:28 -0700346 project = self._GetProjectByPath(manifest, path) or project
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700347
David James8d201162013-10-11 17:03:19 -0700348 if project:
349 projects = [project]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700350
David James8d201162013-10-11 17:03:19 -0700351 if not projects:
352 raise NoSuchProjectError(arg)
353
354 for project in projects:
355 if not missing_ok and not project.Exists:
LaMont Jonescc879a92021-11-18 22:40:18 +0000356 raise NoSuchProjectError('%s (%s)' % (
357 arg, project.RelPath(local=not all_manifests)))
David James8d201162013-10-11 17:03:19 -0700358 if not project.MatchesGroups(groups):
359 raise InvalidProjectGroupsError(arg)
360
361 result.extend(projects)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700362
363 def _getpath(x):
364 return x.relpath
365 result.sort(key=_getpath)
366 return result
367
LaMont Jonescc879a92021-11-18 22:40:18 +0000368 def FindProjects(self, args, inverse=False, all_manifests=False):
369 """Find projects from command line arguments.
370
371 Args:
372 args: a list of (case-insensitive) strings, projects to search for.
373 inverse: a boolean, if True, then projects not matching any |args| are
374 returned.
375 all_manifests: a boolean, if True then all manifests and submanifests are
376 used. If False, then only the local (sub)manifest is used.
377 """
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800378 result = []
David Pursehouse84c4d3c2013-04-30 10:57:37 +0900379 patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args]
LaMont Jonescc879a92021-11-18 22:40:18 +0000380 for project in self.GetProjects('', all_manifests=all_manifests):
381 paths = [project.name, project.RelPath(local=not all_manifests)]
David Pursehouse84c4d3c2013-04-30 10:57:37 +0900382 for pattern in patterns:
LaMont Jonescc879a92021-11-18 22:40:18 +0000383 match = any(pattern.search(x) for x in paths)
Takeshi Kanemoto1f056442016-01-26 14:11:35 +0900384 if not inverse and match:
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800385 result.append(project)
386 break
Takeshi Kanemoto1f056442016-01-26 14:11:35 +0900387 if inverse and match:
388 break
389 else:
390 if inverse:
391 result.append(project)
LaMont Jonescc879a92021-11-18 22:40:18 +0000392 result.sort(key=lambda project: (project.manifest.path_prefix,
393 project.relpath))
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800394 return result
395
LaMont Jonescc879a92021-11-18 22:40:18 +0000396 def ManifestList(self, opt):
397 """Yields all of the manifests to traverse.
398
399 Args:
400 opt: The command options.
401 """
402 top = self.outer_manifest
LaMont Jonesbdcba7d2022-04-11 22:50:11 +0000403 if not opt.outer_manifest or opt.this_manifest_only:
LaMont Jonescc879a92021-11-18 22:40:18 +0000404 top = self.manifest
405 yield top
406 if not opt.this_manifest_only:
407 for child in top.all_children:
408 yield child
409
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700410
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700411class InteractiveCommand(Command):
412 """Command which requires user interaction on the tty and
413 must not run within a pager, even if the user asks to.
414 """
David Pursehouse819827a2020-02-12 15:20:19 +0900415
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700416 def WantPager(self, _opt):
Shawn O. Pearcedb45da12009-04-18 13:49:13 -0700417 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700418
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700419
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700420class PagedCommand(Command):
421 """Command which defaults to output in a pager, as its
422 display tends to be larger than one screen full.
423 """
David Pursehouse819827a2020-02-12 15:20:19 +0900424
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700425 def WantPager(self, _opt):
Shawn O. Pearcedb45da12009-04-18 13:49:13 -0700426 return True
Shawn O. Pearcec95583b2009-03-03 17:47:06 -0800427
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700428
Shawn O. Pearcec95583b2009-03-03 17:47:06 -0800429class MirrorSafeCommand(object):
430 """Command permits itself to run within a mirror,
431 and does not require a working directory.
432 """
Dan Willemsen9ff2ece2015-08-31 15:45:06 -0700433
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700434
Dan Willemsen79360642015-08-31 15:45:06 -0700435class GitcAvailableCommand(object):
Dan Willemsen9ff2ece2015-08-31 15:45:06 -0700436 """Command that requires GITC to be available, but does
437 not require the local client to be a GITC client.
438 """
Dan Willemsen79360642015-08-31 15:45:06 -0700439
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700440
Dan Willemsen79360642015-08-31 15:45:06 -0700441class GitcClientCommand(object):
442 """Command that requires the local client to be a GITC
443 client.
444 """