34df61cd0a47

guts: rename some files to make things cleaner
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Tue, 15 Jun 2010 20:40:50 -0400
parents d5280b38dd4c
children f61b33b27999
branches/tags (none)
files contrib/deploy/wsgi.py review/__init__.py review/api.py review/cli.py review/extension_ui.py review/file_templates.py review/files.py review/messages.py review/tests/util.py review/web.py review/web_ui.py

Changes

--- 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)