diff options
Diffstat (limited to 'command.py')
| -rw-r--r-- | command.py | 137 |
1 files changed, 122 insertions, 15 deletions
| @@ -1,5 +1,3 @@ | |||
| 1 | # -*- coding:utf-8 -*- | ||
| 2 | # | ||
| 3 | # Copyright (C) 2008 The Android Open Source Project | 1 | # Copyright (C) 2008 The Android Open Source Project |
| 4 | # | 2 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| @@ -14,25 +12,65 @@ | |||
| 14 | # See the License for the specific language governing permissions and | 12 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. | 13 | # limitations under the License. |
| 16 | 14 | ||
| 15 | import multiprocessing | ||
| 17 | import os | 16 | import os |
| 18 | import optparse | 17 | import optparse |
| 19 | import platform | ||
| 20 | import re | 18 | import re |
| 21 | import sys | 19 | import sys |
| 22 | 20 | ||
| 23 | from event_log import EventLog | 21 | from event_log import EventLog |
| 24 | from error import NoSuchProjectError | 22 | from error import NoSuchProjectError |
| 25 | from error import InvalidProjectGroupsError | 23 | from error import InvalidProjectGroupsError |
| 24 | import progress | ||
| 25 | |||
| 26 | |||
| 27 | # Are we generating man-pages? | ||
| 28 | GENERATE_MANPAGES = os.environ.get('_REPO_GENERATE_MANPAGES_') == ' indeed! ' | ||
| 29 | |||
| 30 | |||
| 31 | # 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. | ||
| 37 | WORKER_BATCH_SIZE = 32 | ||
| 38 | |||
| 39 | |||
| 40 | # 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. | ||
| 42 | DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8) | ||
| 26 | 43 | ||
| 27 | 44 | ||
| 28 | class Command(object): | 45 | class Command(object): |
| 29 | """Base class for any command line action in repo. | 46 | """Base class for any command line action in repo. |
| 30 | """ | 47 | """ |
| 31 | 48 | ||
| 32 | common = False | 49 | # 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. | ||
| 33 | event_log = EventLog() | 53 | event_log = EventLog() |
| 34 | manifest = None | 54 | |
| 35 | _optparse = None | 55 | # 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 | |||
| 60 | # 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 | |||
| 64 | def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None, | ||
| 65 | git_event_log=None): | ||
| 66 | self.repodir = repodir | ||
| 67 | self.client = client | ||
| 68 | self.manifest = manifest | ||
| 69 | self.gitc_manifest = gitc_manifest | ||
| 70 | self.git_event_log = git_event_log | ||
| 71 | |||
| 72 | # Cache for the OptionParser property. | ||
| 73 | self._optparse = None | ||
| 36 | 74 | ||
| 37 | def WantPager(self, _opt): | 75 | def WantPager(self, _opt): |
| 38 | return False | 76 | return False |
| @@ -66,13 +104,39 @@ class Command(object): | |||
| 66 | usage = self.helpUsage.strip().replace('%prog', me) | 104 | usage = self.helpUsage.strip().replace('%prog', me) |
| 67 | except AttributeError: | 105 | except AttributeError: |
| 68 | usage = 'repo %s' % self.NAME | 106 | usage = 'repo %s' % self.NAME |
| 69 | self._optparse = optparse.OptionParser(usage=usage) | 107 | epilog = 'Run `repo help %s` to view the detailed manual.' % self.NAME |
| 108 | self._optparse = optparse.OptionParser(usage=usage, epilog=epilog) | ||
| 109 | self._CommonOptions(self._optparse) | ||
| 70 | self._Options(self._optparse) | 110 | self._Options(self._optparse) |
| 71 | return self._optparse | 111 | return self._optparse |
| 72 | 112 | ||
| 73 | def _Options(self, p): | 113 | def _CommonOptions(self, p, opt_v=True): |
| 74 | """Initialize the option parser. | 114 | """Initialize the option parser with common options. |
| 115 | |||
| 116 | These will show up for *all* subcommands, so use sparingly. | ||
| 117 | NB: Keep in sync with repo:InitParser(). | ||
| 75 | """ | 118 | """ |
| 119 | g = p.add_option_group('Logging options') | ||
| 120 | opts = ['-v'] if opt_v else [] | ||
| 121 | g.add_option(*opts, '--verbose', | ||
| 122 | dest='output_mode', action='store_true', | ||
| 123 | help='show all output') | ||
| 124 | g.add_option('-q', '--quiet', | ||
| 125 | dest='output_mode', action='store_false', | ||
| 126 | help='only show errors') | ||
| 127 | |||
| 128 | if self.PARALLEL_JOBS is not None: | ||
| 129 | default = 'based on number of CPU cores' | ||
| 130 | if not GENERATE_MANPAGES: | ||
| 131 | # Only include active cpu count if we aren't generating man pages. | ||
| 132 | default = f'%default; {default}' | ||
| 133 | p.add_option( | ||
| 134 | '-j', '--jobs', | ||
| 135 | type=int, default=self.PARALLEL_JOBS, | ||
| 136 | help=f'number of jobs to run in parallel (default: {default})') | ||
| 137 | |||
| 138 | def _Options(self, p): | ||
| 139 | """Initialize the option parser with subcommand-specific options.""" | ||
| 76 | 140 | ||
| 77 | def _RegisteredEnvironmentOptions(self): | 141 | def _RegisteredEnvironmentOptions(self): |
| 78 | """Get options that can be set from environment variables. | 142 | """Get options that can be set from environment variables. |
| @@ -98,6 +162,11 @@ class Command(object): | |||
| 98 | self.OptionParser.print_usage() | 162 | self.OptionParser.print_usage() |
| 99 | sys.exit(1) | 163 | sys.exit(1) |
| 100 | 164 | ||
| 165 | def CommonValidateOptions(self, opt, args): | ||
| 166 | """Validate common options.""" | ||
| 167 | opt.quiet = opt.output_mode is False | ||
| 168 | opt.verbose = opt.output_mode is True | ||
| 169 | |||
| 101 | def ValidateOptions(self, opt, args): | 170 | def ValidateOptions(self, opt, args): |
| 102 | """Validate the user options & arguments before executing. | 171 | """Validate the user options & arguments before executing. |
| 103 | 172 | ||
| @@ -113,6 +182,44 @@ class Command(object): | |||
| 113 | """ | 182 | """ |
| 114 | raise NotImplementedError | 183 | raise NotImplementedError |
| 115 | 184 | ||
| 185 | @staticmethod | ||
| 186 | def ExecuteInParallel(jobs, func, inputs, callback, output=None, ordered=False): | ||
| 187 | """Helper for managing parallel execution boiler plate. | ||
| 188 | |||
| 189 | For subcommands that can easily split their work up. | ||
| 190 | |||
| 191 | Args: | ||
| 192 | jobs: How many parallel processes to use. | ||
| 193 | func: The function to apply to each of the |inputs|. Usually a | ||
| 194 | functools.partial for wrapping additional arguments. It will be run | ||
| 195 | in a separate process, so it must be pickalable, so nested functions | ||
| 196 | won't work. Methods on the subcommand Command class should work. | ||
| 197 | inputs: The list of items to process. Must be a list. | ||
| 198 | callback: The function to pass the results to for processing. It will be | ||
| 199 | executed in the main thread and process the results of |func| as they | ||
| 200 | become available. Thus it may be a local nested function. Its return | ||
| 201 | value is passed back directly. It takes three arguments: | ||
| 202 | - The processing pool (or None with one job). | ||
| 203 | - The |output| argument. | ||
| 204 | - An iterator for the results. | ||
| 205 | output: An output manager. May be progress.Progess or color.Coloring. | ||
| 206 | ordered: Whether the jobs should be processed in order. | ||
| 207 | |||
| 208 | Returns: | ||
| 209 | The |callback| function's results are returned. | ||
| 210 | """ | ||
| 211 | try: | ||
| 212 | # NB: Multiprocessing is heavy, so don't spin it up for one job. | ||
| 213 | if len(inputs) == 1 or jobs == 1: | ||
| 214 | return callback(None, output, (func(x) for x in inputs)) | ||
| 215 | else: | ||
| 216 | with multiprocessing.Pool(jobs) as pool: | ||
| 217 | submit = pool.imap if ordered else pool.imap_unordered | ||
| 218 | return callback(pool, output, submit(func, inputs, chunksize=WORKER_BATCH_SIZE)) | ||
| 219 | finally: | ||
| 220 | if isinstance(output, progress.Progress): | ||
| 221 | output.end() | ||
| 222 | |||
| 116 | def _ResetPathToProjectMap(self, projects): | 223 | def _ResetPathToProjectMap(self, projects): |
| 117 | self._by_path = dict((p.worktree, p) for p in projects) | 224 | self._by_path = dict((p.worktree, p) for p in projects) |
| 118 | 225 | ||
| @@ -123,9 +230,9 @@ class Command(object): | |||
| 123 | project = None | 230 | project = None |
| 124 | if os.path.exists(path): | 231 | if os.path.exists(path): |
| 125 | oldpath = None | 232 | oldpath = None |
| 126 | while path and \ | 233 | while (path and |
| 127 | path != oldpath and \ | 234 | path != oldpath and |
| 128 | path != manifest.topdir: | 235 | path != manifest.topdir): |
| 129 | try: | 236 | try: |
| 130 | project = self._by_path[path] | 237 | project = self._by_path[path] |
| 131 | break | 238 | break |
| @@ -156,9 +263,7 @@ class Command(object): | |||
| 156 | mp = manifest.manifestProject | 263 | mp = manifest.manifestProject |
| 157 | 264 | ||
| 158 | if not groups: | 265 | if not groups: |
| 159 | groups = mp.config.GetString('manifest.groups') | 266 | groups = manifest.GetGroupsStr() |
| 160 | if not groups: | ||
| 161 | groups = 'default,platform-' + platform.system().lower() | ||
| 162 | groups = [x for x in re.split(r'[,\s]+', groups) if x] | 267 | groups = [x for x in re.split(r'[,\s]+', groups) if x] |
| 163 | 268 | ||
| 164 | if not args: | 269 | if not args: |
| @@ -236,6 +341,7 @@ class InteractiveCommand(Command): | |||
| 236 | """Command which requires user interaction on the tty and | 341 | """Command which requires user interaction on the tty and |
| 237 | must not run within a pager, even if the user asks to. | 342 | must not run within a pager, even if the user asks to. |
| 238 | """ | 343 | """ |
| 344 | |||
| 239 | def WantPager(self, _opt): | 345 | def WantPager(self, _opt): |
| 240 | return False | 346 | return False |
| 241 | 347 | ||
| @@ -244,6 +350,7 @@ class PagedCommand(Command): | |||
| 244 | """Command which defaults to output in a pager, as its | 350 | """Command which defaults to output in a pager, as its |
| 245 | display tends to be larger than one screen full. | 351 | display tends to be larger than one screen full. |
| 246 | """ | 352 | """ |
| 353 | |||
| 247 | def WantPager(self, _opt): | 354 | def WantPager(self, _opt): |
| 248 | return True | 355 | return True |
| 249 | 356 | ||
