Skip to content

Commit 466959c

Browse files
committed
Import new version from internal git.
This corresponds to sha1 2d1e3ab3a716cc248288bfddb132ea5331591e0b. Signed-off-by: Nelson Elhage <nelhage@ksplice.com>
1 parent e83f4ae commit 466959c

File tree

1 file changed

+225
-45
lines changed

1 file changed

+225
-45
lines changed

review

Lines changed: 225 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,15 @@ DOMAIN = 'example.com'
2828
import sys
2929
import os
3030
import optparse
31+
import posixpath
3132
import subprocess
3233
import tempfile
3334
import git
35+
import shlex
36+
import codecs
37+
from StringIO import StringIO
38+
from email.message import Message
39+
from email.header import Header
3440

3541
usage = """
3642
%%prog -r <reviewer> [-r <another-reviewer>] [-s <summary>] [-m <message>] [options] {<since>|<revision-range>}
@@ -43,24 +49,60 @@ Name a range of commits, or name a single commit (e.g., 'HEAD^' or
4349
""".strip() % CC_EMAIL
4450

4551

46-
def show_usage(args):
47-
print >>sys.stderr, usage
48-
return 2
52+
# Monkeypatch a bug in git-python.
53+
import git.commit, git.repo
54+
55+
def git__commit__Commit__find_all(cls, repo, ref, path=None, **kwargs):
56+
options = {'pretty': 'raw'}
57+
options.update(kwargs)
58+
59+
lpath = [p for p in (path,) if p is not None]
60+
output = repo.git.rev_list(ref, '--', *lpath, **options)
61+
return cls.list_from_string(repo, output)
62+
git.commit.Commit.find_all = classmethod(git__commit__Commit__find_all)
63+
64+
def git__repo__Repo__commit(self, id, path = None):
65+
options = {'max_count': 1}
66+
67+
commits = git.commit.Commit.find_all(self, id, path, **options)
68+
69+
if not commits:
70+
raise ValueError, 'Invalid identifier %s' % id
71+
return commits[0]
72+
git.repo.Repo.commit = git__repo__Repo__commit
73+
# End monkeypatch.
74+
75+
76+
def check_unicode(option, opt, value):
77+
try:
78+
return unicode(value, 'utf-8', 'strict')
79+
except UnicodeDecodeError:
80+
raise optparse.OptionValueError('option %s: invalid UTF-8 string' % opt)
81+
82+
class MyOption(optparse.Option):
83+
TYPES = optparse.Option.TYPES + ('unicode',)
84+
TYPE_CHECKER = dict(optparse.Option.TYPE_CHECKER, unicode=check_unicode)
4985

5086

