# HG changeset patch # User Steve Losh # Date 1276648850 14400 # Node ID 34df61cd0a47fdf4e37c1c0c5538219a881828c8 # Parent d5280b38dd4c0908a9c573dd535ea81891fdb00f guts: rename some files to make things cleaner diff -r d5280b38dd4c -r 34df61cd0a47 contrib/deploy/wsgi.py --- a/contrib/deploy/wsgi.py Tue Jun 15 19:15:24 2010 -0400 +++ b/contrib/deploy/wsgi.py Tue Jun 15 20:40:50 2010 -0400 @@ -14,7 +14,7 @@ TITLE = 'Your Project' from mercurial import hg, ui -from web_ui import app +from web import app _ui = ui.ui() _ui.setconfig('ui', 'user', ANON_USER) diff -r d5280b38dd4c -r 34df61cd0a47 review/__init__.py --- a/review/__init__.py Tue Jun 15 19:15:24 2010 -0400 +++ b/review/__init__.py Tue Jun 15 20:40:50 2010 -0400 @@ -1,3 +1,3 @@ """commands for code reviewing changesets""" -from extension_ui import * \ No newline at end of file +from cli import * diff -r d5280b38dd4c -r 34df61cd0a47 review/api.py --- a/review/api.py Tue Jun 15 19:15:24 2010 -0400 +++ b/review/api.py Tue Jun 15 20:40:50 2010 -0400 @@ -3,7 +3,7 @@ """The API for interacting with code review data.""" import datetime, operator, os -import file_templates, messages +import files, messages from mercurial import cmdutil, error, hg, patch, util from mercurial.node import hex from mercurial import ui as _ui @@ -708,7 +708,7 @@ """ rendered_date = util.datestr(self.hgdate) lines = ','.join(self.lines) - return file_templates.COMMENT_FILE_TEMPLATE % ( self.author, rendered_date, + return files.COMMENT_FILE_TEMPLATE % ( self.author, rendered_date, self.node, self.filename, lines, self.message ) def __str__(self): @@ -780,7 +780,7 @@ """ rendered_date = util.datestr(self.hgdate) - return file_templates.SIGNOFF_FILE_TEMPLATE % ( self.author, rendered_date, + return files.SIGNOFF_FILE_TEMPLATE % ( self.author, rendered_date, self.node, self.opinion, self.message ) diff -r d5280b38dd4c -r 34df61cd0a47 review/cli.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/review/cli.py Tue Jun 15 20:40:50 2010 -0400 @@ -0,0 +1,367 @@ +"""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 api, helps, messages +from mercurial import help, templatefilters, util +from mercurial.node import short +from mercurial import extensions + + +def _get_message(ui, rd, initial): + message = ui.edit(initial, rd.repo.ui.username()) + return '\n'.join(l for l in message.splitlines() + if not l.startswith('HG: ')).strip() + + +def _web_command(ui, repo, **opts): + ui.note(messages.WEB_START) + read_only = opts.pop('read_only') + allow_anon = opts.pop('allow_anon') + address = opts.pop('address') + port = int(opts.pop('port')) + + import web + web.load_interface(ui, repo, read_only=read_only, allow_anon=allow_anon, + address=address, port=port, open=False) + +def _init_command(ui, repo, **opts): + ui.note(messages.INIT_START) + + try: + api.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 api.RelativeRemotePath: + raise util.Abort(messages.INIT_UNSUPPORTED_RELATIVE_RPATH) + except api.DatastoreRequiresRemotePath: + raise util.Abort(messages.INIT_REQUIRES_REMOTE_PATH) + except api.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') + lines = opts.pop('lines') + message = opts.pop('message').strip() + + rd = api.ReviewDatastore(ui, repo) + rcset = rd[rev] + + if lines and not len(fnames) == 1: + raise util.Abort(messages.COMMENT_LINES_REQUIRE_FILE) + + if lines: + lines = lines.split(',') + + fnames = map(lambda f: api.sanitize_path(f, repo), fnames) if fnames else [''] + + if not message: + message = _get_message(ui, rd, messages.COMMENT_EDITOR_MESSAGE) + if not message: + raise util.Abort(messages.COMMENT_REQUIRES_MESSAGE) + + for fn in fnames: + try: + rcset.add_comment(message=message, filename=fn, lines=lines) + except api.FileNotInChangeset: + raise util.Abort(messages.COMMENT_FILE_DOES_NOT_EXIST % ( + fn, repo[rev].rev())) + +def _signoff_command(ui, repo, **opts): + rd = api.ReviewDatastore(ui, repo) + rcset = rd[opts.pop('rev')] + message = opts.pop('message').strip() + force = opts.pop('force') + + 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 '') + + if rcset.signoffs_for_current_user() and not force: + raise util.Abort(messages.SIGNOFF_EXISTS) + + if not message: + message = _get_message(ui, rd, messages.SIGNOFF_EDITOR_MESSAGE) + if not message: + raise util.Abort(messages.SIGNOFF_REQUIRES_MESSAGE) + + rcset.add_signoff(message=message, opinion=opinion, force=force) + +def _check_command(ui, repo, **opts): + rd = api.ReviewDatastore(ui, repo) + rcset = rd[opts.pop('rev')] + + if opts.pop('no_nos'): + if any(filter(lambda s: s.opinion == "no", rcset.signoffs)): + raise util.Abort(messages.CHECK_HAS_NOS) + + yes_count = opts.pop('yeses') + if yes_count: + yes_count = int(yes_count) + if len(filter(lambda s: s.opinion == "yes", rcset.signoffs)) < yes_count: + raise util.Abort(messages.CHECK_TOO_FEW_YESES) + + if opts.pop('seen'): + if not rcset.signoffs and not rcset.comments: + raise util.Abort(messages.CHECK_UNSEEN) + + ui.note(messages.CHECK_SUCCESS) + +def _review_command(ui, repo, *fnames, **opts): + rev = opts.pop('rev') + context = int(opts.pop('unified')) + + try: + rd = api.ReviewDatastore(ui, repo) + except api.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 = [api.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 *\d+: )(?P[-].*)'), 'deleted'), + (re.compile(r'^(?P *\d+: )(?P[+].*)'), 'inserted'), + (re.compile(r'^(?P#.*)'), 'comments'), + (re.compile(r'^(?P\$.*)'), '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 + - hg help review-check + + 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) + elif opts.pop('check'): + return _check_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 as the comment or signoff message'), + ('r', 'rev', '.', 'the revision to review'), + + ('', 'check', False, 'check the review status of the given revision'), + ('', 'no-nos', False, 'ensure this revision does NOT have signoffs of "no"'), + ('', 'yeses', '', 'ensure this revision has at least NUM signoffs of "yes"'), + ('', 'seen', False, 'ensure this revision has a comment or signoff'), + + ('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'), + ('f', 'force', False, 'overwrite an existing signoff'), + + ('w', 'web', False, 'launch the web interface'), + ('', 'read-only', False, 'make the web interface read-only'), + ('', 'allow-anon', False, 'allow anonymous comments on the web interface'), + ('', '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)), + (['review-check'], ('Checking the review status of changesets'), (helps.CHECK)), +) diff -r d5280b38dd4c -r 34df61cd0a47 review/extension_ui.py --- a/review/extension_ui.py Tue Jun 15 19:15:24 2010 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,367 +0,0 @@ -"""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 api, helps, messages -from mercurial import help, templatefilters, util -from mercurial.node import short -from mercurial import extensions - - -def _get_message(ui, rd, initial): - message = ui.edit(initial, rd.repo.ui.username()) - return '\n'.join(l for l in message.splitlines() - if not l.startswith('HG: ')).strip() - - -def _web_command(ui, repo, **opts): - ui.note(messages.WEB_START) - read_only = opts.pop('read_only') - allow_anon = opts.pop('allow_anon') - address = opts.pop('address') - port = int(opts.pop('port')) - - import web_ui - web_ui.load_interface(ui, repo, read_only=read_only, allow_anon=allow_anon, - address=address, port=port, open=False) - -def _init_command(ui, repo, **opts): - ui.note(messages.INIT_START) - - try: - api.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 api.RelativeRemotePath: - raise util.Abort(messages.INIT_UNSUPPORTED_RELATIVE_RPATH) - except api.DatastoreRequiresRemotePath: - raise util.Abort(messages.INIT_REQUIRES_REMOTE_PATH) - except api.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') - lines = opts.pop('lines') - message = opts.pop('message').strip() - - rd = api.ReviewDatastore(ui, repo) - rcset = rd[rev] - - if lines and not len(fnames) == 1: - raise util.Abort(messages.COMMENT_LINES_REQUIRE_FILE) - - if lines: - lines = lines.split(',') - - fnames = map(lambda f: api.sanitize_path(f, repo), fnames) if fnames else [''] - - if not message: - message = _get_message(ui, rd, messages.COMMENT_EDITOR_MESSAGE) - if not message: - raise util.Abort(messages.COMMENT_REQUIRES_MESSAGE) - - for fn in fnames: - try: - rcset.add_comment(message=message, filename=fn, lines=lines) - except api.FileNotInChangeset: - raise util.Abort(messages.COMMENT_FILE_DOES_NOT_EXIST % ( - fn, repo[rev].rev())) - -def _signoff_command(ui, repo, **opts): - rd = api.ReviewDatastore(ui, repo) - rcset = rd[opts.pop('rev')] - message = opts.pop('message').strip() - force = opts.pop('force') - - 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 '') - - if rcset.signoffs_for_current_user() and not force: - raise util.Abort(messages.SIGNOFF_EXISTS) - - if not message: - message = _get_message(ui, rd, messages.SIGNOFF_EDITOR_MESSAGE) - if not message: - raise util.Abort(messages.SIGNOFF_REQUIRES_MESSAGE) - - rcset.add_signoff(message=message, opinion=opinion, force=force) - -def _check_command(ui, repo, **opts): - rd = api.ReviewDatastore(ui, repo) - rcset = rd[opts.pop('rev')] - - if opts.pop('no_nos'): - if any(filter(lambda s: s.opinion == "no", rcset.signoffs)): - raise util.Abort(messages.CHECK_HAS_NOS) - - yes_count = opts.pop('yeses') - if yes_count: - yes_count = int(yes_count) - if len(filter(lambda s: s.opinion == "yes", rcset.signoffs)) < yes_count: - raise util.Abort(messages.CHECK_TOO_FEW_YESES) - - if opts.pop('seen'): - if not rcset.signoffs and not rcset.comments: - raise util.Abort(messages.CHECK_UNSEEN) - - ui.note(messages.CHECK_SUCCESS) - -def _review_command(ui, repo, *fnames, **opts): - rev = opts.pop('rev') - context = int(opts.pop('unified')) - - try: - rd = api.ReviewDatastore(ui, repo) - except api.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 = [api.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 *\d+: )(?P[-].*)'), 'deleted'), - (re.compile(r'^(?P *\d+: )(?P[+].*)'), 'inserted'), - (re.compile(r'^(?P#.*)'), 'comments'), - (re.compile(r'^(?P\$.*)'), '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 - - hg help review-check - - 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) - elif opts.pop('check'): - return _check_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 as the comment or signoff message'), - ('r', 'rev', '.', 'the revision to review'), - - ('', 'check', False, 'check the review status of the given revision'), - ('', 'no-nos', False, 'ensure this revision does NOT have signoffs of "no"'), - ('', 'yeses', '', 'ensure this revision has at least NUM signoffs of "yes"'), - ('', 'seen', False, 'ensure this revision has a comment or signoff'), - - ('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'), - ('f', 'force', False, 'overwrite an existing signoff'), - - ('w', 'web', False, 'launch the web interface'), - ('', 'read-only', False, 'make the web interface read-only'), - ('', 'allow-anon', False, 'allow anonymous comments on the web interface'), - ('', '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)), - (['review-check'], ('Checking the review status of changesets'), (helps.CHECK)), -) diff -r d5280b38dd4c -r 34df61cd0a47 review/file_templates.py --- a/review/file_templates.py Tue Jun 15 19:15:24 2010 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,18 +0,0 @@ -"""Templates for hg-review's data files.""" - -COMMENT_FILE_TEMPLATE = """\ -author:%s -hgdate:%s -node:%s -filename:%s -lines:%s - -%s""" - -SIGNOFF_FILE_TEMPLATE = """\ -author:%s -hgdate:%s -node:%s -opinion:%s - -%s""" \ No newline at end of file diff -r d5280b38dd4c -r 34df61cd0a47 review/files.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/review/files.py Tue Jun 15 20:40:50 2010 -0400 @@ -0,0 +1,18 @@ +"""Templates for hg-review's data files.""" + +COMMENT_FILE_TEMPLATE = """\ +author:%s +hgdate:%s +node:%s +filename:%s +lines:%s + +%s""" + +SIGNOFF_FILE_TEMPLATE = """\ +author:%s +hgdate:%s +node:%s +opinion:%s + +%s""" \ No newline at end of file diff -r d5280b38dd4c -r 34df61cd0a47 review/messages.py --- a/review/messages.py Tue Jun 15 19:15:24 2010 -0400 +++ b/review/messages.py Tue Jun 15 20:40:50 2010 -0400 @@ -1,8 +1,8 @@ """Messages used by the command-line UI of hg-review. -These are kept in a separate module to avoid repeating them over and over -in the extension_ui module, and to make checking for proper output in the -unit tests much easier. +These are kept in a separate module to avoid repeating them over and over in +the cli module, and to make checking for proper output in the unit tests much +easier. """ NO_DATA_STORE = """\ diff -r d5280b38dd4c -r 34df61cd0a47 review/tests/util.py --- a/review/tests/util.py Tue Jun 15 19:15:24 2010 -0400 +++ b/review/tests/util.py Tue Jun 15 20:40:50 2010 -0400 @@ -5,7 +5,7 @@ import os, shutil import sample_data from mercurial import cmdutil, commands, hg, ui -from .. import api, extension_ui +from .. import api, cli _ui = ui.ui() _ui.setconfig('extensions', 'progress', '!') @@ -22,8 +22,7 @@ _ui.debugflag = True elif verbose: _ui.verbose = True - extension_ui.review( - _ui, get_sandbox_repo(), *files, + cli.review(_ui, get_sandbox_repo(), *files, **dict( init=init, comment=comment, signoff=signoff, check=check, yes=yes, no=no, force=force, message=message, rev=rev, remote_path=remote_path, diff -r d5280b38dd4c -r 34df61cd0a47 review/web.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/review/web.py Tue Jun 15 20:40:50 2010 -0400 @@ -0,0 +1,187 @@ +from __future__ import with_statement + +"""The review extension's web UI.""" + +import sys, os +from hashlib import md5 + +from mercurial import commands, hg, templatefilters +from mercurial.node import short +from mercurial.util import email + +import api + +def unbundle(): + package_path = os.path.split(os.path.realpath(__file__))[0] + template_path = os.path.join(package_path, 'web_templates') + media_path = os.path.join(package_path, 'web_media') + top_path = os.path.split(package_path)[0] + bundled_path = os.path.join(top_path, 'bundled') + flask_path = os.path.join(bundled_path, 'flask') + jinja2_path = os.path.join(bundled_path, 'jinja2') + werkzeug_path = os.path.join(bundled_path, 'werkzeug') + simplejson_path = os.path.join(bundled_path, 'simplejson') + + sys.path.insert(0, flask_path) + sys.path.insert(0, werkzeug_path) + sys.path.insert(0, jinja2_path) + sys.path.insert(0, simplejson_path) + +unbundle() + +from flask import Flask +from flask import abort, g, redirect, render_template, request +app = Flask(__name__) + +LOG_PAGE_LEN = 15 + +def _item_gravatar(item, size=30): + return 'http://www.gravatar.com/avatar/%s?s=%d' % (md5(email(item.author)).hexdigest(), size) + +def _cset_gravatar(cset, size=30): + return 'http://www.gravatar.com/avatar/%s?s=%d' % (md5(email(cset.user())).hexdigest(), size) + +def _line_type(line): + return 'rem' if line[0] == '-' else 'add' if line[0] == '+' else 'con' + +def _categorize_signoffs(signoffs): + return { 'yes': len(filter(lambda s: s.opinion == 'yes', signoffs)), + 'no': len(filter(lambda s: s.opinion == 'no', signoffs)), + 'neutral': len(filter(lambda s: s.opinion == '', signoffs)),} +utils = { + 'node_short': short, + 'md5': md5, + 'email': email, + 'templatefilters': templatefilters, + 'len': len, + 'item_gravatar': _item_gravatar, + 'cset_gravatar': _cset_gravatar, + 'line_type': _line_type, + 'categorize_signoffs': _categorize_signoffs, + 'map': map, + 'str': str, + 'decode': lambda s: s.decode('utf-8'), +} + +def _render(template, **kwargs): + return render_template(template, read_only=app.read_only, + allow_anon=app.allow_anon, utils=utils, datastore=g.datastore, + title=app.title, **kwargs) + + +@app.before_request +def load_datastore(): + g.datastore = api.ReviewDatastore(app.ui, hg.repository(app.ui, app.repo.root)) + +@app.route('/') +def index_newest(): + return index(-1) + +@app.route('//') +def index(rev_max): + tip = g.datastore.target['tip'].rev() + + if rev_max > tip or rev_max < 0: + rev_max = tip + + rev_min = rev_max - LOG_PAGE_LEN if rev_max >= LOG_PAGE_LEN else 0 + if rev_min < 0: + rev_min = 0 + + older = rev_min - 1 if rev_min > 0 else -1 + newer = rev_max + LOG_PAGE_LEN + 1 if rev_max < tip else -1 + if newer > tip: + newer = tip + + rcsets = [g.datastore[r] for r in xrange(rev_max, rev_min - 1, -1)] + return _render('index.html', rcsets=rcsets, newer=newer, older=older) + + +def _handle_signoff(revhash): + signoff = request.form.get('signoff', None) + + if signoff not in ['yes', 'no', 'neutral']: + abort(400) + + if signoff == 'neutral': + signoff = '' + + body = request.form.get('new-signoff-body', '') + rcset = g.datastore[revhash] + rcset.add_signoff(body, signoff, force=True) + + return redirect("%s/changeset/%s/" % (app.site_root, revhash)) + +def _handle_comment(revhash): + filename = request.form.get('filename', '') + lines = str(request.form.get('lines', '')) + if lines: + lines = filter(None, [l.strip() for l in lines.split(',')]) + body = request.form['new-comment-body'] + + if body: + rcset = g.datastore[revhash] + rcset.add_comment(body, filename, lines) + + return redirect("%s/changeset/%s/" % (app.site_root, revhash)) + +@app.route('/changeset//', methods=['GET', 'POST']) +def changeset(revhash): + if request.method == 'POST': + signoff = request.form.get('signoff', None) + if signoff and not app.read_only: + return _handle_signoff(revhash) + elif not app.read_only or app.allow_anon: + return _handle_comment(revhash) + + rcset = g.datastore[revhash] + rev = rcset.target[revhash] + + cu_signoffs = rcset.signoffs_for_current_user() + cu_signoff = cu_signoffs[0] if cu_signoffs else None + + tip = g.datastore.target['tip'].rev() + newer = rcset.target[rev.rev() + 1] if rev.rev() < tip else None + older = rcset.target[rev.rev() - 1] if rev.rev() > 0 else None + + return _render('changeset.html', rcset=rcset, rev=rev, cu_signoff=cu_signoff, + newer=newer, older=older) + + +@app.route('/pull/', methods=['POST']) +def pull(): + if not app.read_only: + path = request.form['path'] + commands.pull(g.datastore.repo.ui, g.datastore.repo, path, update=True) + return redirect('%s/' % app.site_root) + +@app.route('/push/', methods=['POST']) +def push(): + if not app.read_only: + path = request.form['path'] + commands.push(g.datastore.repo.ui, g.datastore.repo, path) + return redirect('%s/' % app.site_root) + + +def load_interface(ui, repo, read_only=False, allow_anon=False, + open=False, address='127.0.0.1', port=8080): + if open: + import webbrowser + webbrowser.open('http://localhost:%d/' % port) + + app.read_only = read_only + app.debug = ui.debugflag + app.allow_anon = allow_anon + app.site_root = '' + + if app.allow_anon: + ui.setconfig('ui', 'username', 'Anonymous ') + + app.ui = ui + app.repo = repo + app.title = os.path.basename(repo.root) + + if app.debug: + from flaskext.lesscss import lesscss + lesscss(app) + app.run(host=address, port=port) diff -r d5280b38dd4c -r 34df61cd0a47 review/web_ui.py --- a/review/web_ui.py Tue Jun 15 19:15:24 2010 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,187 +0,0 @@ -from __future__ import with_statement - -"""The review extension's web UI.""" - -import sys, os -from hashlib import md5 - -from mercurial import commands, hg, templatefilters -from mercurial.node import short -from mercurial.util import email - -import api - -def unbundle(): - package_path = os.path.split(os.path.realpath(__file__))[0] - template_path = os.path.join(package_path, 'web_templates') - media_path = os.path.join(package_path, 'web_media') - top_path = os.path.split(package_path)[0] - bundled_path = os.path.join(top_path, 'bundled') - flask_path = os.path.join(bundled_path, 'flask') - jinja2_path = os.path.join(bundled_path, 'jinja2') - werkzeug_path = os.path.join(bundled_path, 'werkzeug') - simplejson_path = os.path.join(bundled_path, 'simplejson') - - sys.path.insert(0, flask_path) - sys.path.insert(0, werkzeug_path) - sys.path.insert(0, jinja2_path) - sys.path.insert(0, simplejson_path) - -unbundle() - -from flask import Flask -from flask import abort, g, redirect, render_template, request -app = Flask(__name__) - -LOG_PAGE_LEN = 15 - -def _item_gravatar(item, size=30): - return 'http://www.gravatar.com/avatar/%s?s=%d' % (md5(email(item.author)).hexdigest(), size) - -def _cset_gravatar(cset, size=30): - return 'http://www.gravatar.com/avatar/%s?s=%d' % (md5(email(cset.user())).hexdigest(), size) - -def _line_type(line): - return 'rem' if line[0] == '-' else 'add' if line[0] == '+' else 'con' - -def _categorize_signoffs(signoffs): - return { 'yes': len(filter(lambda s: s.opinion == 'yes', signoffs)), - 'no': len(filter(lambda s: s.opinion == 'no', signoffs)), - 'neutral': len(filter(lambda s: s.opinion == '', signoffs)),} -utils = { - 'node_short': short, - 'md5': md5, - 'email': email, - 'templatefilters': templatefilters, - 'len': len, - 'item_gravatar': _item_gravatar, - 'cset_gravatar': _cset_gravatar, - 'line_type': _line_type, - 'categorize_signoffs': _categorize_signoffs, - 'map': map, - 'str': str, - 'decode': lambda s: s.decode('utf-8'), -} - -def _render(template, **kwargs): - return render_template(template, read_only=app.read_only, - allow_anon=app.allow_anon, utils=utils, datastore=g.datastore, - title=app.title, **kwargs) - - -@app.before_request -def load_datastore(): - g.datastore = api.ReviewDatastore(app.ui, hg.repository(app.ui, app.repo.root)) - -@app.route('/') -def index_newest(): - return index(-1) - -@app.route('//') -def index(rev_max): - tip = g.datastore.target['tip'].rev() - - if rev_max > tip or rev_max < 0: - rev_max = tip - - rev_min = rev_max - LOG_PAGE_LEN if rev_max >= LOG_PAGE_LEN else 0 - if rev_min < 0: - rev_min = 0 - - older = rev_min - 1 if rev_min > 0 else -1 - newer = rev_max + LOG_PAGE_LEN + 1 if rev_max < tip else -1 - if newer > tip: - newer = tip - - rcsets = [g.datastore[r] for r in xrange(rev_max, rev_min - 1, -1)] - return _render('index.html', rcsets=rcsets, newer=newer, older=older) - - -def _handle_signoff(revhash): - signoff = request.form.get('signoff', None) - - if signoff not in ['yes', 'no', 'neutral']: - abort(400) - - if signoff == 'neutral': - signoff = '' - - body = request.form.get('new-signoff-body', '') - rcset = g.datastore[revhash] - rcset.add_signoff(body, signoff, force=True) - - return redirect("%s/changeset/%s/" % (app.site_root, revhash)) - -def _handle_comment(revhash): - filename = request.form.get('filename', '') - lines = str(request.form.get('lines', '')) - if lines: - lines = filter(None, [l.strip() for l in lines.split(',')]) - body = request.form['new-comment-body'] - - if body: - rcset = g.datastore[revhash] - rcset.add_comment(body, filename, lines) - - return redirect("%s/changeset/%s/" % (app.site_root, revhash)) - -@app.route('/changeset//', methods=['GET', 'POST']) -def changeset(revhash): - if request.method == 'POST': - signoff = request.form.get('signoff', None) - if signoff and not app.read_only: - return _handle_signoff(revhash) - elif not app.read_only or app.allow_anon: - return _handle_comment(revhash) - - rcset = g.datastore[revhash] - rev = rcset.target[revhash] - - cu_signoffs = rcset.signoffs_for_current_user() - cu_signoff = cu_signoffs[0] if cu_signoffs else None - - tip = g.datastore.target['tip'].rev() - newer = rcset.target[rev.rev() + 1] if rev.rev() < tip else None - older = rcset.target[rev.rev() - 1] if rev.rev() > 0 else None - - return _render('changeset.html', rcset=rcset, rev=rev, cu_signoff=cu_signoff, - newer=newer, older=older) - - -@app.route('/pull/', methods=['POST']) -def pull(): - if not app.read_only: - path = request.form['path'] - commands.pull(g.datastore.repo.ui, g.datastore.repo, path, update=True) - return redirect('%s/' % app.site_root) - -@app.route('/push/', methods=['POST']) -def push(): - if not app.read_only: - path = request.form['path'] - commands.push(g.datastore.repo.ui, g.datastore.repo, path) - return redirect('%s/' % app.site_root) - - -def load_interface(ui, repo, read_only=False, allow_anon=False, - open=False, address='127.0.0.1', port=8080): - if open: - import webbrowser - webbrowser.open('http://localhost:%d/' % port) - - app.read_only = read_only - app.debug = ui.debugflag - app.allow_anon = allow_anon - app.site_root = '' - - if app.allow_anon: - ui.setconfig('ui', 'username', 'Anonymous ') - - app.ui = ui - app.repo = repo - app.title = os.path.basename(repo.root) - - if app.debug: - from flaskext.lesscss import lesscss - lesscss(app) - app.run(host=address, port=port)