--- 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)
--- 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 *
--- 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 )
--- /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<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
+ - 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 <text> 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)),
+)
--- 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<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
- - 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 <text> 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)),
-)
--- 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
--- /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
--- 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 = """\
--- 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,
--- /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('/<int:rev_max>/')
+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/<revhash>/', 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 <anonymous@example.com>')
+
+ 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)
--- 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('/<int:rev_max>/')
-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/<revhash>/', 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 <anonymous@example.com>')
-
- 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)