[PATCH] contribution: code review extension
Boris Glimcher
glimchb at gmail.com
Mon Sep 7 11:54:01 CDT 2009
# HG changeset patch
# User Boris Glimcher
# Date 1252340875 -10800
# Node ID 95aac6072859a4db5d8be521afdafe8972c53be9
# Parent 37042e8b3b342b2e380d8be3e3f7692584c92d33
contribution: code review extension
diff -r 37042e8b3b34 -r 95aac6072859 hgext/codereview.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/codereview.py Mon Sep 07 19:27:55 2009 +0300
@@ -0,0 +1,253 @@
+#!/usr/bin/env python
+
+# Patch Code Review extension for Mercurial
+#
+# Copyright 2009 Boris Glimcher <glimchb at gmail.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+'''CodeReview management tool
+
+This extension allows you to manage reviews for your code .
+
+Code review database is stored in .code-review file in your repository
+root directory as a map of file and revision when review was done.
+
+This extension allows you to add, remove, view status and perform code review.
+'''
+
+from __future__ import with_statement
+
+import os
+import re
+import sys
+import os.path
+
+from mercurial import ui, cmdutil, node
+
+################################################################################
+class CodeReview(object):
+
+ # database
+ DB_FILE = '.code-review'
+
+ def __init__(self, ui, repo, rev):
+ self.ui = ui
+ self.repo = repo
+ self.db_path = os.path.join(self.repo.root, self.DB_FILE)
+
+ def _status(self, base, *files, **opts):
+ # NOTE: files must have absolute paths
+ node1, node2 = None, None
+ if base:
+ node1 = self.repo.lookup(base)
+ # Prefix all files with repo's location
+ files = [self.repo.wjoin(f) for f in files]
+ return self.repo.status(node1=node1, node2=node2, match=cmdutil.match(self.repo, files), **opts)
+
+ def revno(self, rev):
+ '''Return short revision id'''
+ ctx = self.repo.changectx(rev) # get by revision id
+ num = ctx.rev() # numeric id (not SHA-1)
+ if num < 0:
+ return 'null'
+ return num
+
+ def _open_db(self):
+ '''Open Database'''
+ if not os.path.exists(self.db_path):
+ # create the file if it does not exist
+ open(self.db_path, 'wt').close()
+
+ # parse database file
+ db = (line.rstrip('\n') for line in open(self.db_path, 'rt'))
+ db = (line.split('#', 1)[0] for line in db) # remove comments (from # to EOL)
+ db = (line.strip() for line in db) # remove whitespace from both sides of the line
+
+ regex = re.compile('\s+')
+ db = (regex.split(line) for line in db if line)
+
+ # make dictionary from database file
+ try:
+ db = dict((f, (round, rev)) for (rev, round, f) in db)
+ except ValueError:
+ self.ui.warn('Error in Database file %s! Truncating...\n' % self.db_path)
+ db = dict()
+
+ return db
+
+ def _save_db(self, db):
+ ''' Save Database file'''
+ db = sorted(db.iteritems())
+ db = ''.join(['%s %s %s\n' % (rev, round, f) for (f, (round, rev)) in db])
+ with open(self.db_path, 'wt') as f:
+ f.write(db)
+
+ def done_files(self, files, rev=None):
+ '''Mark files as code-review completed'''
+ if not files:
+ self.ui.warn('No files were selected for COMPLETE command\n')
+ return
+
+ # There must be one ONLY parent for working directory (cannot CR during merge)
+ parent, = self.repo[rev].parents()
+ parent_rev = node.hex(parent.node())
+ num = self.revno(parent_rev)
+ self.ui.note('current revision: %s (#%s)\n' % (parent_rev, num))
+
+ # open db
+ db = self._open_db()
+
+ # only checked files
+ for filename in files:
+ # normpath to support unix/linux style
+ filename = os.path.normpath(filename)
+
+ if filename not in db:
+ self.ui.warn('%s is not reviewed!\n' % (filename,))
+ continue
+
+ if any(self._status(None, filename)):
+ self.ui.warn('%s was modified since last commit!\n' % (filename,))
+ continue
+
+ round, old_rev = db[filename]
+ db[filename] = (int(round) + 1, parent_rev)
+ self.ui.status('%s review is done at #%s\n' % (filename, num))
+
+ # save db
+ self._save_db(db)
+
+ def remove_files(self, files):
+ '''REmove files from code-review'''
+ if not files:
+ self.ui.warn('No files were selected for REMOVE\n')
+ return
+
+ # open db
+ db = self._open_db()
+
+ for filename in files:
+ # normpath to support unix/linux style
+ filename = os.path.normpath(filename)
+
+ if filename not in db:
+ self.ui.warn('%s is not reviewed!\n' % (filename,))
+ continue
+
+ self.ui.status('%s removed from review list\n' % (filename,))
+ db.pop(filename)
+
+ # save db
+ self._save_db(db)
+
+ def add_files(self, files):
+ '''Add files to code review'''
+ if not files:
+ self.ui.warn('No files were selected for ADD\n')
+ return
+
+ # open db
+ db = self._open_db()
+
+ for filename in files:
+
+ # abspath to avoid windows/linux and relative paths issues
+ filename = os.path.abspath(os.path.basename(filename))
+
+ if self.repo.root not in filename:
+ self.ui.warn('%s is not under source control!\n' % (filename,))
+ continue
+
+ #DEV: create better solution to find relative path
+ filename = filename.replace(self.repo.root, '')[1:]
+
+ if filename in db:
+ self.ui.warn('%s is already in the review list!\n' % (filename,))
+ continue
+
+ if os.path.isdir(filename):
+ self.ui.warn('cannot review directory \'%s\' !\n' % (filename,))
+ continue
+
+ if ' ' in filename:
+ self.ui.warn('file path cannot contain spaces \'%s\' !\n' % (filename,))
+ continue
+
+ # mark as not-review-yet
+ db[filename] = (0, node.hex(node.nullid))
+ self.ui.status('%s added to review list\n' % (filename,))
+
+ # save db
+ self._save_db(db)
+
+ def list_files(self):
+ '''List files that are managed in code-review'''
+ # make db
+ db = self._open_db()
+ files = sorted(db.keys())
+
+ result = []
+ for f in files:
+ round, base = db.get(f)
+
+ if not base: # file is not CRed
+ self.ui.warn('%s is not reviewed!\n' % (f,))
+ continue
+
+ # run "hg status" to find out if `f` was changed since its last CR
+ res = self._status(base, f, clean=True)
+ (modified, added, removed, deleted, unknown, ignored, clean) = res
+
+ base = self.revno(base)
+ result.append((f, base, round, clean))
+
+ return result
+
+def main(ui, repo, *pats, **opts):
+ """Code Review Plugin (requires Mercurial 1.1.x!)"""
+
+ if sum(map(int, opts.values())) < 1:
+ ui.warn('At most one action must be specified! See "hg cr --help" for details')
+ return
+
+ code_review = CodeReview(ui, repo, None)
+
+ files = []
+ if pats:
+ # Match all files using given patterns
+ m = cmdutil.match(repo, pats, opts)
+ files = m.files()
+ for f in files:
+ ui.note('matched: %s\n' % (f,))
+
+ if opts['list']:
+ format = '%5s %10s %s %s\n'
+ ui.status(format % ('round', 'revision', 'status', 'filename' ) )
+ ui.status(format % ('-----', '--------', '------', '--------' ) )
+ for f, base , round, clean in code_review.list_files():
+ res = 'completed' if clean else 'changed since last review '
+ ui.status(format % (round, code_review.revno(base), res, f) )
+
+ elif opts['complete']:
+ code_review.done_files(files)
+
+ elif opts['add']:
+ code_review.add_files(files)
+
+ elif opts['remove']:
+ code_review.remove_files(files)
+
+ else:
+ ui.warn('At most one action must be specified! See "hg cr --help" for details')
+
+cmdtable = {
+ 'cr': (main,
+ [('c', 'complete', False, 'Mark CR as complete'),
+ ('a', 'add', False, 'Add files to CR list'),
+ ('r', 'remove', False, 'Remove files from CR list'),
+ ('l', 'list', False, 'Print files in CR list'),
+ ],
+ 'hg cr [OPTIONS] [FILES]')
+}
More information about the Mercurial
mailing list