summaryrefslogtreecommitdiffstats
path: root/subcmds/forall.py
diff options
context:
space:
mode:
Diffstat (limited to 'subcmds/forall.py')
-rw-r--r--subcmds/forall.py516
1 files changed, 285 insertions, 231 deletions
diff --git a/subcmds/forall.py b/subcmds/forall.py
index f9f34e33..0a897357 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -23,31 +23,36 @@ import sys
23import subprocess 23import subprocess
24 24
25from color import Coloring 25from color import Coloring
26from command import DEFAULT_LOCAL_JOBS, Command, MirrorSafeCommand, WORKER_BATCH_SIZE 26from command import (
27 DEFAULT_LOCAL_JOBS,
28 Command,
29 MirrorSafeCommand,
30 WORKER_BATCH_SIZE,
31)
27from error import ManifestInvalidRevisionError 32from error import ManifestInvalidRevisionError
28 33
29_CAN_COLOR = [ 34_CAN_COLOR = [
30 'branch', 35 "branch",
31 'diff', 36 "diff",
32 'grep', 37 "grep",
33 'log', 38 "log",
34] 39]
35 40
36 41
37class ForallColoring(Coloring): 42class ForallColoring(Coloring):
38 def __init__(self, config): 43 def __init__(self, config):
39 Coloring.__init__(self, config, 'forall') 44 Coloring.__init__(self, config, "forall")
40 self.project = self.printer('project', attr='bold') 45 self.project = self.printer("project", attr="bold")
41 46
42 47
43class Forall(Command, MirrorSafeCommand): 48class Forall(Command, MirrorSafeCommand):
44 COMMON = False 49 COMMON = False
45 helpSummary = "Run a shell command in each project" 50 helpSummary = "Run a shell command in each project"
46 helpUsage = """ 51 helpUsage = """
47%prog [<project>...] -c <command> [<arg>...] 52%prog [<project>...] -c <command> [<arg>...]
48%prog -r str1 [str2] ... -c <command> [<arg>...] 53%prog -r str1 [str2] ... -c <command> [<arg>...]
49""" 54"""
50 helpDescription = """ 55 helpDescription = """
51Executes the same shell command in each project. 56Executes the same shell command in each project.
52 57
53The -r option allows running the command only on projects matching 58The -r option allows running the command only on projects matching
@@ -125,236 +130,285 @@ terminal and are not redirected.
125If -e is used, when a command exits unsuccessfully, '%prog' will abort 130If -e is used, when a command exits unsuccessfully, '%prog' will abort
126without iterating through the remaining projects. 131without iterating through the remaining projects.
127""" 132"""
128 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS 133 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
129 134
130 @staticmethod 135 @staticmethod
131 def _cmd_option(option, _opt_str, _value, parser): 136 def _cmd_option(option, _opt_str, _value, parser):
132 setattr(parser.values, option.dest, list(parser.rargs)) 137 setattr(parser.values, option.dest, list(parser.rargs))
133 while parser.rargs: 138 while parser.rargs:
134 del parser.rargs[0] 139 del parser.rargs[0]
135 140
136 def _Options(self, p): 141 def _Options(self, p):
137 p.add_option('-r', '--regex', 142 p.add_option(
138 dest='regex', action='store_true', 143 "-r",
139 help='execute the command only on projects matching regex or wildcard expression') 144 "--regex",
140 p.add_option('-i', '--inverse-regex', 145 dest="regex",
141 dest='inverse_regex', action='store_true', 146 action="store_true",
142 help='execute the command only on projects not matching regex or ' 147 help="execute the command only on projects matching regex or "
143 'wildcard expression') 148 "wildcard expression",
144 p.add_option('-g', '--groups', 149 )
145 dest='groups', 150 p.add_option(
146 help='execute the command only on projects matching the specified groups') 151 "-i",
147 p.add_option('-c', '--command', 152 "--inverse-regex",
148 help='command (and arguments) to execute', 153 dest="inverse_regex",
149 dest='command', 154 action="store_true",
150 action='callback', 155 help="execute the command only on projects not matching regex or "
151 callback=self._cmd_option) 156 "wildcard expression",
152 p.add_option('-e', '--abort-on-errors', 157 )
153 dest='abort_on_errors', action='store_true', 158 p.add_option(
154 help='abort if a command exits unsuccessfully') 159 "-g",
155 p.add_option('--ignore-missing', action='store_true', 160 "--groups",
156 help='silently skip & do not exit non-zero due missing ' 161 dest="groups",
157 'checkouts') 162 help="execute the command only on projects matching the specified "
158 163 "groups",
159 g = p.get_option_group('--quiet') 164 )
160 g.add_option('-p', 165 p.add_option(
161 dest='project_header', action='store_true', 166 "-c",
162 help='show project headers before output') 167 "--command",
163 p.add_option('--interactive', 168 help="command (and arguments) to execute",
164 action='store_true', 169 dest="command",
165 help='force interactive usage') 170 action="callback",
166 171 callback=self._cmd_option,
167 def WantPager(self, opt): 172 )
168 return opt.project_header and opt.jobs == 1 173 p.add_option(
169 174 "-e",
170 def ValidateOptions(self, opt, args): 175 "--abort-on-errors",
171 if not opt.command: 176 dest="abort_on_errors",
172 self.Usage() 177 action="store_true",
173 178 help="abort if a command exits unsuccessfully",
174 def Execute(self, opt, args): 179 )
175 cmd = [opt.command[0]] 180 p.add_option(
176 all_trees = not opt.this_manifest_only 181 "--ignore-missing",
177 182 action="store_true",
178 shell = True 183 help="silently skip & do not exit non-zero due missing "
179 if re.compile(r'^[a-z0-9A-Z_/\.-]+$').match(cmd[0]): 184 "checkouts",
180 shell = False 185 )
181 186
182 if shell: 187 g = p.get_option_group("--quiet")
183 cmd.append(cmd[0]) 188 g.add_option(
184 cmd.extend(opt.command[1:]) 189 "-p",
185 190 dest="project_header",
186 # Historically, forall operated interactively, and in serial. If the user 191 action="store_true",
187 # has selected 1 job, then default to interacive mode. 192 help="show project headers before output",
188 if opt.jobs == 1: 193 )
189 opt.interactive = True 194 p.add_option(
190 195 "--interactive", action="store_true", help="force interactive usage"
191 if opt.project_header \ 196 )
192 and not shell \ 197
193 and cmd[0] == 'git': 198 def WantPager(self, opt):
194 # If this is a direct git command that can enable colorized 199 return opt.project_header and opt.jobs == 1
195 # output and the user prefers coloring, add --color into the 200
196 # command line because we are going to wrap the command into 201 def ValidateOptions(self, opt, args):
197 # a pipe and git won't know coloring should activate. 202 if not opt.command:
198 # 203 self.Usage()
199 for cn in cmd[1:]: 204
200 if not cn.startswith('-'): 205 def Execute(self, opt, args):
201 break 206 cmd = [opt.command[0]]
202 else: 207 all_trees = not opt.this_manifest_only
203 cn = None 208
204 if cn and cn in _CAN_COLOR: 209 shell = True
205 class ColorCmd(Coloring): 210 if re.compile(r"^[a-z0-9A-Z_/\.-]+$").match(cmd[0]):
206 def __init__(self, config, cmd): 211 shell = False
207 Coloring.__init__(self, config, cmd) 212
208 if ColorCmd(self.manifest.manifestProject.config, cn).is_on: 213 if shell:
209 cmd.insert(cmd.index(cn) + 1, '--color') 214 cmd.append(cmd[0])
210 215 cmd.extend(opt.command[1:])
211 mirror = self.manifest.IsMirror 216
212 rc = 0 217 # Historically, forall operated interactively, and in serial. If the
213 218 # user has selected 1 job, then default to interacive mode.
214 smart_sync_manifest_name = "smart_sync_override.xml" 219 if opt.jobs == 1:
215 smart_sync_manifest_path = os.path.join( 220 opt.interactive = True
216 self.manifest.manifestProject.worktree, smart_sync_manifest_name) 221
217 222 if opt.project_header and not shell and cmd[0] == "git":
218 if os.path.isfile(smart_sync_manifest_path): 223 # If this is a direct git command that can enable colorized
219 self.manifest.Override(smart_sync_manifest_path) 224 # output and the user prefers coloring, add --color into the
220 225 # command line because we are going to wrap the command into
221 if opt.regex: 226 # a pipe and git won't know coloring should activate.
222 projects = self.FindProjects(args, all_manifests=all_trees) 227 #
223 elif opt.inverse_regex: 228 for cn in cmd[1:]:
224 projects = self.FindProjects(args, inverse=True, all_manifests=all_trees) 229 if not cn.startswith("-"):
225 else: 230 break
226 projects = self.GetProjects(args, groups=opt.groups, all_manifests=all_trees) 231 else:
227 232 cn = None
228 os.environ['REPO_COUNT'] = str(len(projects)) 233 if cn and cn in _CAN_COLOR:
229 234
230 try: 235 class ColorCmd(Coloring):
231 config = self.manifest.manifestProject.config 236 def __init__(self, config, cmd):
232 with multiprocessing.Pool(opt.jobs, InitWorker) as pool: 237 Coloring.__init__(self, config, cmd)
233 results_it = pool.imap( 238
234 functools.partial(DoWorkWrapper, mirror, opt, cmd, shell, config), 239 if ColorCmd(self.manifest.manifestProject.config, cn).is_on:
235 enumerate(projects), 240 cmd.insert(cmd.index(cn) + 1, "--color")
236 chunksize=WORKER_BATCH_SIZE) 241
237 first = True 242 mirror = self.manifest.IsMirror
238 for (r, output) in results_it: 243 rc = 0
239 if output: 244
240 if first: 245 smart_sync_manifest_name = "smart_sync_override.xml"
241 first = False 246 smart_sync_manifest_path = os.path.join(
242 elif opt.project_header: 247 self.manifest.manifestProject.worktree, smart_sync_manifest_name
243 print() 248 )
244 # To simplify the DoWorkWrapper, take care of automatic newlines. 249
245 end = '\n' 250 if os.path.isfile(smart_sync_manifest_path):
246 if output[-1] == '\n': 251 self.manifest.Override(smart_sync_manifest_path)
247 end = '' 252
248 print(output, end=end) 253 if opt.regex:
249 rc = rc or r 254 projects = self.FindProjects(args, all_manifests=all_trees)
250 if r != 0 and opt.abort_on_errors: 255 elif opt.inverse_regex:
251 raise Exception('Aborting due to previous error') 256 projects = self.FindProjects(
252 except (KeyboardInterrupt, WorkerKeyboardInterrupt): 257 args, inverse=True, all_manifests=all_trees
253 # Catch KeyboardInterrupt raised inside and outside of workers 258 )
254 rc = rc or errno.EINTR 259 else:
255 except Exception as e: 260 projects = self.GetProjects(
256 # Catch any other exceptions raised 261 args, groups=opt.groups, all_manifests=all_trees
257 print('forall: unhandled error, terminating the pool: %s: %s' % 262 )
258 (type(e).__name__, e), 263
259 file=sys.stderr) 264 os.environ["REPO_COUNT"] = str(len(projects))
260 rc = rc or getattr(e, 'errno', 1) 265
261 if rc != 0: 266 try:
262 sys.exit(rc) 267 config = self.manifest.manifestProject.config
268 with multiprocessing.Pool(opt.jobs, InitWorker) as pool:
269 results_it = pool.imap(
270 functools.partial(
271 DoWorkWrapper, mirror, opt, cmd, shell, config
272 ),
273 enumerate(projects),
274 chunksize=WORKER_BATCH_SIZE,
275 )
276 first = True
277 for r, output in results_it:
278 if output:
279 if first:
280 first = False
281 elif opt.project_header:
282 print()
283 # To simplify the DoWorkWrapper, take care of automatic
284 # newlines.
285 end = "\n"
286 if output[-1] == "\n":
287 end = ""
288 print(output, end=end)
289 rc = rc or r
290 if r != 0 and opt.abort_on_errors:
291 raise Exception("Aborting due to previous error")
292 except (KeyboardInterrupt, WorkerKeyboardInterrupt):
293 # Catch KeyboardInterrupt raised inside and outside of workers
294 rc = rc or errno.EINTR
295 except Exception as e:
296 # Catch any other exceptions raised
297 print(
298 "forall: unhandled error, terminating the pool: %s: %s"
299 % (type(e).__name__, e),
300 file=sys.stderr,
301 )
302 rc = rc or getattr(e, "errno", 1)
303 if rc != 0:
304 sys.exit(rc)
263 305
264 306
265class WorkerKeyboardInterrupt(Exception): 307class WorkerKeyboardInterrupt(Exception):
266 """ Keyboard interrupt exception for worker processes. """ 308 """Keyboard interrupt exception for worker processes."""
267 309
268 310
269def InitWorker(): 311def InitWorker():
270 signal.signal(signal.SIGINT, signal.SIG_IGN) 312 signal.signal(signal.SIGINT, signal.SIG_IGN)
271 313
272 314
273def DoWorkWrapper(mirror, opt, cmd, shell, config, args): 315def DoWorkWrapper(mirror, opt, cmd, shell, config, args):
274 """ A wrapper around the DoWork() method. 316 """A wrapper around the DoWork() method.
275 317
276 Catch the KeyboardInterrupt exceptions here and re-raise them as a different, 318 Catch the KeyboardInterrupt exceptions here and re-raise them as a
277 ``Exception``-based exception to stop it flooding the console with stacktraces 319 different, ``Exception``-based exception to stop it flooding the console
278 and making the parent hang indefinitely. 320 with stacktraces and making the parent hang indefinitely.
279 321
280 """ 322 """
281 cnt, project = args 323 cnt, project = args
282 try: 324 try:
283 return DoWork(project, mirror, opt, cmd, shell, cnt, config) 325 return DoWork(project, mirror, opt, cmd, shell, cnt, config)
284 except KeyboardInterrupt: 326 except KeyboardInterrupt:
285 print('%s: Worker interrupted' % project.name) 327 print("%s: Worker interrupted" % project.name)
286 raise WorkerKeyboardInterrupt() 328 raise WorkerKeyboardInterrupt()
287 329
288 330
289def DoWork(project, mirror, opt, cmd, shell, cnt, config): 331def DoWork(project, mirror, opt, cmd, shell, cnt, config):
290 env = os.environ.copy() 332 env = os.environ.copy()
291 333
292 def setenv(name, val): 334 def setenv(name, val):
293 if val is None: 335 if val is None:
294 val = '' 336 val = ""
295 env[name] = val 337 env[name] = val
296 338
297 setenv('REPO_PROJECT', project.name) 339 setenv("REPO_PROJECT", project.name)
298 setenv('REPO_OUTERPATH', project.manifest.path_prefix) 340 setenv("REPO_OUTERPATH", project.manifest.path_prefix)
299 setenv('REPO_INNERPATH', project.relpath) 341 setenv("REPO_INNERPATH", project.relpath)
300 setenv('REPO_PATH', project.RelPath(local=opt.this_manifest_only)) 342 setenv("REPO_PATH", project.RelPath(local=opt.this_manifest_only))
301 setenv('REPO_REMOTE', project.remote.name) 343 setenv("REPO_REMOTE", project.remote.name)
302 try: 344 try:
303 # If we aren't in a fully synced state and we don't have the ref the manifest 345 # If we aren't in a fully synced state and we don't have the ref the
304 # wants, then this will fail. Ignore it for the purposes of this code. 346 # manifest wants, then this will fail. Ignore it for the purposes of
305 lrev = '' if mirror else project.GetRevisionId() 347 # this code.
306 except ManifestInvalidRevisionError: 348 lrev = "" if mirror else project.GetRevisionId()
307 lrev = '' 349 except ManifestInvalidRevisionError:
308 setenv('REPO_LREV', lrev) 350 lrev = ""
309 setenv('REPO_RREV', project.revisionExpr) 351 setenv("REPO_LREV", lrev)
310 setenv('REPO_UPSTREAM', project.upstream) 352 setenv("REPO_RREV", project.revisionExpr)
311 setenv('REPO_DEST_BRANCH', project.dest_branch) 353 setenv("REPO_UPSTREAM", project.upstream)
312 setenv('REPO_I', str(cnt + 1)) 354 setenv("REPO_DEST_BRANCH", project.dest_branch)
313 for annotation in project.annotations: 355 setenv("REPO_I", str(cnt + 1))
314 setenv("REPO__%s" % (annotation.name), annotation.value) 356 for annotation in project.annotations:
315 357 setenv("REPO__%s" % (annotation.name), annotation.value)
316 if mirror: 358
317 setenv('GIT_DIR', project.gitdir) 359 if mirror:
318 cwd = project.gitdir 360 setenv("GIT_DIR", project.gitdir)
319 else: 361 cwd = project.gitdir
320 cwd = project.worktree 362 else:
321 363 cwd = project.worktree
322 if not os.path.exists(cwd): 364
323 # Allow the user to silently ignore missing checkouts so they can run on 365 if not os.path.exists(cwd):
324 # partial checkouts (good for infra recovery tools). 366 # Allow the user to silently ignore missing checkouts so they can run on
325 if opt.ignore_missing: 367 # partial checkouts (good for infra recovery tools).
326 return (0, '') 368 if opt.ignore_missing:
327 369 return (0, "")
328 output = '' 370
329 if ((opt.project_header and opt.verbose) 371 output = ""
330 or not opt.project_header): 372 if (opt.project_header and opt.verbose) or not opt.project_header:
331 output = 'skipping %s/' % project.RelPath(local=opt.this_manifest_only) 373 output = "skipping %s/" % project.RelPath(
332 return (1, output) 374 local=opt.this_manifest_only
333 375 )
334 if opt.verbose: 376 return (1, output)
335 stderr = subprocess.STDOUT 377
336 else: 378 if opt.verbose:
337 stderr = subprocess.DEVNULL 379 stderr = subprocess.STDOUT
338 380 else:
339 stdin = None if opt.interactive else subprocess.DEVNULL 381 stderr = subprocess.DEVNULL
340 382
341 result = subprocess.run( 383 stdin = None if opt.interactive else subprocess.DEVNULL
342 cmd, cwd=cwd, shell=shell, env=env, check=False, 384
343 encoding='utf-8', errors='replace', 385 result = subprocess.run(
344 stdin=stdin, stdout=subprocess.PIPE, stderr=stderr) 386 cmd,
345 387 cwd=cwd,
346 output = result.stdout 388 shell=shell,
347 if opt.project_header: 389 env=env,
348 if output: 390 check=False,
349 buf = io.StringIO() 391 encoding="utf-8",
350 out = ForallColoring(config) 392 errors="replace",
351 out.redirect(buf) 393 stdin=stdin,
352 if mirror: 394 stdout=subprocess.PIPE,
353 project_header_path = project.name 395 stderr=stderr,
354 else: 396 )
355 project_header_path = project.RelPath(local=opt.this_manifest_only) 397
356 out.project('project %s/' % project_header_path) 398 output = result.stdout
357 out.nl() 399 if opt.project_header:
358 buf.write(output) 400 if output:
359 output = buf.getvalue() 401 buf = io.StringIO()
360 return (result.returncode, output) 402 out = ForallColoring(config)
403 out.redirect(buf)
404 if mirror:
405 project_header_path = project.name
406 else:
407 project_header_path = project.RelPath(
408 local=opt.this_manifest_only
409 )
410 out.project("project %s/" % project_header_path)
411 out.nl()
412 buf.write(output)
413 output = buf.getvalue()
414 return (result.returncode, output)