5187
def parse_options(args):
52-
parser = optparse.OptionParser(usage)
53-
parser.add_option('-r', '--reviewer', type='string', dest='reviewers', action="append",
88+
parser = optparse.OptionParser(usage, option_class=MyOption)
89+
parser.add_option('--first-parent', action='store_true', dest='first_parent',
90+
help='follow first parents only')
91+
parser.add_option('-r', '--reviewer', type='unicode', dest='reviewers', action="append",
5492
help='the person you are asking to do the review')
5593
parser.add_option('--stdout', action='store_true', dest='stdout',
5694
help='send to standard output rather than send mail')
5795
parser.add_option('--format', type='choice', dest='format',
5896
choices=['oneline', 'message', 'patch'],
5997
help="'patch' (default for one commit), 'message' (default for more), or 'oneline'")
60-
parser.add_option('-s', '--summary', type='string', dest='summary',
98+
parser.add_option('-s', '--summary', type='unicode', dest='summary',
6199
help='summary for subject line')
62-
parser.add_option('-m', '--message', type='string', dest='message',
100+
parser.add_option('-m', '--message', type='unicode', dest='message',
63101
help='message for body of email')
102+
parser.add_option('-t', '--testing', type='unicode', dest='testing',
103+
help='extent and methods of testing employed')
104+
parser.add_option('-e', '--edit', action='store_true', dest='edit',
105+
help='spawn $EDITOR and edit review request')
64106
options, args = parser.parse_args(args)
65107
if not options.reviewers:
66108
parser.error('reviewer required')
@@ -76,68 +118,206 @@ def parse_options(args):
76118
return options, args
77119

78120

79-
def parse_revs(repo, args):
80-
if len(args) == 1 and '..' not in args[0]:
81-
return list(repo.commits_between(args[0], 'HEAD'))
121+
def get_default_remote(repo):
122+
try:
123+
return repo.git.config('--get', 'remotes.default')
124+
except git.errors.GitCommandError:
125+
try:
126+
branch = repo.active_branch
127+
return repo.git.config('--get', 'branch.%s.remote' % branch)
128+
except git.errors.GitCommandError:
129+
return 'origin'
130+
131+
132+
def get_reponame(repo):
133+
remote = get_default_remote(repo)
134+
135+
try:
136+
url = repo.git.config('--get', 'remote.%s.url' % remote)
137+
except git.errors.GitCommandError:
138+
url = repo.wd
139+
140+
name = posixpath.basename(posixpath.normpath(url.split(':', 1)[-1]))
141+
if name.endswith('.git'):
142+
name = name[:-len('.git')]
143+
return name
144+
145+
146+
def parse_revs(repo, opts, args):
147+
args = repo.git.rev_parse(*args).splitlines()
148+
if len(args) == 1:
149+
args = ['^' + args[0].lstrip('^'), 'HEAD']
150+
if opts.first_parent:
151+
args[:0] = ['--first-parent']
82152
return [repo.commit(c) for c in repo.git.rev_list('--reverse', *args).split()]
83153

84154

85-
def write_mail(out, repo, opts, revs):
86-
ident = repo.git.var('GIT_AUTHOR_IDENT')
155+
def make_header(repo, opts, revs):
156+
ident = unicode(repo.git.var('GIT_AUTHOR_IDENT'), 'utf-8', 'replace')
87157
me = ident[:ident.rindex('>') + 1]
88-
reponame = os.path.basename(repo.wd)
89-
objective_summary = '%d commit(s) to %s' % (len(revs), revs[-1].id_abbrev)
158+
reponame = get_reponame(repo)
159+
160+
remote = get_default_remote(repo)
161+
(sha, name) = repo.git.name_rev(revs[-1].id,
162+
refs='refs/remotes/%s/*' % (remote,),
163+
always=True).split()
164+
prefix = 'remotes/' + remote + "/"
165+
if name.startswith(prefix):
166+
name = name[len(prefix):]
167+
tip_name = '%s (%s)' % (name, revs[-1].id_abbrev)
168+
else:
169+
print >>sys.stderr, "WARNING: Can't find this commit in remote -- did you push?"
170+
tip_name = revs[-1].id_abbrev
171+
172+
objective_summary = '%d commit(s) to %s' % (len(revs), tip_name)
90173
summary = ('%s (%s)' % (opts.summary, objective_summary) if opts.summary
91174
else objective_summary)
92175

93-
print >>out, 'From: ' + me
94-
print >>out, 'To: ' + ', '.join(opts.reviewers)
95-
print >>out, 'Cc: ' + CC_EMAIL
96-
print >>out, 'Subject: %s review: %s' % (reponame, summary)
97-
print >>out
98-
print >>out, 'Dear %s,' % ", ".join(opts.reviewers)
99-
print >>out
100-
print >>out, 'At your convenience, please review the following commits.'
101-
print >>out, 'Reply with any comments, or advance master when you are satisfied.'
102-
print >>out
176+
return [('From', Header(me)),
177+
('To', Header(', '.join(opts.reviewers))),
178+
('Cc', Header(CC_EMAIL)),
179+
('Subject', Header('%s review: %s' % (reponame, summary)))]
180+
181+
182+
def write_template(target, repo, opts):
183+
ident = unicode(repo.git.var('GIT_AUTHOR_IDENT'), 'utf-8', 'replace')
184+
me = ident[:ident.rindex('>') + 1]
185+
186+
print >>target, 'Dear %s,' % ", ".join(opts.reviewers)
187+
print >>target
188+
print >>target, 'At your convenience, please review the following commits.'
189+
print >>target, 'Reply with any comments, or advance master when you are satisfied.'
190+
print >>target
103191
if opts.message:
104-
print >>out, opts.message
105-
print >>out
106-
print >>out, 'Thanks,'
107-
print >>out, me
108-
print >>out
109-
print >>out
192+
print >>target, opts.message
193+
print >>target
194+
print >>target, 'Testing:',
195+
if opts.testing:
196+
print >>target, opts.testing
197+
else:
198+
print >>target, '(No formal testing done, or none specified.)'
199+
print >>target
200+
print >>target, 'Thanks,'
201+
print >>target, me
202+
203+
204+
def write_commitmsg(target, repo, opts, revs):
205+
110206
if opts.format == 'oneline':
111207
for r in revs:
112-
print >>out, repo.git.log('-n1', '--oneline', r)
208+
print >>target, unicode(repo.git.log('-n1', '--oneline', r), 'utf-8', 'replace')
113209
elif opts.format == 'message' or opts.format is None and len(revs) > 1:
114210
for r in revs:
115-
print >>out, repo.git.log('-n1', '--stat', r)
116-
print >>out
211+
if opts.first_parent:
212+
print >>target, unicode(repo.git.log('-n1', r), 'utf-8', 'replace')
213+
print >>target, unicode(repo.git.diff('--stat', str(r)+'^', r), 'utf-8', 'replace')
214+
else:
215+
print >>target, unicode(repo.git.log('-n1', '--stat', r), 'utf-8', 'replace')
216+
print >>target
117217
elif opts.format == 'patch' or opts.format is None and len(revs) == 1:
118-
print >>out, repo.git.show('--stat', '-p', *revs)
218+
for r in revs:
219+
if opts.first_parent:
220+
print >>target, unicode(repo.git.log('-n1', r), 'utf-8', 'replace')
221+
print >>target, unicode(repo.git.diff('--stat', '-p', str(r)+'^', r), 'utf-8', 'replace')
222+
else:
223+
print >>target, unicode(repo.git.log('-n1', '--stat', '-p', r), 'utf-8', 'replace')
224+
print >>target
119225
else:
120-
this_is_an_error
226+
raise Exception("Bad format option.")
227+
228+
229+
def edit(repo, opts, revs):
230+
headers = make_header(repo, opts, revs)
231+
232+
template = StringIO()
233+
commitmsg = StringIO()
234+
235+
write_template(template, repo, opts)
236+
write_commitmsg(commitmsg, repo, opts, revs)
237+
238+
temp = codecs.getwriter('utf-8')(tempfile.NamedTemporaryFile(prefix="review-"))
239+
240+
# Prepare editable buffer.
241+
242+
print >>temp, """# This is an editable review request. All lines beginning with # will
243+
# be ignored. To abort the commit, remove all lines from this buffer."""
244+
print >>temp, "#"
245+
for (key, value) in headers:
246+
print >>temp, u"# %s: %s" % (key, value)
247+
print >>temp
248+
print >>temp, template.getvalue()
249+
for line in commitmsg.getvalue().splitlines():
250+
print >>temp, "# " + line
251+
temp.flush()
252+
253+
# Open EDITOR to edit buffer.
254+
255+
editor = os.getenv('EDITOR','emacs')
256+
subprocess.check_call(shlex.split(editor) + [temp.name])
257+
258+
# Check if buffer is empty, and if so abort.
259+
260+
if (os.path.getsize(temp.name) == 0):
261+
print >>sys.stderr, "Aborting due to empty buffer."
262+
sys.exit(2)
263+
264+
# Reopen temp file, slurp it in, and reconstruct mail.
265+
266+
final = codecs.open(temp.name, 'r', 'utf-8')
267+
msg = Message()
268+
for (key, value) in headers:
269+
msg[key] = value
270+
msg.set_payload(
271+
("".join(line for line in final if not line.startswith("#")).strip() +
272+
"\n\n" + commitmsg.getvalue()).encode('utf-8'),
273+
'utf-8')
274+
275+
# Clean up.
276+
277+
temp.close()
278+
final.close()
279+
try:
280+
os.unlink(temp.name)
281+
except OSError:
282+
pass
283+
return msg
121284

122285

123286
def main(args):
124287
opts, args = parse_options(args)
125288
repo = git.Repo()
126-
revs = parse_revs(repo, args[1:])
289+
revs = parse_revs(repo, opts, args[1:])
127290
if not revs:
128291
print >>sys.stderr, '%s: no revisions specified' % os.path.basename(args[0])
129292
return 2
293+
294+
if opts.edit:
295+
msg = edit(repo, opts, revs)
296+
297+
else:
298+
# Just build the message.
299+
msg = Message()
300+
for (key, value) in make_header(repo, opts, revs):
301+
msg[key] = value
302+
303+
template = StringIO()
304+
commitmsg = StringIO()
305+
306+
write_template(template, repo, opts)
307+
write_commitmsg(commitmsg, repo, opts, revs)
308+
msg.set_payload(
309+
(template.getvalue() + "\n" + commitmsg.getvalue()).encode('utf-8'),
310+
'utf-8')
311+
312+
# Send or print the message, as appropriate.
130313
if opts.stdout:
131-
out = sys.stdout
314+
for (key, value) in msg.items():
315+
print >>sys.stdout, u"%s: %s" % (key, value)
316+
print >>sys.stdout
317+
print >>sys.stdout, msg.get_payload(decode=True),
132318
else:
133-
outfd, outname = tempfile.mkstemp(prefix='review-')
134-
out = file(outname, 'w')
135-
write_mail(out, repo, opts, revs)
136-
if not opts.stdout:
137-
out.close()
138-
subprocess.check_call(['/usr/sbin/sendmail', '-bm', '-t'],
139-
stdin=file(outname))
140-
319+
subprocess.Popen(['/usr/sbin/sendmail', '-bm', '-t'],
320+
stdin=subprocess.PIPE).communicate(msg.as_string())
141321

142322
if __name__ == '__main__':
143323
sys.exit(main(sys.argv))

0 commit comments

Comments
 (0)