blob: dc765db00fe946ad1413bc2affb6718ccd21c3a2 [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
Conley Owensd21720d2012-04-16 11:02:21 -070018import platform
Colin Cross5acde752012-03-28 20:15:45 -070019import re
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070020import sys
21
David Rileye0684ad2017-04-05 00:02:59 -070022from event_log import EventLog
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070023from error import NoSuchProjectError
Colin Cross5acde752012-03-28 20:15:45 -070024from error import InvalidProjectGroupsError
Mike Frysingerb5d075d2021-03-01 00:56:38 -050025import progress
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070026
David Pursehouseb148ac92012-11-16 09:33:39 +090027
Mike Frysinger7c871162021-02-16 01:45:39 -050028# Number of projects to submit to a single worker process at a time.
29# This number represents a tradeoff between the overhead of IPC and finer
30# grained opportunity for parallelism. This particular value was chosen by
31# iterating through powers of two until the overall performance no longer
32# improved. The performance of this batch size is not a function of the
33# number of cores on the system.
34WORKER_BATCH_SIZE = 32
35
36
Mike Frysinger6a2400a2021-02-16 01:43:31 -050037# How many jobs to run in parallel by default? This assumes the jobs are
38# largely I/O bound and do not hit the network.
39DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8)
40
41
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070042class Command(object):
43 """Base class for any command line action in repo.
44 """
45
Mike Frysingerd88b3692021-06-14 16:09:29 -040046 # Singleton for all commands to track overall repo command execution and
47 # provide event summary to callers. Only used by sync subcommand currently.
48 #
49 # NB: This is being replaced by git trace2 events. See git_trace2_event_log.
50 event_log = EventLog()
51
Mike Frysinger4f210542021-06-14 16:05:19 -040052 # Whether this command is a "common" one, i.e. whether the user would commonly
53 # use it or it's a more uncommon command. This is used by the help command to
54 # show short-vs-full summaries.
55 COMMON = False
56
Mike Frysinger6a2400a2021-02-16 01:43:31 -050057 # Whether this command supports running in parallel. If greater than 0,
58 # it is the number of parallel jobs to default to.
59 PARALLEL_JOBS = None
60
Mike Frysingerd58d0dd2021-06-14 16:17:27 -040061 def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None):
62 self.repodir = repodir
63 self.client = client
64 self.manifest = manifest
65 self.gitc_manifest = gitc_manifest
66
67 # Cache for the OptionParser property.
68 self._optparse = None
69
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -070070 def WantPager(self, _opt):
Shawn O. Pearcedb45da12009-04-18 13:49:13 -070071 return False
72
David Pursehouseb148ac92012-11-16 09:33:39 +090073 def ReadEnvironmentOptions(self, opts):
74 """ Set options from environment variables. """
75
76 env_options = self._RegisteredEnvironmentOptions()
77
78 for env_key, opt_key in env_options.items():
79 # Get the user-set option value if any
80 opt_value = getattr(opts, opt_key)
81
82 # If the value is set, it means the user has passed it as a command
83 # line option, and we should use that. Otherwise we can try to set it
84 # with the value from the corresponding environment variable.
85 if opt_value is not None:
86 continue
87
88 env_value = os.environ.get(env_key)
89 if env_value is not None:
90 setattr(opts, opt_key, env_value)
91
92 return opts
93
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070094 @property
95 def OptionParser(self):
96 if self._optparse is None:
97 try:
98 me = 'repo %s' % self.NAME
99 usage = self.helpUsage.strip().replace('%prog', me)
100 except AttributeError:
101 usage = 'repo %s' % self.NAME
Mike Frysinger72ebf192020-02-19 01:20:18 -0500102 epilog = 'Run `repo help %s` to view the detailed manual.' % self.NAME
103 self._optparse = optparse.OptionParser(usage=usage, epilog=epilog)
Mike Frysinger9180a072021-04-13 14:57:40 -0400104 self._CommonOptions(self._optparse)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700105 self._Options(self._optparse)
106 return self._optparse
107
Mike Frysinger9180a072021-04-13 14:57:40 -0400108 def _CommonOptions(self, p, opt_v=True):
109 """Initialize the option parser with common options.
110
111 These will show up for *all* subcommands, so use sparingly.
112 NB: Keep in sync with repo:InitParser().
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700113 """
Mike Frysinger9180a072021-04-13 14:57:40 -0400114 g = p.add_option_group('Logging options')
115 opts = ['-v'] if opt_v else []
116 g.add_option(*opts, '--verbose',
117 dest='output_mode', action='store_true',
118 help='show all output')
119 g.add_option('-q', '--quiet',
120 dest='output_mode', action='store_false',
121 help='only show errors')
122
Mike Frysinger6a2400a2021-02-16 01:43:31 -0500123 if self.PARALLEL_JOBS is not None:
124 p.add_option(
125 '-j', '--jobs',
126 type=int, default=self.PARALLEL_JOBS,
127 help='number of jobs to run in parallel (default: %s)' % self.PARALLEL_JOBS)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700128
Mike Frysinger9180a072021-04-13 14:57:40 -0400129 def _Options(self, p):
130 """Initialize the option parser with subcommand-specific options."""
131
David Pursehouseb148ac92012-11-16 09:33:39 +0900132 def _RegisteredEnvironmentOptions(self):
133 """Get options that can be set from environment variables.
134
135 Return a dictionary mapping environment variable name
136 to option key name that it can override.
137
138 Example: {'REPO_MY_OPTION': 'my_option'}
139
140 Will allow the option with key value 'my_option' to be set
141 from the value in the environment variable named 'REPO_MY_OPTION'.
142
143 Note: This does not work properly for options that are explicitly
144 set to None by the user, or options that are defined with a
145 default value other than None.
146
147 """
148 return {}
149
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700150 def Usage(self):
151 """Display usage and terminate.
152 """
153 self.OptionParser.print_usage()
154 sys.exit(1)
155
Mike Frysinger9180a072021-04-13 14:57:40 -0400156 def CommonValidateOptions(self, opt, args):
157 """Validate common options."""
158 opt.quiet = opt.output_mode is False
159 opt.verbose = opt.output_mode is True
160
Mike Frysingerae6cb082019-08-27 01:10:59 -0400161 def ValidateOptions(self, opt, args):
162 """Validate the user options & arguments before executing.
163
164 This is meant to help break the code up into logical steps. Some tips:
165 * Use self.OptionParser.error to display CLI related errors.
166 * Adjust opt member defaults as makes sense.
167 * Adjust the args list, but do so inplace so the caller sees updates.
168 * Try to avoid updating self state. Leave that to Execute.
169 """
170
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700171 def Execute(self, opt, args):
172 """Perform the action, after option parsing is complete.
173 """
174 raise NotImplementedError
Conley Owens971de8e2012-04-16 10:36:08 -0700175
Mike Frysingerb5d075d2021-03-01 00:56:38 -0500176 @staticmethod
177 def ExecuteInParallel(jobs, func, inputs, callback, output=None, ordered=False):
178 """Helper for managing parallel execution boiler plate.
179
180 For subcommands that can easily split their work up.
181
182 Args:
183 jobs: How many parallel processes to use.
184 func: The function to apply to each of the |inputs|. Usually a
185 functools.partial for wrapping additional arguments. It will be run
186 in a separate process, so it must be pickalable, so nested functions
187 won't work. Methods on the subcommand Command class should work.
188 inputs: The list of items to process. Must be a list.
189 callback: The function to pass the results to for processing. It will be
190 executed in the main thread and process the results of |func| as they
191 become available. Thus it may be a local nested function. Its return
192 value is passed back directly. It takes three arguments:
193 - The processing pool (or None with one job).
194 - The |output| argument.
195 - An iterator for the results.
196 output: An output manager. May be progress.Progess or color.Coloring.
197 ordered: Whether the jobs should be processed in order.
198
199 Returns:
200 The |callback| function's results are returned.
201 """
202 try:
203 # NB: Multiprocessing is heavy, so don't spin it up for one job.
204 if len(inputs) == 1 or jobs == 1:
205 return callback(None, output, (func(x) for x in inputs))
206 else:
207 with multiprocessing.Pool(jobs) as pool:
208 submit = pool.imap if ordered else pool.imap_unordered
209 return callback(pool, output, submit(func, inputs, chunksize=WORKER_BATCH_SIZE))
210 finally:
211 if isinstance(output, progress.Progress):
212 output.end()
213
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800214 def _ResetPathToProjectMap(self, projects):
215 self._by_path = dict((p.worktree, p) for p in projects)
216
217 def _UpdatePathToProjectMap(self, project):
218 self._by_path[project.worktree] = project
219
Simran Basib9a1b732015-08-20 12:19:28 -0700220 def _GetProjectByPath(self, manifest, path):
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800221 project = None
222 if os.path.exists(path):
223 oldpath = None
David Pursehouse5a2517f2020-02-12 14:55:01 +0900224 while (path and
225 path != oldpath and
226 path != manifest.topdir):
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800227 try:
228 project = self._by_path[path]
229 break
230 except KeyError:
231 oldpath = path
232 path = os.path.dirname(path)
Mark E. Hamiltonf9fe3e12016-02-23 18:10:42 -0700233 if not project and path == manifest.topdir:
234 try:
235 project = self._by_path[path]
236 except KeyError:
237 pass
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800238 else:
239 try:
240 project = self._by_path[path]
241 except KeyError:
242 pass
243 return project
244
Simran Basib9a1b732015-08-20 12:19:28 -0700245 def GetProjects(self, args, manifest=None, groups='', missing_ok=False,
246 submodules_ok=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700247 """A list of projects that match the arguments.
248 """
Simran Basib9a1b732015-08-20 12:19:28 -0700249 if not manifest:
250 manifest = self.manifest
251 all_projects_list = manifest.projects
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700252 result = []
253
Simran Basib9a1b732015-08-20 12:19:28 -0700254 mp = manifest.manifestProject
Colin Cross5acde752012-03-28 20:15:45 -0700255
Graham Christensen0369a062015-07-29 17:02:54 -0500256 if not groups:
Raman Tenneti080877e2021-03-09 15:19:06 -0800257 groups = manifest.GetGroupsStr()
David Pursehouse1d947b32012-10-25 12:23:11 +0900258 groups = [x for x in re.split(r'[,\s]+', groups) if x]
Colin Cross5acde752012-03-28 20:15:45 -0700259
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700260 if not args:
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800261 derived_projects = {}
262 for project in all_projects_list:
263 if submodules_ok or project.sync_s:
264 derived_projects.update((p.name, p)
265 for p in project.GetDerivedSubprojects())
266 all_projects_list.extend(derived_projects.values())
267 for project in all_projects_list:
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700268 if (missing_ok or project.Exists) and project.MatchesGroups(groups):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700269 result.append(project)
270 else:
David James8d201162013-10-11 17:03:19 -0700271 self._ResetPathToProjectMap(all_projects_list)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700272
273 for arg in args:
Mike Frysingere778e572019-10-04 14:21:41 -0400274 # We have to filter by manifest groups in case the requested project is
275 # checked out multiple times or differently based on them.
276 projects = [project for project in manifest.GetProjectsWithName(arg)
277 if project.MatchesGroups(groups)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700278
David James8d201162013-10-11 17:03:19 -0700279 if not projects:
Anthony Newnamdf14a702011-01-09 17:31:57 -0800280 path = os.path.abspath(arg).replace('\\', '/')
Simran Basib9a1b732015-08-20 12:19:28 -0700281 project = self._GetProjectByPath(manifest, path)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700282
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800283 # If it's not a derived project, update path->project mapping and
284 # search again, as arg might actually point to a derived subproject.
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700285 if (project and not project.Derived and (submodules_ok or
286 project.sync_s)):
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800287 search_again = False
288 for subproject in project.GetDerivedSubprojects():
289 self._UpdatePathToProjectMap(subproject)
290 search_again = True
291 if search_again:
Simran Basib9a1b732015-08-20 12:19:28 -0700292 project = self._GetProjectByPath(manifest, path) or project
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700293
David James8d201162013-10-11 17:03:19 -0700294 if project:
295 projects = [project]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700296
David James8d201162013-10-11 17:03:19 -0700297 if not projects:
298 raise NoSuchProjectError(arg)
299
300 for project in projects:
301 if not missing_ok and not project.Exists:
Mike Frysingere778e572019-10-04 14:21:41 -0400302 raise NoSuchProjectError('%s (%s)' % (arg, project.relpath))
David James8d201162013-10-11 17:03:19 -0700303 if not project.MatchesGroups(groups):
304 raise InvalidProjectGroupsError(arg)
305
306 result.extend(projects)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700307
308 def _getpath(x):
309 return x.relpath
310 result.sort(key=_getpath)
311 return result
312
Takeshi Kanemoto1f056442016-01-26 14:11:35 +0900313 def FindProjects(self, args, inverse=False):
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800314 result = []
David Pursehouse84c4d3c2013-04-30 10:57:37 +0900315 patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args]
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800316 for project in self.GetProjects(''):
David Pursehouse84c4d3c2013-04-30 10:57:37 +0900317 for pattern in patterns:
Takeshi Kanemoto1f056442016-01-26 14:11:35 +0900318 match = pattern.search(project.name) or pattern.search(project.relpath)
319 if not inverse and match:
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800320 result.append(project)
321 break
Takeshi Kanemoto1f056442016-01-26 14:11:35 +0900322 if inverse and match:
323 break
324 else:
325 if inverse:
326 result.append(project)
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800327 result.sort(key=lambda project: project.relpath)
328 return result
329
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700330
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700331class InteractiveCommand(Command):
332 """Command which requires user interaction on the tty and
333 must not run within a pager, even if the user asks to.
334 """
David Pursehouse819827a2020-02-12 15:20:19 +0900335
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700336 def WantPager(self, _opt):
Shawn O. Pearcedb45da12009-04-18 13:49:13 -0700337 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700338
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700339
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700340class PagedCommand(Command):
341 """Command which defaults to output in a pager, as its
342 display tends to be larger than one screen full.
343 """
David Pursehouse819827a2020-02-12 15:20:19 +0900344
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700345 def WantPager(self, _opt):
Shawn O. Pearcedb45da12009-04-18 13:49:13 -0700346 return True
Shawn O. Pearcec95583b2009-03-03 17:47:06 -0800347
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700348
Shawn O. Pearcec95583b2009-03-03 17:47:06 -0800349class MirrorSafeCommand(object):
350 """Command permits itself to run within a mirror,
351 and does not require a working directory.
352 """
Dan Willemsen9ff2ece2015-08-31 15:45:06 -0700353
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700354
Dan Willemsen79360642015-08-31 15:45:06 -0700355class GitcAvailableCommand(object):
Dan Willemsen9ff2ece2015-08-31 15:45:06 -0700356 """Command that requires GITC to be available, but does
357 not require the local client to be a GITC client.
358 """
Dan Willemsen79360642015-08-31 15:45:06 -0700359
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700360
Dan Willemsen79360642015-08-31 15:45:06 -0700361class GitcClientCommand(object):
362 """Command that requires the local client to be a GITC
363 client.
364 """