"""The review extension's command-line UI.
This module is imported in __init__.py so that Mercurial will add the
review command to its own UI when you add the extension in ~/.hgrc.
"""
import re
import helps, messages
from api import *
from mercurial import help, templatefilters, util
from mercurial.node import short
from mercurial import extensions
def _web_command(ui, repo, **opts):
ui.note(messages.WEB_START)
read_only = opts.pop('read_only')
address = opts.pop('address')
port = int(opts.pop('port'))
import web_ui
web_ui.load_interface(
ui, repo, read_only=read_only,
address=address, port=port,
open=False
)
def _init_command(ui, repo, **opts):
ui.note(messages.INIT_START)
try:
ReviewDatastore(ui, repo, rpath=opts.pop('remote_path'), create=True)
if '.hgreview' not in repo['tip'].manifest():
ui.status(messages.INIT_SUCCESS_UNCOMMITTED)
else:
ui.status(messages.INIT_SUCCESS_CLONED)
except RelativeRemotePath:
raise util.Abort(messages.INIT_UNSUPPORTED_RELATIVE_RPATH)
except DatastoreRequiresRemotePath:
raise util.Abort(messages.INIT_REQUIRES_REMOTE_PATH)
except PreexistingDatastore, e:
if e.committed:
ui.note(messages.INIT_EXISTS)
else:
raise util.Abort(messages.INIT_EXISTS_UNCOMMITTED)
def _comment_command(ui, repo, *fnames, **opts):
rev = opts.pop('rev')
message = opts.pop('message')
lines = opts.pop('lines')
rd = ReviewDatastore(ui, repo)
rcset = rd[rev]
if lines and not len(fnames) == 1:
raise util.Abort(messages.COMMENT_LINES_REQUIRE_FILE)
if not message:
raise util.Abort(messages.COMMENT_REQUIRES_MESSAGE)
if lines:
lines=lines.split(',')
if fnames:
fnames = map(lambda f: sanitize_path(f, repo), fnames)
else:
fnames = ['']
for fn in fnames:
try:
rcset.add_comment(message=message, filename=fn, lines=lines)
except FileNotInChangeset:
raise util.Abort(
messages.COMMENT_FILE_DOES_NOT_EXIST % (fn, repo[rev].rev())
)
def _signoff_command(ui, repo, **opts):
rd = ReviewDatastore(ui, repo)
rcset = rd[opts.pop('rev')]
message = opts.pop('message')
if not message:
raise util.Abort(messages.SIGNOFF_REQUIRES_MESSAGE)
yes, no = opts.pop('yes'), opts.pop('no')
if yes and no:
raise util.Abort(messages.SIGNOFF_OPINION_CONFLICT)
opinion = 'yes' if yes else ('no' if no else '')
try:
rcset.add_signoff(message=message, opinion=opinion,
force=opts.pop('force'))
except SignoffExists:
raise util.Abort(messages.SIGNOFF_EXISTS)
def _review_command(ui, repo, *fnames, **opts):
rev = opts.pop('rev')
context = int(opts.pop('unified'))
try:
rd = ReviewDatastore(ui, repo)
except UninitializedDatastore:
raise util.Abort(messages.NO_DATA_STORE)
cset = repo[rev]
rcset = rd[rev]
comment_count = len(rcset.comments)
author_count = len(set(comment.author for comment in rcset.comments))
ui.write(messages.REVIEW_LOG_CSET % (cset.rev(), short(cset.node())))
ui.write(messages.REVIEW_LOG_AUTHOR % cset.user())
ui.write(messages.REVIEW_LOG_SUMMARY % cset.description().split('\n')[0])
signoffs = rcset.signoffs
signoffs_yes = filter(lambda s: s.opinion == 'yes', signoffs)
signoffs_no = filter(lambda s: s.opinion == 'no', signoffs)
signoffs_neutral = set(signoffs).difference(signoffs_yes + signoffs_no)
ui.write(messages.REVIEW_LOG_SIGNOFFS % (
len(signoffs), len(signoffs_yes), len(signoffs_no), len(signoffs_neutral))
)
ui.write(messages.REVIEW_LOG_COMMENTS % (comment_count, author_count))
def _build_item_header(item, author_template, author_extra=None):
author = templatefilters.person(item.author)
author_args = (author,)
if author_extra:
author_args = author_args + author_extra
author_part = author_template % author_args
age = templatefilters.age(item.hgdate)
age_part = messages.REVIEW_LOG_AGE % age
if ui.debugflag:
hash_part = messages.REVIEW_LOG_IDENTIFIER % item.identifier
elif ui.verbose:
hash_part = messages.REVIEW_LOG_IDENTIFIER % item.identifier[:12]
else:
hash_part = ''
detail_part = age_part + hash_part
spacing = 80 - (len(author_part) + len(detail_part))
if spacing <= 0:
spacing = 1
spacing = ' ' * spacing
return author_part + spacing + detail_part + '\n'
def _print_comment(comment, before='', after=''):
ui.write(before)
ui.write(_build_item_header(comment, messages.REVIEW_LOG_COMMENT_AUTHOR))
for line in comment.message.splitlines():
ui.write(messages.REVIEW_LOG_COMMENT_LINE % line)
ui.write(after)
def _print_signoff(signoff, before='', after=''):
ui.write(before)
opinion = signoff.opinion or 'neutral'
ui.write(_build_item_header(signoff, messages.REVIEW_LOG_SIGNOFF_AUTHOR, (opinion,)))
for line in signoff.message.splitlines():
ui.write(messages.REVIEW_LOG_SIGNOFF_LINE % line)
ui.write(after)
if rcset.signoffs:
ui.write('\n')
for signoff in rcset.signoffs:
_print_signoff(signoff, before='\n')
review_level_comments = rcset.review_level_comments()
if review_level_comments:
ui.write('\n')
for comment in review_level_comments:
_print_comment(comment, before='\n')
if ui.quiet:
return
if not fnames:
fnames = rcset.files()
fnames = [sanitize_path(fname, repo) for fname in fnames]
fnames = [fname for fname in fnames if rcset.has_diff(fname)]
for filename in fnames:
header = messages.REVIEW_LOG_FILE_HEADER % filename
print '\n\n%s %s' % (header, '-'*(80-(len(header)+1)))
for comment in rcset.file_level_comments(filename):
_print_comment(comment)
annotated_diff = rcset.annotated_diff(filename, context)
prefix = '%%%dd: ' % len(str(annotated_diff.next()))
for line in annotated_diff:
if line['skipped']:
ui.write(messages.REVIEW_LOG_SKIPPED % line['skipped'])
for comment in line['comments']:
_print_comment(comment)
continue
ui.write('%s %s\n' % (prefix % line['number'], line['content']))
for comment in line['comments']:
_print_comment(comment)
_review_effects = {
'deleted': ['red'],
'inserted': ['green'],
'comments': ['cyan'],
'signoffs': ['yellow'],
}
_review_re = [
(re.compile(r'^(?P<rest> *\d+: )(?P<colorized>[-].*)'), 'deleted'),
(re.compile(r'^(?P<rest> *\d+: )(?P<colorized>[+].*)'), 'inserted'),
(re.compile(r'^(?P<colorized>#.*)'), 'comments'),
(re.compile(r'^(?P<colorized>\$.*)'), 'signoffs'),
]
def colorwrap(orig, *args):
'''wrap ui.write for colored diff output'''
def _colorize(s):
lines = s.split('\n')
for i, line in enumerate(lines):
if not line:
continue
else:
for r, style in _review_re:
m = r.match(line)
if m:
lines[i] = "%s%s" % (m.groupdict().get('rest', ''),
render_effects(m.group('colorized'), _review_effects[style]))
break
return '\n'.join(lines)
orig(*[_colorize(s) for s in args])
def colorreview(orig, ui, repo, *fnames, **opts):
'''colorize review command output'''
oldwrite = extensions.wrapfunction(ui, 'write', colorwrap)
try:
orig(ui, repo, *fnames, **opts)
finally:
ui.write = oldwrite
_ui = None
def uisetup(ui):
global _ui
_ui = ui
def extsetup():
try:
color = extensions.find('color')
color._setupcmd(_ui, 'review', cmdtable, colorreview,
_review_effects)
global render_effects
render_effects = color.render_effects
except KeyError:
pass
def review(ui, repo, *fnames, **opts):
"""code review changesets in the current repository
To start using the review extension with a repository, you need to
initialize the code review data:
hg help review-init
Once you've initialized it (and cloned the review data repo to a
place where others can get to it), you can start reviewing changesets.
See the following help topics if you want to use the command-line
interface:
hg help review-review
hg help review-comment
hg help review-signoff
Once you've reviewed some changesets, don't forget to push your
comments and signoffs so other people can view them.
"""
if opts.pop('web'):
return _web_command(ui, repo, **opts)
elif opts.pop('init'):
return _init_command(ui, repo, **opts)
elif opts.pop('comment'):
return _comment_command(ui, repo, *fnames, **opts)
elif opts.pop('signoff'):
return _signoff_command(ui, repo, **opts)
else:
return _review_command(ui, repo, *fnames, **opts)
cmdtable = {
'review': (review, [
('U', 'unified', '5', 'number of lines of context to show'),
('m', 'message', '', 'use <text> as the comment or signoff message'),
('r', 'rev', '.', 'the revision to review'),
('', 'check', False, 'check the review status of the given revision'),
('i', 'init', False, 'start code reviewing this repository'),
('', 'remote-path', '', 'the remote path to code review data'),
('c', 'comment', False, 'add a comment'),
('l', 'lines', '', 'the line(s) of the file to comment on'),
('s', 'signoff', False, 'sign off'),
('', 'yes', False, 'sign off as stating the changeset is good'),
('', 'no', False, 'sign off as stating the changeset is bad'),
('', 'force', False, 'overwrite an existing signoff'),
('w', 'web', False, 'launch the web interface'),
('', 'read-only', False, 'make the web interface read-only'),
('', 'address', '127.0.0.1', 'run the web interface on the specified address'),
('', 'port', '8080', 'run the web interface on the specified port'),
],
'hg review')
}
help.helptable += (
(['review-init'], ('Initializing code review for a repository'), (helps.INIT)),
(['review-review'], ('Viewing code review data for changesets'), (helps.REVIEW)),
(['review-comment'], ('Adding code review comments for changesets'), (helps.COMMENT)),
(['review-signoff'], ('Adding code review signoffs for changesets'), (helps.SIGNOFF)),
)