review/extension_ui.py @ 7fef894ba429

Add the ability to comment on multiple files at once.

fixes issue 9
author Steve Losh <steve@stevelosh.com>
date Sat, 10 Oct 2009 18:28:33 -0400
parents 44d1d0012482
children 8d14fdcfb92d
"""The review extension's command-line UI.

This module is imported in __init__.py so that Mercurial will add the
review command to its own UI when you add the extension in ~/.hgrc.

"""

import operator, os
import messages
from api import *
from mercurial import templatefilters, util
from mercurial.node import short


def _init_command(ui, repo, **opts):
    ui.note(messages.INIT_START)
    try:
        ReviewDatastore(ui, repo, lpath=opts.pop('local_path'),
            rpath=opts.pop('remote_path'), create=True)
        ui.status(messages.INIT_SUCCESS)
        return
    except PreexistingDatastore, e:
        if e.committed:
            ui.note(messages.INIT_EXISTS_COMMITTED)
        else:
            raise util.Abort(messages.INIT_EXISTS_UNCOMMITTED)

def _comment_command(ui, repo, *fnames, **opts):
    rev = opts.pop('rev')
    message = opts.pop('message')
    lines = opts.pop('lines')
    
    rd = ReviewDatastore(ui, repo)
    rcset = rd[rev]
    
    if lines and not len(fnames) == 1:
        raise util.Abort(messages.COMMENT_LINES_REQUIRE_FILE)
    
    if not message:
        raise util.Abort(messages.COMMENT_REQUIRES_MESSAGE)
    
    if lines:
        lines=lines.split(',')
    
    if fnames:
        fnames = map(lambda f: sanitize_path(f, repo), fnames)
    else:
        fnames = ['']
    
    for fn in fnames:
        try:
            rcset.add_comment(message=message, filename=fn, lines=lines)
        except FileNotInChangeset:
            raise util.Abort(
                messages.COMMENT_FILE_DOES_NOT_EXIST % (fn, repo[rev].rev())
            )

def _signoff_command(ui, repo, **opts):
    rd = ReviewDatastore(ui, repo)
    rcset = rd[opts.pop('rev')]
    message = opts.pop('message')
    
    if not message:
        raise util.Abort(messages.SIGNOFF_REQUIRES_MESSAGE)
    
    yes, no = opts.pop('yes'), opts.pop('no')
    if yes and no:
        raise util.Abort(messages.SIGNOFF_OPINION_CONFLICT)
    opinion = 'yes' if yes else ('no' if no else '')
    
    try:
        rcset.add_signoff(message=message, opinion=opinion,
            force=opts.pop('force'))
    except SignoffExists:
        raise util.Abort(messages.SIGNOFF_EXISTS)

def _review_command(ui, repo, *fnames, **opts):
    rev = opts.pop('rev')
    context = int(opts.pop('unified'))
    
    rd = ReviewDatastore(ui, repo)
    cset = repo[rev]
    rcset = rd[rev]
    
    comment_count = len(rcset.comments)
    author_count = len(set(comment.author for comment in rcset.comments))
    
    ui.write(messages.REVIEW_LOG_CSET % (cset.rev(), short(cset.node())))
    ui.write(messages.REVIEW_LOG_AUTHOR % cset.user())
    ui.write(messages.REVIEW_LOG_SUMMARY % cset.description().split('\n')[0])
    
    signoffs = rcset.signoffs
    signoffs_yes = filter(lambda s: s.opinion == 'yes', signoffs)
    signoffs_no = filter(lambda s: s.opinion == 'no', signoffs)
    signoffs_neutral = set(signoffs).difference(signoffs_yes + signoffs_no)
    
    ui.write(messages.REVIEW_LOG_SIGNOFFS % (
        len(signoffs), len(signoffs_yes), len(signoffs_no), len(signoffs_neutral))
    )
    ui.write(messages.REVIEW_LOG_COMMENTS % (comment_count, author_count))
    
    def _print_comment(comment, before='', after=''):
        ui.write(before)
        
        author = templatefilters.person(comment.author)
        author_part = messages.REVIEW_LOG_COMMENT_AUTHOR % author
        
        age = templatefilters.age(comment.hgdate)
        age_part = messages.REVIEW_LOG_COMMENT_AGE % age 
        
        spacing = ' ' * (80 - (len(author_part) + len(age_part)))
        
        ui.write(author_part + spacing + age_part + '\n')
        for line in comment.message.splitlines():
            ui.write(messages.REVIEW_LOG_COMMENT_LINE % line)
        
        ui.write(after)
    
    review_level_comments = filter(lambda c: not c.filename, rcset.comments)
    if review_level_comments:
        ui.write('\n')
    for comment in review_level_comments:
        _print_comment(comment, before='\n')
    
    fnames = [sanitize_path(fname, repo) for fname in fnames]
    diffs = rcset.diffs(fnames, context)
    
    for filename, diff in diffs.iteritems():
        max_line = diff['max']
        content = diff['content']
        
        header = messages.REVIEW_LOG_FILE_HEADER % filename
        print '\n\n%s %s' % (header, '-'*(80-(len(header)+1)))
        
        file_level_comments = filter(
            lambda c: filename == c.filename and not c.lines, rcset.comments
        )
        for comment in file_level_comments:
            _print_comment(comment)
        
        line_level_comments = filter(
            lambda c: filename == c.filename and c.lines, rcset.comments
        )
        prefix = '%%%dd: ' % len(str(content[-1][0]))
        previous_n = -1
        for n, line in content:
            if n - 1 > previous_n:
                skipped = n - previous_n
                if previous_n == -1:
                    skipped -= 1
                ui.write(messages.REVIEW_LOG_SKIPPED % skipped)
                skipped_comments = filter(
                    lambda c: max(c.lines) in range(previous_n + 1, n),
                    line_level_comments
                )
                for comment in skipped_comments:
                    _print_comment(comment)
            
            ui.write('%s %s\n' % (prefix % n, line))
            
            line_comments = filter(
                lambda c: max(c.lines) == n, line_level_comments
            )
            for comment in line_comments:
                _print_comment(comment)
            
            previous_n = n
        
        if previous_n < max_line:
            skipped = max_line - previous_n
            ui.write(messages.REVIEW_LOG_SKIPPED % skipped)
    


def review(ui, repo, *fnames, **opts):
    """code review a changeset in the current repository
    """
    if opts.pop('init'):
        return _init_command(ui, repo, **opts)
    elif opts.pop('comment'):
        return _comment_command(ui, repo, *fnames, **opts)
    elif opts.pop('signoff'):
        return _signoff_command(ui, repo, **opts)
    else:
        return _review_command(ui, repo, *fnames, **opts)


cmdtable = {
    'review': (review, [
        ('i', 'init',        False, 'start code reviewing this repository'),
        ('',  'local-path',  '',    'the local path to the code review data'),
        ('',  'remote-path', '',    'the remote path to code review data'),
        ('c', 'comment',     False, 'add a comment'),
        ('s', 'signoff',     False, 'sign off'),
        ('',  'yes',         False, 'sign off as stating the changeset is good'),
        ('',  'no',          False, 'sign off as stating the changeset is bad'),
        ('m', 'message',     '',    'use <text> as the comment or signoff message'),
        ('',  'force',       False, 'overwrite an existing signoff'),
        ('r', 'rev',         '.',   'the revision to review'),
        ('l', 'lines',       '',    'the line(s) of the file to comment on'),
        ('U', 'unified',     '5',   'number of lines of context to show'),
    ],
    'hg review')
}