blob: 4087cab5964d1764afd2d596af728676c8b63c93 [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 Frysinger7c871162021-02-16 01:45:39 -050027# Number of projects to submit to a single worker process at a time.
28# This number represents a tradeoff between the overhead of IPC and finer
29# grained opportunity for parallelism. This particular value was chosen by
30# iterating through powers of two until the overall performance no longer
31# improved. The performance of this batch size is not a function of the
32# number of cores on the system.
33WORKER_BATCH_SIZE = 32
34
35
Mike Frysinger6a2400a2021-02-16 01:43:31 -050036# How many jobs to run in parallel by default? This assumes the jobs are
37# largely I/O bound and do not hit the network.
38DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8)
39
40
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070041class Command(object):
42 """Base class for any command line action in repo.
43 """
44
Mike Frysingerd88b3692021-06-14 16:09:29 -040045 # Singleton for all commands to track overall repo command execution and
46 # provide event summary to callers. Only used by sync subcommand currently.
47 #
48 # NB: This is being replaced by git trace2 events. See git_trace2_event_log.
49 event_log = EventLog()
50
Mike Frysinger4f210542021-06-14 16:05:19 -040051 # Whether this command is a "common" one, i.e. whether the user would commonly
52 # use it or it's a more uncommon command. This is used by the help command to
53 # show short-vs-full summaries.
54 COMMON = False
55
Mike Frysinger6a2400a2021-02-16 01:43:31 -050056 # Whether this command supports running in parallel. If greater than 0,
57 # it is the number of parallel jobs to default to.
58 PARALLEL_JOBS = None
59
Raman Tenneti784e16f2021-06-11 17:29:45 -070060 def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None,
61 git_event_log=None):
Mike Frysingerd58d0dd2021-06-14 16:17:27 -040062 self.repodir = repodir
63 self.client = client
64 self.manifest = manifest
65 self.gitc_manifest = gitc_manifest
Raman Tenneti784e16f2021-06-11 17:29:45 -070066 self.git_event_log = git_event_log
Mike Frysingerd58d0dd2021-06-14 16:17:27 -040067
68 # Cache for the OptionParser property.
69 self._optparse = None
70
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -070071 def WantPager(self, _opt):
Shawn O. Pearcedb45da12009-04-18 13:49:13 -070072 return False
73
David Pursehouseb148ac92012-11-16 09:33:39 +090074 def ReadEnvironmentOptions(self, opts):
75 """ Set options from environment variables. """
76
77 env_options = self._RegisteredEnvironmentOptions()
78
79 for env_key, opt_key in env_options.items():
80 # Get the user-set option value if any
81 opt_value = getattr(opts, opt_key)
82
83 # If the value is set, it means the user has passed it as a command
84 # line option, and we should use that. Otherwise we can try to set it
85 # with the value from the corresponding environment variable.
86 if opt_value is not None:
87 continue
88
89 env_value = os.environ.get(env_key)
90 if env_value is not None:
91 setattr(opts, opt_key, env_value)
92
93 return opts
94
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070095 @property
96 def OptionParser(self):
97 if self._optparse is None:
98 try:
99 me = 'repo %s' % self.NAME
100 usage = self.helpUsage.strip().replace('%prog', me)
101 except AttributeError:
102 usage = 'repo %s' % self.NAME
Mike Frysinger72ebf192020-02-19 01:20:18 -0500103 epilog = 'Run `repo help %s` to view the detailed manual.' % self.NAME
104 self._optparse = optparse.OptionParser(usage=usage, epilog=epilog)
Mike Frysinger9180a072021-04-13 14:57:40 -0400105 self._CommonOptions(self._optparse)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700106 self._Options(self._optparse)
107 return self._optparse
108
Mike Frysinger9180a072021-04-13 14:57:40 -0400109 def _CommonOptions(self, p, opt_v=True):
110 """Initialize the option parser with common options.
111
112 These will show up for *all* subcommands, so use sparingly.
113 NB: Keep in sync with repo:InitParser().
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700114 """
Mike Frysinger9180a072021-04-13 14:57:40 -0400115 g = p.add_option_group('Logging options')
116 opts = ['-v'] if opt_v else []
117 g.add_option(*opts, '--verbose',
118 dest='output_mode', action='store_true',
119 help='show all output')
120 g.add_option('-q', '--quiet',
121 dest='output_mode', action='store_false',
122 help='only show errors')
123
Mike Frysinger6a2400a2021-02-16 01:43:31 -0500124 if self.PARALLEL_JOBS is not None:
125 p.add_option(
126 '-j', '--jobs',
127 type=int, default=self.PARALLEL_JOBS,
128 help='number of jobs to run in parallel (default: %s)' % self.PARALLEL_JOBS)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700129
Mike Frysinger9180a072021-04-13 14:57:40 -0400130 def _Options(self, p):
131 """Initialize the option parser with subcommand-specific options."""
132
David Pursehouseb148ac92012-11-16 09:33:39 +0900133 def _RegisteredEnvironmentOptions(self):
134 """Get options that can be set from environment variables.
135
136 Return a dictionary mapping environment variable name
137 to option key name that it can override.
138
139 Example: {'REPO_MY_OPTION': 'my_option'}
140
141 Will allow the option with key value 'my_option' to be set
142 from the value in the environment variable named 'REPO_MY_OPTION'.
143
144 Note: This does not work properly for options that are explicitly
145 set to None by the user, or options that are defined with a
146 default value other than None.
147
148 """
149 return {}
150
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700151 def Usage(self):
152 """Display usage and terminate.
153 """
154 self.OptionParser.print_usage()
155 sys.exit(1)
156
Mike Frysinger9180a072021-04-13 14:57:40 -0400157 def CommonValidateOptions(self, opt, args):
158 """Validate common options."""
159 opt.quiet = opt.output_mode is False
160 opt.verbose = opt.output_mode is True
161
Mike Frysingerae6cb082019-08-27 01:10:59 -0400162 def ValidateOptions(self, opt, args):
163 """Validate the user options & arguments before executing.
164
165 This is meant to help break the code up into logical steps. Some tips:
166 * Use self.OptionParser.error to display CLI related errors.
167 * Adjust opt member defaults as makes sense.
168 * Adjust the args list, but do so inplace so the caller sees updates.
169 * Try to avoid updating self state. Leave that to Execute.
170 """
171
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700172 def Execute(self, opt, args):
173 """Perform the action, after option parsing is complete.
174 """
175 raise NotImplementedError
Conley Owens971de8e2012-04-16 10:36:08 -0700176
Mike Frysingerb5d075d2021-03-01 00:56:38 -0500177 @staticmethod
178 def ExecuteInParallel(jobs, func, inputs, callback, output=None, ordered=False):
179 """Helper for managing parallel execution boiler plate.
180
181 For subcommands that can easily split their work up.
182
183 Args:
184 jobs: How many parallel processes to use.
185 func: The function to apply to each of the |inputs|. Usually a
186 functools.partial for wrapping additional arguments. It will be run
187 in a separate process, so it must be pickalable, so nested functions
188 won't work. Methods on the subcommand Command class should work.
189 inputs: The list of items to process. Must be a list.
190 callback: The function to pass the results to for processing. It will be
191 executed in the main thread and process the results of |func| as they
192 become available. Thus it may be a local nested function. Its return
193 value is passed back directly. It takes three arguments:
194 - The processing pool (or None with one job).
195 - The |output| argument.
196 - An iterator for the results.
197 output: An output manager. May be progress.Progess or color.Coloring.
198 ordered: Whether the jobs should be processed in order.
199
200 Returns:
201 The |callback| function's results are returned.
202 """
203 try:
204 # NB: Multiprocessing is heavy, so don't spin it up for one job.
205 if len(inputs) == 1 or jobs == 1:
206 return callback(None, output, (func(x) for x in inputs))
207 else:
208 with multiprocessing.Pool(jobs) as pool:
209 submit = pool.imap if ordered else pool.imap_unordered
210 return callback(pool, output, submit(func, inputs, chunksize=WORKER_BATCH_SIZE))
211 finally:
212 if isinstance(output, progress.Progress):
213 output.end()
214
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800215 def _ResetPathToProjectMap(self, projects):
216 self._by_path = dict((p.worktree, p) for p in projects)
217
218 def _UpdatePathToProjectMap(self, project):
219 self._by_path[project.worktree] = project
220
Simran Basib9a1b732015-08-20 12:19:28 -0700221 def _GetProjectByPath(self, manifest, path):
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800222 project = None
223 if os.path.exists(path):
224 oldpath = None
David Pursehouse5a2517f2020-02-12 14:55:01 +0900225 while (path and
226 path != oldpath and
227 path != manifest.topdir):
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800228 try:
229 project = self._by_path[path]
230 break
231 except KeyError:
232 oldpath = path
233 path = os.path.dirname(path)
Mark E. Hamiltonf9fe3e12016-02-23 18:10:42 -0700234 if not project and path == manifest.topdir:
235 try:
236 project = self._by_path[path]
237 except KeyError:
238 pass
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800239 else:
240 try:
241 project = self._by_path[path]
242 except KeyError:
243 pass
244 return project
245
Simran Basib9a1b732015-08-20 12:19:28 -0700246 def GetProjects(self, args, manifest=None, groups='', missing_ok=False,
247 submodules_ok=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700248 """A list of projects that match the arguments.
249 """
Simran Basib9a1b732015-08-20 12:19:28 -0700250 if not manifest:
251 manifest = self.manifest
252 all_projects_list = manifest.projects
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700253 result = []
254
Simran Basib9a1b732015-08-20 12:19:28 -0700255 mp = manifest.manifestProject
Colin Cross5acde752012-03-28 20:15:45 -0700256
Graham Christensen0369a062015-07-29 17:02:54 -0500257 if not groups:
Raman Tenneti080877e2021-03-09 15:19:06 -0800258 groups = manifest.GetGroupsStr()
David Pursehouse1d947b32012-10-25 12:23:11 +0900259 groups = [x for x in re.split(r'[,\s]+', groups) if x]
Colin Cross5acde752012-03-28 20:15:45 -0700260
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700261 if not args:
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800262 derived_projects = {}
263 for project in all_projects_list:
264 if submodules_ok or project.sync_s:
265 derived_projects.update((p.name, p)
266 for p in project.GetDerivedSubprojects())
267 all_projects_list.extend(derived_projects.values())
268 for project in all_projects_list:
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700269 if (missing_ok or project.Exists) and project.MatchesGroups(groups):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700270 result.append(project)
271 else:
David James8d201162013-10-11 17:03:19 -0700272 self._ResetPathToProjectMap(all_projects_list)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700273
274 for arg in args:
Mike Frysingere778e572019-10-04 14:21:41 -0400275 # We have to filter by manifest groups in case the requested project is
276 # checked out multiple times or differently based on them.
277 projects = [project for project in manifest.GetProjectsWithName(arg)
278 if project.MatchesGroups(groups)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700279
David James8d201162013-10-11 17:03:19 -0700280 if not projects:
Anthony Newnamdf14a702011-01-09 17:31:57 -0800281 path = os.path.abspath(arg).replace('\\', '/')
Simran Basib9a1b732015-08-20 12:19:28 -0700282 project = self._GetProjectByPath(manifest, path)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700283
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800284 # If it's not a derived project, update path->project mapping and
285 # search again, as arg might actually point to a derived subproject.
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700286 if (project and not project.Derived and (submodules_ok or
287 project.sync_s)):
Che-Liang Chioub2bd91c2012-01-11 11:28:42 +0800288 search_again = False
289 for subproject in project.GetDerivedSubprojects():
290 self._UpdatePathToProjectMap(subproject)
291 search_again = True
292 if search_again:
Simran Basib9a1b732015-08-20 12:19:28 -0700293 project = self._GetProjectByPath(manifest, path) or project
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700294
David James8d201162013-10-11 17:03:19 -0700295 if project:
296 projects = [project]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700297
David James8d201162013-10-11 17:03:19 -0700298 if not projects:
299 raise NoSuchProjectError(arg)
300
301 for project in projects:
302 if not missing_ok and not project.Exists:
Mike Frysingere778e572019-10-04 14:21:41 -0400303 raise NoSuchProjectError('%s (%s)' % (arg, project.relpath))
David James8d201162013-10-11 17:03:19 -0700304 if not project.MatchesGroups(groups):
305 raise InvalidProjectGroupsError(arg)
306
307 result.extend(projects)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700308
309 def _getpath(x):
310 return x.relpath
311 result.sort(key=_getpath)
312 return result
313
Takeshi Kanemoto1f056442016-01-26 14:11:35 +0900314 def FindProjects(self, args, inverse=False):
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800315 result = []
David Pursehouse84c4d3c2013-04-30 10:57:37 +0900316 patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args]
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800317 for project in self.GetProjects(''):
David Pursehouse84c4d3c2013-04-30 10:57:37 +0900318 for pattern in patterns:
Takeshi Kanemoto1f056442016-01-26 14:11:35 +0900319 match = pattern.search(project.name) or pattern.search(project.relpath)
320 if not inverse and match:
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800321 result.append(project)
322 break
Takeshi Kanemoto1f056442016-01-26 14:11:35 +0900323 if inverse and match:
324 break
325 else:
326 if inverse:
327 result.append(project)
Zhiguang Lia8864fb2013-03-15 10:32:10 +0800328 result.sort(key=lambda project: project.relpath)
329 return result
330
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700331
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700332class InteractiveCommand(Command):
333 """Command which requires user interaction on the tty and
334 must not run within a pager, even if the user asks to.
335 """
David Pursehouse819827a2020-02-12 15:20:19 +0900336
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700337 def WantPager(self, _opt):
Shawn O. Pearcedb45da12009-04-18 13:49:13 -0700338 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700339
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700340
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700341class PagedCommand(Command):
342 """Command which defaults to output in a pager, as its
343 display tends to be larger than one screen full.
344 """
David Pursehouse819827a2020-02-12 15:20:19 +0900345
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700346 def WantPager(self, _opt):
Shawn O. Pearcedb45da12009-04-18 13:49:13 -0700347 return True
Shawn O. Pearcec95583b2009-03-03 17:47:06 -0800348
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700349
Shawn O. Pearcec95583b2009-03-03 17:47:06 -0800350class MirrorSafeCommand(object):
351 """Command permits itself to run within a mirror,
352 and does not require a working directory.
353 """
Dan Willemsen9ff2ece2015-08-31 15:45:06 -0700354
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700355
Dan Willemsen79360642015-08-31 15:45:06 -0700356class GitcAvailableCommand(object):
Dan Willemsen9ff2ece2015-08-31 15:45:06 -0700357 """Command that requires GITC to be available, but does
358 not require the local client to be a GITC client.
359 """
Dan Willemsen79360642015-08-31 15:45:06 -0700360
Mark E. Hamilton8ccfa742016-02-10 10:44:30 -0700361
Dan Willemsen79360642015-08-31 15:45:06 -0700362class GitcClientCommand(object):
363 """Command that requires the local client to be a GITC
364 client.
365 """