"""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 operator, os
import messages
from api import *
from mercurial import templatefilters, util
from mercurial.node import short
def _init_command(ui, repo, **opts):
ui.note(messages.INIT_START)
try:
ReviewDatastore(ui, repo, lpath=opts.pop('local_path'),
rpath=opts.pop('remote_path'), create=True)
ui.status(messages.INIT_SUCCESS)
return
except PreexistingDatastore, e:
if e.committed:
ui.note(messages.INIT_EXISTS_COMMITTED)
else:
raise util.Abort(messages.INIT_EXISTS_UNCOMMITTED)
def _comment_command(ui, repo, *fnames, **opts):
rev = opts.pop('rev')
filename = fnames[0] if fnames else ''
message = opts.pop('message')
lines = opts.pop('lines')
rd = ReviewDatastore(ui, repo)
rcset = rd[rev]
if filename:
filename = sanitize_path(filename, repo)
if lines and not filename:
raise util.Abort(messages.COMMENT_LINES_REQUIRE_FILE)
if not message:
raise util.Abort(messages.COMMENT_REQUIRES_MESSAGE)
if lines:
lines=lines.split(',')
try:
rcset.add_comment(message=message, filename=filename, lines=lines)
except FileNotInChangeset:
raise util.Abort(
messages.COMMENT_FILE_DOES_NOT_EXIST % (filename, 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'))
rd = ReviewDatastore(ui, repo)
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 _print_comment(comment, before='', after=''):
ui.write(before)
author = templatefilters.person(comment.author)
author_part = messages.REVIEW_LOG_COMMENT_AUTHOR % author
age = templatefilters.age(comment.hgdate)
age_part = messages.REVIEW_LOG_COMMENT_AGE % age
spacing = ' ' * (80 - (len(author_part) + len(age_part)))
ui.write(author_part + spacing + age_part + '\n')
for line in comment.message.splitlines():
ui.write(messages.REVIEW_LOG_COMMENT_LINE % line)
ui.write(after)
review_level_comments = filter(lambda c: not c.filename, rcset.comments)
if review_level_comments:
ui.write('\n')
for comment in review_level_comments:
_print_comment(comment, before='\n')
fnames = [sanitize_path(fname, repo) for fname in fnames]
diffs = rcset.diffs(fnames, context)
for filename, diff in diffs.iteritems():
max_line = diff['max']
content = diff['content']
header = messages.REVIEW_LOG_FILE_HEADER % filename
print '\n\n%s %s' % (header, '-'*(80-(len(header)+1)))
file_level_comments = filter(
lambda c: filename == c.filename and not c.lines, rcset.comments
)
for comment in file_level_comments:
_print_comment(comment)
line_level_comments = filter(
lambda c: filename == c.filename and c.lines, rcset.comments
)
prefix = '%%%dd: ' % len(str(content[-1][0]))
previous_n = -1
for n, line in content:
if n - 1 > previous_n:
skipped = n - previous_n
if previous_n == -1:
skipped -= 1
ui.write(messages.REVIEW_LOG_SKIPPED % skipped)
skipped_comments = filter(
lambda c: max(c.lines) in range(previous_n + 1, n),
line_level_comments
)
for comment in skipped_comments:
_print_comment(comment)
ui.write('%s %s\n' % (prefix % n, line))
line_comments = filter(
lambda c: max(c.lines) == n, line_level_comments
)
for comment in line_comments:
_print_comment(comment)
previous_n = n
if previous_n < max_line:
skipped = max_line - previous_n
ui.write(messages.REVIEW_LOG_SKIPPED % skipped)
def review(ui, repo, *fnames, **opts):
"""code review a changeset in the current repository
"""
if 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, [
('i', 'init', False, 'start code reviewing this repository'),
('', 'local-path', '', 'the local path to the code review data'),
('', 'remote-path', '', 'the remote path to code review data'),
('c', 'comment', False, 'add a comment'),
('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'),
('m', 'message', '', 'use <text> as the comment or signoff message'),
('', 'force', False, 'overwrite an existing signoff'),
('f', 'file', '', 'comment on <file>'),
('r', 'rev', '.', 'the revision to review'),
('l', 'lines', '', 'the line(s) of the file to comment on'),
('U', 'unified', '5', 'number of lines of context to show'),
],
'hg review')
}