mirror of https://github.com/zulip/zulip.git
Import Ksplice code review script
From https://github.com/ksplice/code-review (imported from commit ff6ca29832749ab8f2f6434eb64395a239031f1c)
This commit is contained in:
parent
75c6fa7202
commit
a7fbb1c15f
|
@ -0,0 +1,323 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (C) 2010 Ksplice, Inc.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Author: Greg Price <price@ksplice.com>
|
||||
|
||||
## CC_EMAIL: All review requests will be CC'd here.
|
||||
CC_EMAIL = 'code-review@example.com'
|
||||
|
||||
## DOMAIN: Reviewers without an '@' will be assumed to be localparts here.
|
||||
DOMAIN = 'example.com'
|
||||
|
||||
##### END CONFIG #####
|
||||
## But you might want to change behavior below.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import optparse
|
||||
import posixpath
|
||||
import subprocess
|
||||
import tempfile
|
||||
import git
|
||||
import shlex
|
||||
import codecs
|
||||
from StringIO import StringIO
|
||||
from email.message import Message
|
||||
from email.header import Header
|
||||
|
||||
usage = """
|
||||
%%prog -r <reviewer> [-r <another-reviewer>] [-s <summary>] [-m <message>] [options] {<since>|<revision-range>}
|
||||
|
||||
Send a patch series for review. Sends mail to the reviewer,
|
||||
CC %s, identifying the commits to be reviewed.
|
||||
|
||||
Name a range of commits, or name a single commit (e.g., 'HEAD^' or
|
||||
'origin/master') to identify all commits on HEAD since that commit.
|
||||
""".strip() % CC_EMAIL
|
||||
|
||||
|
||||
# Monkeypatch a bug in git-python.
|
||||
import git.commit, git.repo
|
||||
|
||||
def git__commit__Commit__find_all(cls, repo, ref, path=None, **kwargs):
|
||||
options = {'pretty': 'raw'}
|
||||
options.update(kwargs)
|
||||
|
||||
lpath = [p for p in (path,) if p is not None]
|
||||
output = repo.git.rev_list(ref, '--', *lpath, **options)
|
||||
return cls.list_from_string(repo, output)
|
||||
git.commit.Commit.find_all = classmethod(git__commit__Commit__find_all)
|
||||
|
||||
def git__repo__Repo__commit(self, id, path = None):
|
||||
options = {'max_count': 1}
|
||||
|
||||
commits = git.commit.Commit.find_all(self, id, path, **options)
|
||||
|
||||
if not commits:
|
||||
raise ValueError, 'Invalid identifier %s' % id
|
||||
return commits[0]
|
||||
git.repo.Repo.commit = git__repo__Repo__commit
|
||||
# End monkeypatch.
|
||||
|
||||
|
||||
def check_unicode(option, opt, value):
|
||||
try:
|
||||
return unicode(value, 'utf-8', 'strict')
|
||||
except UnicodeDecodeError:
|
||||
raise optparse.OptionValueError('option %s: invalid UTF-8 string' % opt)
|
||||
|
||||
class MyOption(optparse.Option):
|
||||
TYPES = optparse.Option.TYPES + ('unicode',)
|
||||
TYPE_CHECKER = dict(optparse.Option.TYPE_CHECKER, unicode=check_unicode)
|
||||
|
||||
|
||||
def parse_options(args):
|
||||
parser = optparse.OptionParser(usage, option_class=MyOption)
|
||||
parser.add_option('--first-parent', action='store_true', dest='first_parent',
|
||||
help='follow first parents only')
|
||||
parser.add_option('-r', '--reviewer', type='unicode', dest='reviewers', action="append",
|
||||
help='the person you are asking to do the review')
|
||||
parser.add_option('--stdout', action='store_true', dest='stdout',
|
||||
help='send to standard output rather than send mail')
|
||||
parser.add_option('--format', type='choice', dest='format',
|
||||
choices=['oneline', 'message', 'patch'],
|
||||
help="'patch' (default for one commit), 'message' (default for more), or 'oneline'")
|
||||
parser.add_option('-s', '--summary', type='unicode', dest='summary',
|
||||
help='summary for subject line')
|
||||
parser.add_option('-m', '--message', type='unicode', dest='message',
|
||||
help='message for body of email')
|
||||
parser.add_option('-t', '--testing', type='unicode', dest='testing',
|
||||
help='extent and methods of testing employed')
|
||||
parser.add_option('-e', '--edit', action='store_true', dest='edit',
|
||||
help='spawn $EDITOR and edit review request')
|
||||
options, args = parser.parse_args(args)
|
||||
if not options.reviewers:
|
||||
parser.error('reviewer required')
|
||||
reviewers_fixed = []
|
||||
for reviewer in options.reviewers:
|
||||
if '@' not in reviewer:
|
||||
reviewers_fixed.append(reviewer + '@' + DOMAIN)
|
||||
else:
|
||||
reviewers_fixed.append(reviewer)
|
||||
options.reviewers = reviewers_fixed
|
||||
if len(args) < 2:
|
||||
parser.error('must specify revision(s) to be reviewed')
|
||||
return options, args
|
||||
|
||||
|
||||
def get_default_remote(repo):
|
||||
try:
|
||||
return repo.git.config('--get', 'remotes.default')
|
||||
except git.errors.GitCommandError:
|
||||
try:
|
||||
branch = repo.active_branch
|
||||
return repo.git.config('--get', 'branch.%s.remote' % branch)
|
||||
except git.errors.GitCommandError:
|
||||
return 'origin'
|
||||
|
||||
|
||||
def get_reponame(repo):
|
||||
remote = get_default_remote(repo)
|
||||
|
||||
try:
|
||||
url = repo.git.config('--get', 'remote.%s.url' % remote)
|
||||
except git.errors.GitCommandError:
|
||||
url = repo.wd
|
||||
|
||||
name = posixpath.basename(posixpath.normpath(url.split(':', 1)[-1]))
|
||||
if name.endswith('.git'):
|
||||
name = name[:-len('.git')]
|
||||
return name
|
||||
|
||||
|
||||
def parse_revs(repo, opts, args):
|
||||
args = repo.git.rev_parse(*args).splitlines()
|
||||
if len(args) == 1:
|
||||
args = ['^' + args[0].lstrip('^'), 'HEAD']
|
||||
if opts.first_parent:
|
||||
args[:0] = ['--first-parent']
|
||||
return [repo.commit(c) for c in repo.git.rev_list('--reverse', *args).split()]
|
||||
|
||||
|
||||
def make_header(repo, opts, revs):
|
||||
ident = unicode(repo.git.var('GIT_AUTHOR_IDENT'), 'utf-8', 'replace')
|
||||
me = ident[:ident.rindex('>') + 1]
|
||||
reponame = get_reponame(repo)
|
||||
|
||||
remote = get_default_remote(repo)
|
||||
(sha, name) = repo.git.name_rev(revs[-1].id,
|
||||
refs='refs/remotes/%s/*' % (remote,),
|
||||
always=True).split()
|
||||
prefix = 'remotes/' + remote + "/"
|
||||
if name.startswith(prefix):
|
||||
name = name[len(prefix):]
|
||||
tip_name = '%s (%s)' % (name, revs[-1].id_abbrev)
|
||||
else:
|
||||
print >>sys.stderr, "WARNING: Can't find this commit in remote -- did you push?"
|
||||
tip_name = revs[-1].id_abbrev
|
||||
|
||||
objective_summary = '%d commit(s) to %s' % (len(revs), tip_name)
|
||||
summary = ('%s (%s)' % (opts.summary, objective_summary) if opts.summary
|
||||
else objective_summary)
|
||||
|
||||
return [('From', Header(me)),
|
||||
('To', Header(', '.join(opts.reviewers))),
|
||||
('Cc', Header(CC_EMAIL)),
|
||||
('Subject', Header('%s review: %s' % (reponame, summary)))]
|
||||
|
||||
|
||||
def write_template(target, repo, opts):
|
||||
ident = unicode(repo.git.var('GIT_AUTHOR_IDENT'), 'utf-8', 'replace')
|
||||
me = ident[:ident.rindex('>') + 1]
|
||||
|
||||
print >>target, 'Dear %s,' % ", ".join(opts.reviewers)
|
||||
print >>target
|
||||
print >>target, 'At your convenience, please review the following commits.'
|
||||
print >>target, 'Reply with any comments, or advance master when you are satisfied.'
|
||||
print >>target
|
||||
if opts.message:
|
||||
print >>target, opts.message
|
||||
print >>target
|
||||
print >>target, 'Testing:',
|
||||
if opts.testing:
|
||||
print >>target, opts.testing
|
||||
else:
|
||||
print >>target, '(No formal testing done, or none specified.)'
|
||||
print >>target
|
||||
print >>target, 'Thanks,'
|
||||
print >>target, me
|
||||
|
||||
|
||||
def write_commitmsg(target, repo, opts, revs):
|
||||
|
||||
if opts.format == 'oneline':
|
||||
for r in revs:
|
||||
print >>target, unicode(repo.git.log('-n1', '--oneline', r), 'utf-8', 'replace')
|
||||
elif opts.format == 'message' or opts.format is None and len(revs) > 1:
|
||||
for r in revs:
|
||||
if opts.first_parent:
|
||||
print >>target, unicode(repo.git.log('-n1', r), 'utf-8', 'replace')
|
||||
print >>target, unicode(repo.git.diff('--stat', str(r)+'^', r), 'utf-8', 'replace')
|
||||
else:
|
||||
print >>target, unicode(repo.git.log('-n1', '--stat', r), 'utf-8', 'replace')
|
||||
print >>target
|
||||
elif opts.format == 'patch' or opts.format is None and len(revs) == 1:
|
||||
for r in revs:
|
||||
if opts.first_parent:
|
||||
print >>target, unicode(repo.git.log('-n1', r), 'utf-8', 'replace')
|
||||
print >>target, unicode(repo.git.diff('--stat', '-p', str(r)+'^', r), 'utf-8', 'replace')
|
||||
else:
|
||||
print >>target, unicode(repo.git.log('-n1', '--stat', '-p', r), 'utf-8', 'replace')
|
||||
print >>target
|
||||
else:
|
||||
raise Exception("Bad format option.")
|
||||
|
||||
|
||||
def edit(repo, opts, revs):
|
||||
headers = make_header(repo, opts, revs)
|
||||
|
||||
template = StringIO()
|
||||
commitmsg = StringIO()
|
||||
|
||||
write_template(template, repo, opts)
|
||||
write_commitmsg(commitmsg, repo, opts, revs)
|
||||
|
||||
temp = codecs.getwriter('utf-8')(tempfile.NamedTemporaryFile(prefix="review-"))
|
||||
|
||||
# Prepare editable buffer.
|
||||
|
||||
print >>temp, """# This is an editable review request. All lines beginning with # will
|
||||
# be ignored. To abort the commit, remove all lines from this buffer."""
|
||||
print >>temp, "#"
|
||||
for (key, value) in headers:
|
||||
print >>temp, u"# %s: %s" % (key, value)
|
||||
print >>temp
|
||||
print >>temp, template.getvalue()
|
||||
for line in commitmsg.getvalue().splitlines():
|
||||
print >>temp, "# " + line
|
||||
temp.flush()
|
||||
|
||||
# Open EDITOR to edit buffer.
|
||||
|
||||
editor = os.getenv('EDITOR','emacs')
|
||||
subprocess.check_call(shlex.split(editor) + [temp.name])
|
||||
|
||||
# Check if buffer is empty, and if so abort.
|
||||
|
||||
if (os.path.getsize(temp.name) == 0):
|
||||
print >>sys.stderr, "Aborting due to empty buffer."
|
||||
sys.exit(2)
|
||||
|
||||
# Reopen temp file, slurp it in, and reconstruct mail.
|
||||
|
||||
final = codecs.open(temp.name, 'r', 'utf-8')
|
||||
msg = Message()
|
||||
for (key, value) in headers:
|
||||
msg[key] = value
|
||||
msg.set_payload(
|
||||
("".join(line for line in final if not line.startswith("#")).strip() +
|
||||
"\n\n" + commitmsg.getvalue()).encode('utf-8'),
|
||||
'utf-8')
|
||||
|
||||
# Clean up.
|
||||
|
||||
temp.close()
|
||||
final.close()
|
||||
try:
|
||||
os.unlink(temp.name)
|
||||
except OSError:
|
||||
pass
|
||||
return msg
|
||||
|
||||
|
||||
def main(args):
|
||||
opts, args = parse_options(args)
|
||||
repo = git.Repo()
|
||||
revs = parse_revs(repo, opts, args[1:])
|
||||
if not revs:
|
||||
print >>sys.stderr, '%s: no revisions specified' % os.path.basename(args[0])
|
||||
return 2
|
||||
|
||||
if opts.edit:
|
||||
msg = edit(repo, opts, revs)
|
||||
|
||||
else:
|
||||
# Just build the message.
|
||||
msg = Message()
|
||||
for (key, value) in make_header(repo, opts, revs):
|
||||
msg[key] = value
|
||||
|
||||
template = StringIO()
|
||||
commitmsg = StringIO()
|
||||
|
||||
write_template(template, repo, opts)
|
||||
write_commitmsg(commitmsg, repo, opts, revs)
|
||||
msg.set_payload(
|
||||
(template.getvalue() + "\n" + commitmsg.getvalue()).encode('utf-8'),
|
||||
'utf-8')
|
||||
|
||||
# Send or print the message, as appropriate.
|
||||
if opts.stdout:
|
||||
for (key, value) in msg.items():
|
||||
print >>sys.stdout, u"%s: %s" % (key, value)
|
||||
print >>sys.stdout
|
||||
print >>sys.stdout, msg.get_payload(decode=True),
|
||||
else:
|
||||
subprocess.Popen(['/usr/sbin/sendmail', '-bm', '-t'],
|
||||
stdin=subprocess.PIPE).communicate(msg.as_string())
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
Loading…
Reference in New Issue