review/extension_ui.py @ 68e589a7c8c4

Add tests for contextual diffs with comments.

fixes issue 12
author Steve Losh <steve@stevelosh.com>
date Sat, 10 Oct 2009 10:51:51 -0400
parents 69bbcf7f0830
children 1f2f3cb23ac3
"""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 util
from mercurial.node import short


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

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

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

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


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


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