summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn O. Pearce <sop@google.com>2009-04-18 13:49:13 -0700
committerShawn O. Pearce <sop@google.com>2009-04-18 13:49:13 -0700
commitdb45da12089bf131579d100ff7990cbc18d07325 (patch)
treec81dbff8a644272dc12cfcdc129ee1f0db3a559f
parent50fa1ac6db388c0aa16751b5ad69d296e5eea047 (diff)
downloadgit-repo-db45da12089bf131579d100ff7990cbc18d07325.tar.gz
Add -p to `repo forall` to improve output formatting
When trying to read log output from many projects at once it can be difficult to make sense of which messages came from where. For many professional developers it is common to want to view the last week's worth of your work, so you can write a weekly summary of your activity for your status report. This is easier with the new -p option: repo forall -pc git log --reverse --since=1.week.ago --author=sop produces a report of all commits written by me in the last week, formatted in a paged output display, with headers inserted in front of each project's output. Where this can be even more useful is with git log's pickaxe, e.g. now we can use: repo forall -pc git log -Sbar v1.0..v1.1 to locate all additions or removals of the symbol 'bar' since v1.0, up to and including v1.1. Before displaying the matching commits in a project, a project header is shown, giving the user some context information for the matching results. Signed-off-by: Shawn O. Pearce <sop@google.com>
-rw-r--r--color.py3
-rw-r--r--command.py7
-rwxr-xr-xmain.py5
-rw-r--r--subcmds/forall.py141
4 files changed, 151 insertions, 5 deletions
diff --git a/color.py b/color.py
index 7fa21d23..5c612acc 100644
--- a/color.py
+++ b/color.py
@@ -110,6 +110,9 @@ class Coloring(object):
110 def write(self, fmt, *args): 110 def write(self, fmt, *args):
111 self._out.write(fmt % args) 111 self._out.write(fmt % args)
112 112
113 def flush(self):
114 self._out.flush()
115
113 def nl(self): 116 def nl(self):
114 self._out.write('\n') 117 self._out.write('\n')
115 118
diff --git a/command.py b/command.py
index c3cad5ea..a941b95a 100644
--- a/command.py
+++ b/command.py
@@ -27,6 +27,9 @@ class Command(object):
27 manifest = None 27 manifest = None
28 _optparse = None 28 _optparse = None
29 29
30 def WantPager(self, opt):
31 return False
32
30 @property 33 @property
31 def OptionParser(self): 34 def OptionParser(self):
32 if self._optparse is None: 35 if self._optparse is None:
@@ -109,11 +112,15 @@ class InteractiveCommand(Command):
109 """Command which requires user interaction on the tty and 112 """Command which requires user interaction on the tty and
110 must not run within a pager, even if the user asks to. 113 must not run within a pager, even if the user asks to.
111 """ 114 """
115 def WantPager(self, opt):
116 return False
112 117
113class PagedCommand(Command): 118class PagedCommand(Command):
114 """Command which defaults to output in a pager, as its 119 """Command which defaults to output in a pager, as its
115 display tends to be larger than one screen full. 120 display tends to be larger than one screen full.
116 """ 121 """
122 def WantPager(self, opt):
123 return True
117 124
118class MirrorSafeCommand(object): 125class MirrorSafeCommand(object):
119 """Command permits itself to run within a mirror, 126 """Command permits itself to run within a mirror,
diff --git a/main.py b/main.py
index 740fb3a6..6fa1e51b 100755
--- a/main.py
+++ b/main.py
@@ -105,6 +105,8 @@ class _Repo(object):
105 % name 105 % name
106 sys.exit(1) 106 sys.exit(1)
107 107
108 copts, cargs = cmd.OptionParser.parse_args(argv)
109
108 if not gopts.no_pager and not isinstance(cmd, InteractiveCommand): 110 if not gopts.no_pager and not isinstance(cmd, InteractiveCommand):
109 config = cmd.manifest.globalConfig 111 config = cmd.manifest.globalConfig
110 if gopts.pager: 112 if gopts.pager:
@@ -112,11 +114,10 @@ class _Repo(object):
112 else: 114 else:
113 use_pager = config.GetBoolean('pager.%s' % name) 115 use_pager = config.GetBoolean('pager.%s' % name)
114 if use_pager is None: 116 if use_pager is None:
115 use_pager = isinstance(cmd, PagedCommand) 117 use_pager = cmd.WantPager(copts)
116 if use_pager: 118 if use_pager:
117 RunPager(config) 119 RunPager(config)
118 120
119 copts, cargs = cmd.OptionParser.parse_args(argv)
120 try: 121 try:
121 cmd.Execute(copts, cargs) 122 cmd.Execute(copts, cargs)
122 except ManifestInvalidRevisionError, e: 123 except ManifestInvalidRevisionError, e:
diff --git a/subcmds/forall.py b/subcmds/forall.py
index 8a5a41ad..478b3c76 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -13,12 +13,29 @@
13# See the License for the specific language governing permissions and 13# See the License for the specific language governing permissions and
14# limitations under the License. 14# limitations under the License.
15 15
16import fcntl
16import re 17import re
17import os 18import os
19import select
18import sys 20import sys
19import subprocess 21import subprocess
22
23from color import Coloring
20from command import Command, MirrorSafeCommand 24from command import Command, MirrorSafeCommand
21 25
26_CAN_COLOR = [
27 'branch',
28 'diff',
29 'grep',
30 'log',
31]
32
33class ForallColoring(Coloring):
34 def __init__(self, config):
35 Coloring.__init__(self, config, 'forall')
36 self.project = self.printer('project', attr='bold')
37
38
22class Forall(Command, MirrorSafeCommand): 39class Forall(Command, MirrorSafeCommand):
23 common = False 40 common = False
24 helpSummary = "Run a shell command in each project" 41 helpSummary = "Run a shell command in each project"
@@ -28,6 +45,24 @@ class Forall(Command, MirrorSafeCommand):
28 helpDescription = """ 45 helpDescription = """
29Executes the same shell command in each project. 46Executes the same shell command in each project.
30 47
48Output Formatting
49-----------------
50
51The -p option causes '%prog' to bind pipes to the command's stdin,
52stdout and stderr streams, and pipe all output into a continuous
53stream that is displayed in a single pager session. Project headings
54are inserted before the output of each command is displayed. If the
55command produces no output in a project, no heading is displayed.
56
57The formatting convention used by -p is very suitable for some
58types of searching, e.g. `repo forall -p -c git log -SFoo` will
59print all commits that add or remove references to Foo.
60
61The -v option causes '%prog' to display stderr messages if a
62command produces output only on stderr. Normally the -p option
63causes command output to be suppressed until the command produces
64at least one byte of output on stdout.
65
31Environment 66Environment
32----------- 67-----------
33 68
@@ -50,8 +85,8 @@ as written in the manifest.
50shell positional arguments ($1, $2, .., $#) are set to any arguments 85shell positional arguments ($1, $2, .., $#) are set to any arguments
51following <command>. 86following <command>.
52 87
53stdin, stdout, stderr are inherited from the terminal and are 88Unless -p is used, stdin, stdout, stderr are inherited from the
54not redirected. 89terminal and are not redirected.
55""" 90"""
56 91
57 def _Options(self, p): 92 def _Options(self, p):
@@ -65,6 +100,17 @@ not redirected.
65 action='callback', 100 action='callback',
66 callback=cmd) 101 callback=cmd)
67 102
103 g = p.add_option_group('Output')
104 g.add_option('-p',
105 dest='project_header', action='store_true',
106 help='Show project headers before output')
107 g.add_option('-v', '--verbose',
108 dest='verbose', action='store_true',
109 help='Show command error messages')
110
111 def WantPager(self, opt):
112 return opt.project_header
113
68 def Execute(self, opt, args): 114 def Execute(self, opt, args):
69 if not opt.command: 115 if not opt.command:
70 self.Usage() 116 self.Usage()
@@ -79,8 +125,31 @@ not redirected.
79 cmd.append(cmd[0]) 125 cmd.append(cmd[0])
80 cmd.extend(opt.command[1:]) 126 cmd.extend(opt.command[1:])
81 127
128 if opt.project_header \
129 and not shell \
130 and cmd[0] == 'git':
131 # If this is a direct git command that can enable colorized
132 # output and the user prefers coloring, add --color into the
133 # command line because we are going to wrap the command into
134 # a pipe and git won't know coloring should activate.
135 #
136 for cn in cmd[1:]:
137 if not cn.startswith('-'):
138 break
139 if cn in _CAN_COLOR:
140 class ColorCmd(Coloring):
141 def __init__(self, config, cmd):
142 Coloring.__init__(self, config, cmd)
143 if ColorCmd(self.manifest.manifestProject.config, cn).is_on:
144 cmd.insert(cmd.index(cn) + 1, '--color')
145
82 mirror = self.manifest.IsMirror 146 mirror = self.manifest.IsMirror
147 out = ForallColoring(self.manifest.manifestProject.config)
148 out.redirect(sys.stderr)
149
83 rc = 0 150 rc = 0
151 first = True
152
84 for project in self.GetProjects(args): 153 for project in self.GetProjects(args):
85 env = dict(os.environ.iteritems()) 154 env = dict(os.environ.iteritems())
86 def setenv(name, val): 155 def setenv(name, val):
@@ -102,10 +171,76 @@ not redirected.
102 else: 171 else:
103 cwd = project.worktree 172 cwd = project.worktree
104 173
174 if opt.project_header:
175 stdin = subprocess.PIPE
176 stdout = subprocess.PIPE
177 stderr = subprocess.PIPE
178 else:
179 stdin = None
180 stdout = None
181 stderr = None
182
105 p = subprocess.Popen(cmd, 183 p = subprocess.Popen(cmd,
106 cwd = cwd, 184 cwd = cwd,
107 shell = shell, 185 shell = shell,
108 env = env) 186 env = env,
187 stdin = stdin,
188 stdout = stdout,
189 stderr = stderr)
190
191 if opt.project_header:
192 class sfd(object):
193 def __init__(self, fd, dest):
194 self.fd = fd
195 self.dest = dest
196 def fileno(self):
197 return self.fd.fileno()
198
199 empty = True
200 didout = False
201 errbuf = ''
202
203 p.stdin.close()
204 s_in = [sfd(p.stdout, sys.stdout),
205 sfd(p.stderr, sys.stderr)]
206
207 for s in s_in:
208 flags = fcntl.fcntl(s.fd, fcntl.F_GETFL)
209 fcntl.fcntl(s.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
210
211 while s_in:
212 in_ready, out_ready, err_ready = select.select(s_in, [], [])
213 for s in in_ready:
214 buf = s.fd.read(4096)
215 if not buf:
216 s.fd.close()
217 s_in.remove(s)
218 continue
219
220 if not opt.verbose:
221 if s.fd == p.stdout:
222 didout = True
223 else:
224 errbuf += buf
225 continue
226
227 if empty:
228 if first:
229 first = False
230 else:
231 out.nl()
232 out.project('project %s/', project.relpath)
233 out.nl()
234 out.flush()
235 if errbuf:
236 sys.stderr.write(errbuf)
237 sys.stderr.flush()
238 errbuf = ''
239 empty = False
240
241 s.dest.write(buf)
242 s.dest.flush()
243
109 r = p.wait() 244 r = p.wait()
110 if r != 0 and r != rc: 245 if r != 0 and r != rc:
111 rc = r 246 rc = r