# # Copyright (C) 2008 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import fcntl import re import os import select import sys import subprocess from color import Coloring from command import Command, MirrorSafeCommand _CAN_COLOR = [ 'branch', 'diff', 'grep', 'log', ] class ForallColoring(Coloring): def __init__(self, config): Coloring.__init__(self, config, 'forall') self.project = self.printer('project', attr='bold') class Forall(Command, MirrorSafeCommand): common = False helpSummary = "Run a shell command in each project" helpUsage = """ %prog [...] -c [...] %prog -r str1 [str2] ... -c [...]" """ helpDescription = """ Executes the same shell command in each project. The -r option allows running the command only on projects matching regex or wildcard expression. Output Formatting ----------------- The -p option causes '%prog' to bind pipes to the command's stdin, stdout and stderr streams, and pipe all output into a continuous stream that is displayed in a single pager session. Project headings are inserted before the output of each command is displayed. If the command produces no output in a project, no heading is displayed. The formatting convention used by -p is very suitable for some types of searching, e.g. `repo forall -p -c git log -SFoo` will print all commits that add or remove references to Foo. The -v option causes '%prog' to display stderr messages if a command produces output only on stderr. Normally the -p option causes command output to be suppressed until the command produces at least one byte of output on stdout. Environment ----------- pwd is the project's working directory. If the current client is a mirror client, then pwd is the Git repository. REPO_PROJECT is set to the unique name of the project. REPO_PATH is the path relative the the root of the client. REPO_REMOTE is the name of the remote system from the manifest. REPO_LREV is the name of the revision from the manifest, translated to a local tracking branch. If you need to pass the manifest revision to a locally executed git command, use REPO_LREV. REPO_RREV is the name of the revision from the manifest, exactly as written in the manifest. REPO__* are any extra environment variables, specified by the "annotation" element under any project element. This can be useful for differentiating trees based on user-specific criteria, or simply annotating tree details. shell positional arguments ($1, $2, .., $#) are set to any arguments following . Unless -p is used, stdin, stdout, stderr are inherited from the terminal and are not redirected. If -e is used, when a command exits unsuccessfully, '%prog' will abort without iterating through the remaining projects. """ def _Options(self, p): def cmd(option, opt_str, value, parser): setattr(parser.values, option.dest, list(parser.rargs)) while parser.rargs: del parser.rargs[0] p.add_option('-r', '--regex', dest='regex', action='store_true', help="Execute the command only on projects matching regex or wildcard expression") p.add_option('-c', '--command', help='Command (and arguments) to execute', dest='command', action='callback', callback=cmd) p.add_option('-e', '--abort-on-errors', dest='abort_on_errors', action='store_true', help='Abort if a command exits unsuccessfully') g = p.add_option_group('Output') g.add_option('-p', dest='project_header', action='store_true', help='Show project headers before output') g.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Show command error messages') def WantPager(self, opt): return opt.project_header def Execute(self, opt, args): if not opt.command: self.Usage() cmd = [opt.command[0]] shell = True if re.compile(r'^[a-z0-9A-Z_/\.-]+$').match(cmd[0]): shell = False if shell: cmd.append(cmd[0]) cmd.extend(opt.command[1:]) if opt.project_header \ and not shell \ and cmd[0] == 'git': # If this is a direct git command that can enable colorized # output and the user prefers coloring, add --color into the # command line because we are going to wrap the command into # a pipe and git won't know coloring should activate. # for cn in cmd[1:]: if not cn.startswith('-'): break else: cn = None # pylint: disable=W0631 if cn and cn in _CAN_COLOR: class ColorCmd(Coloring): def __init__(self, config, cmd): Coloring.__init__(self, config, cmd) if ColorCmd(self.manifest.manifestProject.config, cn).is_on: cmd.insert(cmd.index(cn) + 1, '--color') # pylint: enable=W0631 mirror = self.manifest.IsMirror out = ForallColoring(self.manifest.manifestProject.config) out.redirect(sys.stdout) rc = 0 first = True if not opt.regex: projects = self.GetProjects(args) else: projects = self.FindProjects(args) for project in projects: env = os.environ.copy() def setenv(name, val): if val is None: val = '' env[name] = val.encode() setenv('REPO_PROJECT', project.name) setenv('REPO_PATH', project.relpath) setenv('REPO_REMOTE', project.remote.name) setenv('REPO_LREV', project.GetRevisionId()) setenv('REPO_RREV', project.revisionExpr) for a in project.annotations: setenv("REPO__%s" % (a.name), a.value) if mirror: setenv('GIT_DIR', project.gitdir) cwd = project.gitdir else: cwd = project.worktree if not os.path.exists(cwd): if (opt.project_header and opt.verbose) \ or not opt.project_header: print('skipping %s/' % project.relpath, file=sys.stderr) continue if opt.project_header: stdin = subprocess.PIPE stdout = subprocess.PIPE stderr = subprocess.PIPE else: stdin = None stdout = None stderr = None p = subprocess.Popen(cmd, cwd = cwd, shell = shell, env = env, stdin = stdin, stdout = stdout, stderr = stderr) if opt.project_header: class sfd(object): def __init__(self, fd, dest): self.fd = fd self.dest = dest def fileno(self): return self.fd.fileno() empty = True errbuf = '' p.stdin.close() s_in = [sfd(p.stdout, sys.stdout), sfd(p.stderr, sys.stderr)] for s in s_in: flags = fcntl.fcntl(s.fd, fcntl.F_GETFL) fcntl.fcntl(s.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) while s_in: in_ready, _out_ready, _err_ready = select.select(s_in, [], []) for s in in_ready: buf = s.fd.read(4096) if not buf: s.fd.close() s_in.remove(s) continue if not opt.verbose: if s.fd != p.stdout: errbuf += buf continue if empty: if first: first = False else: out.nl() if mirror: project_header_path = project.name else: project_header_path = project.relpath out.project('project %s/', project_header_path) out.nl() out.flush() if errbuf: sys.stderr.write(errbuf) sys.stderr.flush() errbuf = '' empty = False s.dest.write(buf) s.dest.flush() r = p.wait() if r != 0: if r != rc: rc = r if opt.abort_on_errors: print("error: %s: Aborting due to previous error" % project.relpath, file=sys.stderr) sys.exit(r) if rc != 0: sys.exit(rc)