Commit bbf952c3 authored by Russ Cox's avatar Russ Cox

codereview: cleanup + basic tests

R=adg, bradfitz
CC=golang-dev
https://golang.org/cl/5395044
parent 0bbbb44e
...@@ -22,7 +22,7 @@ To configure, set the following options in ...@@ -22,7 +22,7 @@ To configure, set the following options in
your repository's .hg/hgrc file. your repository's .hg/hgrc file.
[extensions] [extensions]
codereview = path/to/codereview.py codereview = /path/to/codereview.py
[codereview] [codereview]
server = codereview.appspot.com server = codereview.appspot.com
...@@ -38,110 +38,60 @@ For example, if change 123456 contains the files x.go and y.go, ...@@ -38,110 +38,60 @@ For example, if change 123456 contains the files x.go and y.go,
"hg diff @123456" is equivalent to"hg diff x.go y.go". "hg diff @123456" is equivalent to"hg diff x.go y.go".
''' '''
from mercurial import cmdutil, commands, hg, util, error, match, discovery import sys
from mercurial.node import nullrev, hex, nullid, short
import os, re, time
import stat
import subprocess
import threading
from HTMLParser import HTMLParser
# The standard 'json' package is new in Python 2.6.
# Before that it was an external package named simplejson.
try:
# Standard location in 2.6 and beyond.
import json
except Exception, e:
try:
# Conventional name for earlier package.
import simplejson as json
except:
try:
# Was also bundled with django, which is commonly installed.
from django.utils import simplejson as json
except:
# We give up.
raise e
try:
hgversion = util.version()
except:
from mercurial.version import version as v
hgversion = v.get_version()
# in Mercurial 1.9 the cmdutil.match and cmdutil.revpair moved to scmutil
if hgversion >= '1.9':
from mercurial import scmutil
else:
scmutil = cmdutil
oldMessage = """
The code review extension requires Mercurial 1.3 or newer.
To install a new Mercurial,
sudo easy_install mercurial
works on most systems.
"""
linuxMessage = """
You may need to clear your current Mercurial installation by running:
sudo apt-get remove mercurial mercurial-common
sudo rm -rf /etc/mercurial
"""
if hgversion < '1.3': if __name__ == "__main__":
msg = oldMessage print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly."
if os.access("/etc/mercurial", 0): sys.exit(2)
msg += linuxMessage
raise util.Abort(msg)
def promptyesno(ui, msg): # We require Python 2.6 for the json package.
# Arguments to ui.prompt changed between 1.3 and 1.3.1. if sys.version < '2.6':
# Even so, some 1.3.1 distributions seem to have the old prompt!?!? print >>sys.stderr, "The codereview extension requires Python 2.6 or newer."
# What a terrible way to maintain software. print >>sys.stderr, "You are running Python " + sys.version
try: sys.exit(2)
return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0
except AttributeError:
return ui.prompt(msg, ["&yes", "&no"], "y") != "n"
def incoming(repo, other): import json
fui = FakeMercurialUI() import os
ret = commands.incoming(fui, repo, *[other.path], **{'bundle': '', 'force': False}) import re
if ret and ret != 1: import stat
raise util.Abort(ret) import subprocess
out = fui.output import threading
return out import time
def outgoing(repo): from mercurial import commands as hg_commands
fui = FakeMercurialUI() from mercurial import util as hg_util
ret = commands.outgoing(fui, repo, *[], **{})
if ret and ret != 1:
raise util.Abort(ret)
out = fui.output
return out
# To experiment with Mercurial in the python interpreter: defaultcc = None
# >>> repo = hg.repository(ui.ui(), path = ".") codereview_disabled = None
real_rollback = None
releaseBranch = None
server = "codereview.appspot.com"
server_url_base = None
####################################################################### #######################################################################
# Normally I would split this into multiple files, but it simplifies # Normally I would split this into multiple files, but it simplifies
# import path headaches to keep it all in one file. Sorry. # import path headaches to keep it all in one file. Sorry.
# The different parts of the file are separated by banners like this one.
import sys #######################################################################
if __name__ == "__main__": # Helpers
print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly."
sys.exit(2)
server = "codereview.appspot.com" def RelativePath(path, cwd):
server_url_base = None n = len(cwd)
defaultcc = None if path.startswith(cwd) and path[n] == '/':
contributors = {} return path[n+1:]
missing_codereview = None return path
real_rollback = None
releaseBranch = None def Sub(l1, l2):
return [l for l in l1 if l not in l2]
def Add(l1, l2):
l = l1 + Sub(l2, l1)
l.sort()
return l
def Intersect(l1, l2):
return [l for l in l1 if l in l2]
####################################################################### #######################################################################
# RE: UNICODE STRING HANDLING # RE: UNICODE STRING HANDLING
...@@ -168,7 +118,7 @@ releaseBranch = None ...@@ -168,7 +118,7 @@ releaseBranch = None
def typecheck(s, t): def typecheck(s, t):
if type(s) != t: if type(s) != t:
raise util.Abort("type check failed: %s has type %s != %s" % (repr(s), type(s), t)) raise hg_util.Abort("type check failed: %s has type %s != %s" % (repr(s), type(s), t))
# If we have to pass unicode instead of str, ustr does that conversion clearly. # If we have to pass unicode instead of str, ustr does that conversion clearly.
def ustr(s): def ustr(s):
...@@ -199,6 +149,40 @@ def default_to_utf8(): ...@@ -199,6 +149,40 @@ def default_to_utf8():
default_to_utf8() default_to_utf8()
#######################################################################
# Status printer for long-running commands
global_status = None
def set_status(s):
# print >>sys.stderr, "\t", time.asctime(), s
global global_status
global_status = s
class StatusThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
# pause a reasonable amount of time before
# starting to display status messages, so that
# most hg commands won't ever see them.
time.sleep(30)
# now show status every 15 seconds
while True:
time.sleep(15 - time.time() % 15)
s = global_status
if s is None:
continue
if s == "":
s = "(unknown status)"
print >>sys.stderr, time.asctime(), s
def start_status_thread():
t = StatusThread()
t.setDaemon(True) # allowed to exit if t is still running
t.start()
####################################################################### #######################################################################
# Change list parsing. # Change list parsing.
# #
...@@ -275,13 +259,14 @@ class CL(object): ...@@ -275,13 +259,14 @@ class CL(object):
typecheck(s, str) typecheck(s, str)
return s return s
def PendingText(self): def PendingText(self, quick=False):
cl = self cl = self
s = cl.name + ":" + "\n" s = cl.name + ":" + "\n"
s += Indent(cl.desc, "\t") s += Indent(cl.desc, "\t")
s += "\n" s += "\n"
if cl.copied_from: if cl.copied_from:
s += "\tAuthor: " + cl.copied_from + "\n" s += "\tAuthor: " + cl.copied_from + "\n"
if not quick:
s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n" s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n"
for (who, line) in cl.lgtm: for (who, line) in cl.lgtm:
s += "\t\t" + who + ": " + line + "\n" s += "\t\t" + who + ": " + line + "\n"
...@@ -360,7 +345,7 @@ class CL(object): ...@@ -360,7 +345,7 @@ class CL(object):
uploaded_diff_file = [("data", "data.diff", emptydiff)] uploaded_diff_file = [("data", "data.diff", emptydiff)]
if vcs and self.name != "new": if vcs and self.name != "new":
form_fields.append(("subject", "diff -r " + vcs.base_rev + " " + getremote(ui, repo, {}).path)) form_fields.append(("subject", "diff -r " + vcs.base_rev + " " + ui.expandpath("default")))
else: else:
# First upload sets the subject for the CL itself. # First upload sets the subject for the CL itself.
form_fields.append(("subject", self.Subject())) form_fields.append(("subject", self.Subject()))
...@@ -379,7 +364,7 @@ class CL(object): ...@@ -379,7 +364,7 @@ class CL(object):
ui.status(msg + "\n") ui.status(msg + "\n")
set_status("uploaded CL metadata + diffs") set_status("uploaded CL metadata + diffs")
if not response_body.startswith("Issue created.") and not response_body.startswith("Issue updated."): if not response_body.startswith("Issue created.") and not response_body.startswith("Issue updated."):
raise util.Abort("failed to update issue: " + response_body) raise hg_util.Abort("failed to update issue: " + response_body)
issue = msg[msg.rfind("/")+1:] issue = msg[msg.rfind("/")+1:]
self.name = issue self.name = issue
if not self.url: if not self.url:
...@@ -404,7 +389,7 @@ class CL(object): ...@@ -404,7 +389,7 @@ class CL(object):
pmsg += " (cc: %s)" % (', '.join(self.cc),) pmsg += " (cc: %s)" % (', '.join(self.cc),)
pmsg += ",\n" pmsg += ",\n"
pmsg += "\n" pmsg += "\n"
repourl = getremote(ui, repo, {}).path repourl = ui.expandpath("default")
if not self.mailed: if not self.mailed:
pmsg += "I'd like you to review this change to\n" + repourl + "\n" pmsg += "I'd like you to review this change to\n" + repourl + "\n"
else: else:
...@@ -567,37 +552,6 @@ def LoadCL(ui, repo, name, web=True): ...@@ -567,37 +552,6 @@ def LoadCL(ui, repo, name, web=True):
set_status("loaded CL " + name) set_status("loaded CL " + name)
return cl, '' return cl, ''
global_status = None
def set_status(s):
# print >>sys.stderr, "\t", time.asctime(), s
global global_status
global_status = s
class StatusThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
# pause a reasonable amount of time before
# starting to display status messages, so that
# most hg commands won't ever see them.
time.sleep(30)
# now show status every 15 seconds
while True:
time.sleep(15 - time.time() % 15)
s = global_status
if s is None:
continue
if s == "":
s = "(unknown status)"
print >>sys.stderr, time.asctime(), s
def start_status_thread():
t = StatusThread()
t.setDaemon(True) # allowed to exit if t is still running
t.start()
class LoadCLThread(threading.Thread): class LoadCLThread(threading.Thread):
def __init__(self, ui, repo, dir, f, web): def __init__(self, ui, repo, dir, f, web):
threading.Thread.__init__(self) threading.Thread.__init__(self)
...@@ -735,101 +689,6 @@ _change_prolog = """# Change list. ...@@ -735,101 +689,6 @@ _change_prolog = """# Change list.
# Multi-line values should be indented. # Multi-line values should be indented.
""" """
#######################################################################
# Mercurial helper functions
# Get effective change nodes taking into account applied MQ patches
def effective_revpair(repo):
try:
return scmutil.revpair(repo, ['qparent'])
except:
return scmutil.revpair(repo, None)
# Return list of changed files in repository that match pats.
# Warn about patterns that did not match.
def matchpats(ui, repo, pats, opts):
matcher = scmutil.match(repo, pats, opts)
node1, node2 = effective_revpair(repo)
modified, added, removed, deleted, unknown, ignored, clean = repo.status(node1, node2, matcher, ignored=True, clean=True, unknown=True)
return (modified, added, removed, deleted, unknown, ignored, clean)
# Return list of changed files in repository that match pats.
# The patterns came from the command line, so we warn
# if they have no effect or cannot be understood.
def ChangedFiles(ui, repo, pats, opts, taken=None):
taken = taken or {}
# Run each pattern separately so that we can warn about
# patterns that didn't do anything useful.
for p in pats:
modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, [p], opts)
redo = False
for f in unknown:
promptadd(ui, repo, f)
redo = True
for f in deleted:
promptremove(ui, repo, f)
redo = True
if redo:
modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, [p], opts)
for f in modified + added + removed:
if f in taken:
ui.warn("warning: %s already in CL %s\n" % (f, taken[f].name))
if not modified and not added and not removed:
ui.warn("warning: %s did not match any modified files\n" % (p,))
# Again, all at once (eliminates duplicates)
modified, added, removed = matchpats(ui, repo, pats, opts)[:3]
l = modified + added + removed
l.sort()
if taken:
l = Sub(l, taken.keys())
return l
# Return list of changed files in repository that match pats and still exist.
def ChangedExistingFiles(ui, repo, pats, opts):
modified, added = matchpats(ui, repo, pats, opts)[:2]
l = modified + added
l.sort()
return l
# Return list of files claimed by existing CLs
def Taken(ui, repo):
all = LoadAllCL(ui, repo, web=False)
taken = {}
for _, cl in all.items():
for f in cl.files:
taken[f] = cl
return taken
# Return list of changed files that are not claimed by other CLs
def DefaultFiles(ui, repo, pats, opts):
return ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
def Sub(l1, l2):
return [l for l in l1 if l not in l2]
def Add(l1, l2):
l = l1 + Sub(l2, l1)
l.sort()
return l
def Intersect(l1, l2):
return [l for l in l1 if l in l2]
def getremote(ui, repo, opts):
# save $http_proxy; creating the HTTP repo object will
# delete it in an attempt to "help"
proxy = os.environ.get('http_proxy')
source = hg.parseurl(ui.expandpath("default"), None)[0]
try:
remoteui = hg.remoteui # hg 1.6
except:
remoteui = cmdutil.remoteui
other = hg.repository(remoteui(repo, opts), source)
if proxy is not None:
os.environ['http_proxy'] = proxy
return other
desc_re = '^(.+: |(tag )?(release|weekly)\.|fix build|undo CL)' desc_re = '^(.+: |(tag )?(release|weekly)\.|fix build|undo CL)'
desc_msg = '''Your CL description appears not to use the standard form. desc_msg = '''Your CL description appears not to use the standard form.
...@@ -851,15 +710,17 @@ Examples: ...@@ -851,15 +710,17 @@ Examples:
''' '''
def promptyesno(ui, msg):
return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0
def promptremove(ui, repo, f): def promptremove(ui, repo, f):
if promptyesno(ui, "hg remove %s (y/n)?" % (f,)): if promptyesno(ui, "hg remove %s (y/n)?" % (f,)):
if commands.remove(ui, repo, 'path:'+f) != 0: if hg_commands.remove(ui, repo, 'path:'+f) != 0:
ui.warn("error removing %s" % (f,)) ui.warn("error removing %s" % (f,))
def promptadd(ui, repo, f): def promptadd(ui, repo, f):
if promptyesno(ui, "hg add %s (y/n)?" % (f,)): if promptyesno(ui, "hg add %s (y/n)?" % (f,)):
if commands.add(ui, repo, 'path:'+f) != 0: if hg_commands.add(ui, repo, 'path:'+f) != 0:
ui.warn("error adding %s" % (f,)) ui.warn("error adding %s" % (f,))
def EditCL(ui, repo, cl): def EditCL(ui, repo, cl):
...@@ -899,10 +760,14 @@ def EditCL(ui, repo, cl): ...@@ -899,10 +760,14 @@ def EditCL(ui, repo, cl):
# Check file list for files that need to be hg added or hg removed # Check file list for files that need to be hg added or hg removed
# or simply aren't understood. # or simply aren't understood.
pats = ['path:'+f for f in clx.files] pats = ['path:'+f for f in clx.files]
modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, pats, {}) changed = hg_matchPattern(ui, repo, *pats, modified=True, added=True, removed=True)
deleted = hg_matchPattern(ui, repo, *pats, deleted=True)
unknown = hg_matchPattern(ui, repo, *pats, unknown=True)
ignored = hg_matchPattern(ui, repo, *pats, ignored=True)
clean = hg_matchPattern(ui, repo, *pats, clean=True)
files = [] files = []
for f in clx.files: for f in clx.files:
if f in modified or f in added or f in removed: if f in changed:
files.append(f) files.append(f)
continue continue
if f in deleted: if f in deleted:
...@@ -954,7 +819,7 @@ def CommandLineCL(ui, repo, pats, opts, defaultcc=None): ...@@ -954,7 +819,7 @@ def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
else: else:
cl = CL("new") cl = CL("new")
cl.local = True cl.local = True
cl.files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo)) cl.files = ChangedFiles(ui, repo, pats, taken=Taken(ui, repo))
if not cl.files: if not cl.files:
return None, "no files changed" return None, "no files changed"
if opts.get('reviewer'): if opts.get('reviewer'):
...@@ -972,13 +837,362 @@ def CommandLineCL(ui, repo, pats, opts, defaultcc=None): ...@@ -972,13 +837,362 @@ def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
return None, err return None, err
return cl, "" return cl, ""
# reposetup replaces cmdutil.match with this wrapper, #######################################################################
# which expands the syntax @clnumber to mean the files # Change list file management
# in that CL.
original_match = None # Return list of changed files in repository that match pats.
global_repo = None # The patterns came from the command line, so we warn
global_ui = None # if they have no effect or cannot be understood.
def ReplacementForCmdutilMatch(ctx, pats=None, opts=None, globbed=False, default='relpath'): def ChangedFiles(ui, repo, pats, taken=None):
taken = taken or {}
# Run each pattern separately so that we can warn about
# patterns that didn't do anything useful.
for p in pats:
for f in hg_matchPattern(ui, repo, p, unknown=True):
promptadd(ui, repo, f)
for f in hg_matchPattern(ui, repo, p, removed=True):
promptremove(ui, repo, f)
files = hg_matchPattern(ui, repo, p, modified=True, added=True, removed=True)
for f in files:
if f in taken:
ui.warn("warning: %s already in CL %s\n" % (f, taken[f].name))
if not files:
ui.warn("warning: %s did not match any modified files\n" % (p,))
# Again, all at once (eliminates duplicates)
l = hg_matchPattern(ui, repo, *pats, modified=True, added=True, removed=True)
l.sort()
if taken:
l = Sub(l, taken.keys())
return l
# Return list of changed files in repository that match pats and still exist.
def ChangedExistingFiles(ui, repo, pats, opts):
l = hg_matchPattern(ui, repo, *pats, modified=True, added=True)
l.sort()
return l
# Return list of files claimed by existing CLs
def Taken(ui, repo):
all = LoadAllCL(ui, repo, web=False)
taken = {}
for _, cl in all.items():
for f in cl.files:
taken[f] = cl
return taken
# Return list of changed files that are not claimed by other CLs
def DefaultFiles(ui, repo, pats):
return ChangedFiles(ui, repo, pats, taken=Taken(ui, repo))
#######################################################################
# File format checking.
def CheckFormat(ui, repo, files, just_warn=False):
set_status("running gofmt")
CheckGofmt(ui, repo, files, just_warn)
CheckTabfmt(ui, repo, files, just_warn)
# Check that gofmt run on the list of files does not change them
def CheckGofmt(ui, repo, files, just_warn):
files = [f for f in files if (f.startswith('src/') or f.startswith('test/bench/')) and f.endswith('.go')]
if not files:
return
cwd = os.getcwd()
files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
files = [f for f in files if os.access(f, 0)]
if not files:
return
try:
cmd = subprocess.Popen(["gofmt", "-l"] + files, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=sys.platform != "win32")
cmd.stdin.close()
except:
raise hg_util.Abort("gofmt: " + ExceptionDetail())
data = cmd.stdout.read()
errors = cmd.stderr.read()
cmd.wait()
set_status("done with gofmt")
if len(errors) > 0:
ui.warn("gofmt errors:\n" + errors.rstrip() + "\n")
return
if len(data) > 0:
msg = "gofmt needs to format these files (run hg gofmt):\n" + Indent(data, "\t").rstrip()
if just_warn:
ui.warn("warning: " + msg + "\n")
else:
raise hg_util.Abort(msg)
return
# Check that *.[chys] files indent using tabs.
def CheckTabfmt(ui, repo, files, just_warn):
files = [f for f in files if f.startswith('src/') and re.search(r"\.[chys]$", f)]
if not files:
return
cwd = os.getcwd()
files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
files = [f for f in files if os.access(f, 0)]
badfiles = []
for f in files:
try:
for line in open(f, 'r'):
# Four leading spaces is enough to complain about,
# except that some Plan 9 code uses four spaces as the label indent,
# so allow that.
if line.startswith(' ') and not re.match(' [A-Za-z0-9_]+:', line):
badfiles.append(f)
break
except:
# ignore cannot open file, etc.
pass
if len(badfiles) > 0:
msg = "these files use spaces for indentation (use tabs instead):\n\t" + "\n\t".join(badfiles)
if just_warn:
ui.warn("warning: " + msg + "\n")
else:
raise hg_util.Abort(msg)
return
#######################################################################
# CONTRIBUTORS file parsing
contributors = {}
def ReadContributors(ui, repo):
global contributors
try:
f = open(repo.root + '/CONTRIBUTORS', 'r')
except:
ui.write("warning: cannot open %s: %s\n" % (repo.root+'/CONTRIBUTORS', ExceptionDetail()))
return
for line in f:
# CONTRIBUTORS is a list of lines like:
# Person <email>
# Person <email> <alt-email>
# The first email address is the one used in commit logs.
if line.startswith('#'):
continue
m = re.match(r"([^<>]+\S)\s+(<[^<>\s]+>)((\s+<[^<>\s]+>)*)\s*$", line)
if m:
name = m.group(1)
email = m.group(2)[1:-1]
contributors[email.lower()] = (name, email)
for extra in m.group(3).split():
contributors[extra[1:-1].lower()] = (name, email)
def CheckContributor(ui, repo, user=None):
set_status("checking CONTRIBUTORS file")
user, userline = FindContributor(ui, repo, user, warn=False)
if not userline:
raise hg_util.Abort("cannot find %s in CONTRIBUTORS" % (user,))
return userline
def FindContributor(ui, repo, user=None, warn=True):
if not user:
user = ui.config("ui", "username")
if not user:
raise hg_util.Abort("[ui] username is not configured in .hgrc")
user = user.lower()
m = re.match(r".*<(.*)>", user)
if m:
user = m.group(1)
if user not in contributors:
if warn:
ui.warn("warning: cannot find %s in CONTRIBUTORS\n" % (user,))
return user, None
user, email = contributors[user]
return email, "%s <%s>" % (user, email)
#######################################################################
# Mercurial helper functions.
# Read http://mercurial.selenic.com/wiki/MercurialApi before writing any of these.
# We use the ui.pushbuffer/ui.popbuffer + hg_commands.xxx tricks for all interaction
# with Mercurial. It has proved the most stable as they make changes.
# We require Mercurial 1.4 for now. The minimum required version can be pushed forward.
oldMessage = """
The code review extension requires Mercurial 1.4 or newer.
To install a new Mercurial,
sudo easy_install mercurial
works on most systems.
"""
linuxMessage = """
You may need to clear your current Mercurial installation by running:
sudo apt-get remove mercurial mercurial-common
sudo rm -rf /etc/mercurial
"""
hgversion = hg_util.version()
if hgversion < '1.4':
msg = oldMessage
if os.access("/etc/mercurial", 0):
msg += linuxMessage
raise hg_util.Abort(msg)
from mercurial.hg import clean as hg_clean
from mercurial import cmdutil as hg_cmdutil
from mercurial import error as hg_error
from mercurial import match as hg_match
from mercurial import node as hg_node
class uiwrap(object):
def __init__(self, ui):
self.ui = ui
ui.pushbuffer()
self.oldQuiet = ui.quiet
ui.quiet = True
self.oldVerbose = ui.verbose
ui.verbose = False
def output(self):
ui = self.ui
ui.quiet = self.oldQuiet
ui.verbose = self.oldVerbose
return ui.popbuffer()
def hg_matchPattern(ui, repo, *pats, **opts):
w = uiwrap(ui)
hg_commands.status(ui, repo, *pats, **opts)
text = w.output()
ret = []
prefix = os.path.realpath(repo.root)+'/'
for line in text.split('\n'):
f = line.split()
if len(f) > 1:
if len(pats) > 0:
# Given patterns, Mercurial shows relative to cwd
p = os.path.realpath(f[1])
if not p.startswith(prefix):
print >>sys.stderr, "File %s not in repo root %s.\n" % (p, prefix)
else:
ret.append(p[len(prefix):])
else:
# Without patterns, Mercurial shows relative to root (what we want)
ret.append(f[1])
return ret
def hg_heads(ui, repo):
w = uiwrap(ui)
ret = hg_commands.heads(ui, repo)
if ret:
raise hg_util.Abort(ret)
return w.output()
noise = [
"",
"resolving manifests",
"searching for changes",
"couldn't find merge tool hgmerge",
"adding changesets",
"adding manifests",
"adding file changes",
"all local heads known remotely",
]
def isNoise(line):
line = str(line)
for x in noise:
if line == x:
return True
return False
def hg_incoming(ui, repo):
w = uiwrap(ui)
ret = hg_commands.incoming(ui, repo, force=False, bundle="")
if ret and ret != 1:
raise hg_util.Abort(ret)
return w.output()
def hg_log(ui, repo, **opts):
for k in ['date', 'keyword', 'rev', 'user']:
if not opts.has_key(k):
opts[k] = ""
w = uiwrap(ui)
ret = hg_commands.log(ui, repo, **opts)
if ret:
raise hg_util.Abort(ret)
return w.output()
def hg_outgoing(ui, repo, **opts):
w = uiwrap(ui)
ret = hg_commands.outgoing(ui, repo, **opts)
if ret and ret != 1:
raise hg_util.Abort(ret)
return w.output()
def hg_pull(ui, repo, **opts):
w = uiwrap(ui)
ui.quiet = False
ui.verbose = True # for file list
err = hg_commands.pull(ui, repo, **opts)
for line in w.output().split('\n'):
if isNoise(line):
continue
if line.startswith('moving '):
line = 'mv ' + line[len('moving '):]
if line.startswith('getting ') and line.find(' to ') >= 0:
line = 'mv ' + line[len('getting '):]
if line.startswith('getting '):
line = '+ ' + line[len('getting '):]
if line.startswith('removing '):
line = '- ' + line[len('removing '):]
ui.write(line + '\n')
return err
def hg_push(ui, repo, **opts):
w = uiwrap(ui)
ui.quiet = False
ui.verbose = True
err = hg_commands.push(ui, repo, **opts)
for line in w.output().split('\n'):
if not isNoise(line):
ui.write(line + '\n')
return err
def hg_commit(ui, repo, *pats, **opts):
return hg_commands.commit(ui, repo, *pats, **opts)
#######################################################################
# Mercurial precommit hook to disable commit except through this interface.
commit_okay = False
def precommithook(ui, repo, **opts):
if commit_okay:
return False # False means okay.
ui.write("\ncodereview extension enabled; use mail, upload, or submit instead of commit\n\n")
return True
#######################################################################
# @clnumber file pattern support
# We replace scmutil.match with the MatchAt wrapper to add the @clnumber pattern.
match_repo = None
match_ui = None
match_orig = None
def InstallMatch(ui, repo):
global match_repo
global match_ui
global match_orig
match_ui = ui
match_repo = repo
from mercurial import scmutil
match_orig = scmutil.match
scmutil.match = MatchAt
def MatchAt(ctx, pats=None, opts=None, globbed=False, default='relpath'):
taken = [] taken = []
files = [] files = []
pats = pats or [] pats = pats or []
...@@ -988,101 +1202,29 @@ def ReplacementForCmdutilMatch(ctx, pats=None, opts=None, globbed=False, default ...@@ -988,101 +1202,29 @@ def ReplacementForCmdutilMatch(ctx, pats=None, opts=None, globbed=False, default
if p.startswith('@'): if p.startswith('@'):
taken.append(p) taken.append(p)
clname = p[1:] clname = p[1:]
if clname == "default":
files = DefaultFiles(match_ui, match_repo, [])
else:
if not GoodCLName(clname): if not GoodCLName(clname):
raise util.Abort("invalid CL name " + clname) raise hg_util.Abort("invalid CL name " + clname)
cl, err = LoadCL(global_repo.ui, global_repo, clname, web=False) cl, err = LoadCL(match_repo.ui, match_repo, clname, web=False)
if err != '': if err != '':
raise util.Abort("loading CL " + clname + ": " + err) raise hg_util.Abort("loading CL " + clname + ": " + err)
if not cl.files: if not cl.files:
raise util.Abort("no files in CL " + clname) raise hg_util.Abort("no files in CL " + clname)
files = Add(files, cl.files) files = Add(files, cl.files)
pats = Sub(pats, taken) + ['path:'+f for f in files] pats = Sub(pats, taken) + ['path:'+f for f in files]
# work-around for http://selenic.com/hg/rev/785bbc8634f8 # work-around for http://selenic.com/hg/rev/785bbc8634f8
if hgversion >= '1.9' and not hasattr(ctx, 'match'): if not hasattr(ctx, 'match'):
ctx = ctx[None] ctx = ctx[None]
return original_match(ctx, pats=pats, opts=opts, globbed=globbed, default=default) return match_orig(ctx, pats=pats, opts=opts, globbed=globbed, default=default)
def RelativePath(path, cwd):
n = len(cwd)
if path.startswith(cwd) and path[n] == '/':
return path[n+1:]
return path
def CheckFormat(ui, repo, files, just_warn=False):
set_status("running gofmt")
CheckGofmt(ui, repo, files, just_warn)
CheckTabfmt(ui, repo, files, just_warn)
# Check that gofmt run on the list of files does not change them
def CheckGofmt(ui, repo, files, just_warn):
files = [f for f in files if (f.startswith('src/') or f.startswith('test/bench/')) and f.endswith('.go')]
if not files:
return
cwd = os.getcwd()
files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
files = [f for f in files if os.access(f, 0)]
if not files:
return
try:
cmd = subprocess.Popen(["gofmt", "-l"] + files, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=sys.platform != "win32")
cmd.stdin.close()
except:
raise util.Abort("gofmt: " + ExceptionDetail())
data = cmd.stdout.read()
errors = cmd.stderr.read()
cmd.wait()
set_status("done with gofmt")
if len(errors) > 0:
ui.warn("gofmt errors:\n" + errors.rstrip() + "\n")
return
if len(data) > 0:
msg = "gofmt needs to format these files (run hg gofmt):\n" + Indent(data, "\t").rstrip()
if just_warn:
ui.warn("warning: " + msg + "\n")
else:
raise util.Abort(msg)
return
# Check that *.[chys] files indent using tabs.
def CheckTabfmt(ui, repo, files, just_warn):
files = [f for f in files if f.startswith('src/') and re.search(r"\.[chys]$", f)]
if not files:
return
cwd = os.getcwd()
files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
files = [f for f in files if os.access(f, 0)]
badfiles = []
for f in files:
try:
for line in open(f, 'r'):
# Four leading spaces is enough to complain about,
# except that some Plan 9 code uses four spaces as the label indent,
# so allow that.
if line.startswith(' ') and not re.match(' [A-Za-z0-9_]+:', line):
badfiles.append(f)
break
except:
# ignore cannot open file, etc.
pass
if len(badfiles) > 0:
msg = "these files use spaces for indentation (use tabs instead):\n\t" + "\n\t".join(badfiles)
if just_warn:
ui.warn("warning: " + msg + "\n")
else:
raise util.Abort(msg)
return
####################################################################### #######################################################################
# Mercurial commands # Commands added by code review extension.
# every command must take a ui and and repo as arguments. #######################################################################
# opts is a dict where you can find other command line flags # hg change
#
# Other parameters are taken in order from items on the command line that
# don't start with a dash. If no default value is given in the parameter list,
# they are required.
#
def change(ui, repo, *pats, **opts): def change(ui, repo, *pats, **opts):
"""create, edit or delete a change list """create, edit or delete a change list
...@@ -1106,8 +1248,8 @@ def change(ui, repo, *pats, **opts): ...@@ -1106,8 +1248,8 @@ def change(ui, repo, *pats, **opts):
before running hg change -d 123456. before running hg change -d 123456.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
dirty = {} dirty = {}
if len(pats) > 0 and GoodCLName(pats[0]): if len(pats) > 0 and GoodCLName(pats[0]):
...@@ -1121,12 +1263,12 @@ def change(ui, repo, *pats, **opts): ...@@ -1121,12 +1263,12 @@ def change(ui, repo, *pats, **opts):
if not cl.local and (opts["stdin"] or not opts["stdout"]): if not cl.local and (opts["stdin"] or not opts["stdout"]):
return "cannot change non-local CL " + name return "cannot change non-local CL " + name
else: else:
if repo[None].branch() != "default":
return "cannot run hg change outside default branch"
name = "new" name = "new"
cl = CL("new") cl = CL("new")
if repo[None].branch() != "default":
return "cannot create CL outside default branch"
dirty[cl] = True dirty[cl] = True
files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo)) files = ChangedFiles(ui, repo, pats, taken=Taken(ui, repo))
if opts["delete"] or opts["deletelocal"]: if opts["delete"] or opts["deletelocal"]:
if opts["delete"] and opts["deletelocal"]: if opts["delete"] and opts["deletelocal"]:
...@@ -1194,17 +1336,24 @@ def change(ui, repo, *pats, **opts): ...@@ -1194,17 +1336,24 @@ def change(ui, repo, *pats, **opts):
ui.write("CL created: " + cl.url + "\n") ui.write("CL created: " + cl.url + "\n")
return return
#######################################################################
# hg code-login (broken?)
def code_login(ui, repo, **opts): def code_login(ui, repo, **opts):
"""log in to code review server """log in to code review server
Logs in to the code review server, saving a cookie in Logs in to the code review server, saving a cookie in
a file in your home directory. a file in your home directory.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
MySend(None) MySend(None)
#######################################################################
# hg clpatch / undo / release-apply / download
# All concerned with applying or unapplying patches to the repository.
def clpatch(ui, repo, clname, **opts): def clpatch(ui, repo, clname, **opts):
"""import a patch from the code review server """import a patch from the code review server
...@@ -1274,16 +1423,16 @@ def release_apply(ui, repo, clname, **opts): ...@@ -1274,16 +1423,16 @@ def release_apply(ui, repo, clname, **opts):
return "no active release branches" return "no active release branches"
if c.branch() != releaseBranch: if c.branch() != releaseBranch:
if c.modified() or c.added() or c.removed(): if c.modified() or c.added() or c.removed():
raise util.Abort("uncommitted local changes - cannot switch branches") raise hg_util.Abort("uncommitted local changes - cannot switch branches")
err = hg.clean(repo, releaseBranch) err = hg_clean(repo, releaseBranch)
if err: if err:
return err return err
try: try:
err = clpatch_or_undo(ui, repo, clname, opts, mode="backport") err = clpatch_or_undo(ui, repo, clname, opts, mode="backport")
if err: if err:
raise util.Abort(err) raise hg_util.Abort(err)
except Exception, e: except Exception, e:
hg.clean(repo, "default") hg_clean(repo, "default")
raise e raise e
return None return None
...@@ -1318,14 +1467,10 @@ backportFooter = """ ...@@ -1318,14 +1467,10 @@ backportFooter = """
# Implementation of clpatch/undo. # Implementation of clpatch/undo.
def clpatch_or_undo(ui, repo, clname, opts, mode): def clpatch_or_undo(ui, repo, clname, opts, mode):
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
if mode == "undo" or mode == "backport": if mode == "undo" or mode == "backport":
if hgversion < '1.4':
# Don't have cmdutil.match (see implementation of sync command).
return "hg is too old to run hg %s - update to 1.4 or newer" % mode
# Find revision in Mercurial repository. # Find revision in Mercurial repository.
# Assume CL number is 7+ decimal digits. # Assume CL number is 7+ decimal digits.
# Otherwise is either change log sequence number (fewer decimal digits), # Otherwise is either change log sequence number (fewer decimal digits),
...@@ -1333,12 +1478,8 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1333,12 +1478,8 @@ def clpatch_or_undo(ui, repo, clname, opts, mode):
# Mercurial will fall over long before the change log # Mercurial will fall over long before the change log
# sequence numbers get to be 7 digits long. # sequence numbers get to be 7 digits long.
if re.match('^[0-9]{7,}$', clname): if re.match('^[0-9]{7,}$', clname):
found = False for r in hg_log(ui, repo, keyword="codereview.appspot.com/"+clname, limit=100, template="{node}\n").split():
matchfn = scmutil.match(repo, [], {'rev': None}) rev = repo[r]
def prep(ctx, fns):
pass
for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev': None}, prep):
rev = repo[ctx.rev()]
# Last line with a code review URL is the actual review URL. # Last line with a code review URL is the actual review URL.
# Earlier ones might be part of the CL description. # Earlier ones might be part of the CL description.
n = rev2clname(rev) n = rev2clname(rev)
...@@ -1356,7 +1497,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1356,7 +1497,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode):
return "cannot find CL name in revision description" return "cannot find CL name in revision description"
# Create fresh CL and start with patch that would reverse the change. # Create fresh CL and start with patch that would reverse the change.
vers = short(rev.node()) vers = hg_node.short(rev.node())
cl = CL("new") cl = CL("new")
desc = str(rev.description()) desc = str(rev.description())
if mode == "undo": if mode == "undo":
...@@ -1364,7 +1505,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1364,7 +1505,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode):
else: else:
cl.desc = (backportHeader % (releaseBranch, line1(desc), clname, vers)) + desc + undoFooter cl.desc = (backportHeader % (releaseBranch, line1(desc), clname, vers)) + desc + undoFooter
v1 = vers v1 = vers
v0 = short(rev.parents()[0].node()) v0 = hg_node.short(rev.parents()[0].node())
if mode == "undo": if mode == "undo":
arg = v1 + ":" + v0 arg = v1 + ":" + v0
else: else:
...@@ -1382,7 +1523,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1382,7 +1523,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode):
# find current hg version (hg identify) # find current hg version (hg identify)
ctx = repo[None] ctx = repo[None]
parents = ctx.parents() parents = ctx.parents()
id = '+'.join([short(p.node()) for p in parents]) id = '+'.join([hg_node.short(p.node()) for p in parents])
# if version does not match the patch version, # if version does not match the patch version,
# try to update the patch line numbers. # try to update the patch line numbers.
...@@ -1415,7 +1556,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1415,7 +1556,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode):
cl.files = out.strip().split() cl.files = out.strip().split()
if not cl.files and not opts["ignore_hgpatch_failure"]: if not cl.files and not opts["ignore_hgpatch_failure"]:
return "codereview issue %s has no changed files" % clname return "codereview issue %s has no changed files" % clname
files = ChangedFiles(ui, repo, [], opts) files = ChangedFiles(ui, repo, [])
extra = Sub(cl.files, files) extra = Sub(cl.files, files)
if extra: if extra:
ui.warn("warning: these files were listed in the patch but not changed:\n\t" + "\n\t".join(extra) + "\n") ui.warn("warning: these files were listed in the patch but not changed:\n\t" + "\n\t".join(extra) + "\n")
...@@ -1495,8 +1636,8 @@ def download(ui, repo, clname, **opts): ...@@ -1495,8 +1636,8 @@ def download(ui, repo, clname, **opts):
Download prints a description of the given change list Download prints a description of the given change list
followed by its diff, downloaded from the code review server. followed by its diff, downloaded from the code review server.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
cl, vers, patch, err = DownloadCL(ui, repo, clname) cl, vers, patch, err = DownloadCL(ui, repo, clname)
if err != "": if err != "":
...@@ -1505,6 +1646,9 @@ def download(ui, repo, clname, **opts): ...@@ -1505,6 +1646,9 @@ def download(ui, repo, clname, **opts):
ui.write(patch + "\n") ui.write(patch + "\n")
return return
#######################################################################
# hg file
def file(ui, repo, clname, pat, *pats, **opts): def file(ui, repo, clname, pat, *pats, **opts):
"""assign files to or remove files from a change list """assign files to or remove files from a change list
...@@ -1513,8 +1657,8 @@ def file(ui, repo, clname, pat, *pats, **opts): ...@@ -1513,8 +1657,8 @@ def file(ui, repo, clname, pat, *pats, **opts):
The -d option only removes files from the change list. The -d option only removes files from the change list.
It does not edit them or remove them from the repository. It does not edit them or remove them from the repository.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
pats = tuple([pat] + list(pats)) pats = tuple([pat] + list(pats))
if not GoodCLName(clname): if not GoodCLName(clname):
...@@ -1527,7 +1671,7 @@ def file(ui, repo, clname, pat, *pats, **opts): ...@@ -1527,7 +1671,7 @@ def file(ui, repo, clname, pat, *pats, **opts):
if not cl.local: if not cl.local:
return "cannot change non-local CL " + clname return "cannot change non-local CL " + clname
files = ChangedFiles(ui, repo, pats, opts) files = ChangedFiles(ui, repo, pats)
if opts["delete"]: if opts["delete"]:
oldfiles = Intersect(files, cl.files) oldfiles = Intersect(files, cl.files)
...@@ -1567,14 +1711,17 @@ def file(ui, repo, clname, pat, *pats, **opts): ...@@ -1567,14 +1711,17 @@ def file(ui, repo, clname, pat, *pats, **opts):
d.Flush(ui, repo) d.Flush(ui, repo)
return return
#######################################################################
# hg gofmt
def gofmt(ui, repo, *pats, **opts): def gofmt(ui, repo, *pats, **opts):
"""apply gofmt to modified files """apply gofmt to modified files
Applies gofmt to the modified files in the repository that match Applies gofmt to the modified files in the repository that match
the given patterns. the given patterns.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
files = ChangedExistingFiles(ui, repo, pats, opts) files = ChangedExistingFiles(ui, repo, pats, opts)
files = [f for f in files if f.endswith(".go")] files = [f for f in files if f.endswith(".go")]
...@@ -1587,21 +1734,24 @@ def gofmt(ui, repo, *pats, **opts): ...@@ -1587,21 +1734,24 @@ def gofmt(ui, repo, *pats, **opts):
if not opts["list"]: if not opts["list"]:
cmd += ["-w"] cmd += ["-w"]
if os.spawnvp(os.P_WAIT, "gofmt", cmd + files) != 0: if os.spawnvp(os.P_WAIT, "gofmt", cmd + files) != 0:
raise util.Abort("gofmt did not exit cleanly") raise hg_util.Abort("gofmt did not exit cleanly")
except error.Abort, e: except hg_error.Abort, e:
raise raise
except: except:
raise util.Abort("gofmt: " + ExceptionDetail()) raise hg_util.Abort("gofmt: " + ExceptionDetail())
return return
#######################################################################
# hg mail
def mail(ui, repo, *pats, **opts): def mail(ui, repo, *pats, **opts):
"""mail a change for review """mail a change for review
Uploads a patch to the code review server and then sends mail Uploads a patch to the code review server and then sends mail
to the reviewer and CC list asking for a review. to the reviewer and CC list asking for a review.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc) cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc)
if err != "": if err != "":
...@@ -1623,63 +1773,55 @@ def mail(ui, repo, *pats, **opts): ...@@ -1623,63 +1773,55 @@ def mail(ui, repo, *pats, **opts):
cl.Mail(ui, repo) cl.Mail(ui, repo)
#######################################################################
# hg p / hg pq / hg ps / hg pending
def ps(ui, repo, *pats, **opts):
"""alias for hg p --short
"""
opts['short'] = True
return pending(ui, repo, *pats, **opts)
def pq(ui, repo, *pats, **opts):
"""alias for hg p --quick
"""
opts['quick'] = True
return pending(ui, repo, *pats, **opts)
def pending(ui, repo, *pats, **opts): def pending(ui, repo, *pats, **opts):
"""show pending changes """show pending changes
Lists pending changes followed by a list of unassigned but modified files. Lists pending changes followed by a list of unassigned but modified files.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
m = LoadAllCL(ui, repo, web=True) quick = opts.get('quick', False)
short = opts.get('short', False)
m = LoadAllCL(ui, repo, web=not quick and not short)
names = m.keys() names = m.keys()
names.sort() names.sort()
for name in names: for name in names:
cl = m[name] cl = m[name]
ui.write(cl.PendingText() + "\n") if short:
ui.write(name + "\t" + line1(cl.desc) + "\n")
else:
ui.write(cl.PendingText(quick=quick) + "\n")
files = DefaultFiles(ui, repo, [], opts) if short:
return
files = DefaultFiles(ui, repo, [])
if len(files) > 0: if len(files) > 0:
s = "Changed files not in any CL:\n" s = "Changed files not in any CL:\n"
for f in files: for f in files:
s += "\t" + f + "\n" s += "\t" + f + "\n"
ui.write(s) ui.write(s)
def reposetup(ui, repo): #######################################################################
global original_match # hg submit
if original_match is None:
global global_repo, global_ui
global_repo = repo
global_ui = ui
start_status_thread()
original_match = scmutil.match
scmutil.match = ReplacementForCmdutilMatch
RietveldSetup(ui, repo)
def CheckContributor(ui, repo, user=None):
set_status("checking CONTRIBUTORS file")
user, userline = FindContributor(ui, repo, user, warn=False)
if not userline:
raise util.Abort("cannot find %s in CONTRIBUTORS" % (user,))
return userline
def FindContributor(ui, repo, user=None, warn=True):
if not user:
user = ui.config("ui", "username")
if not user:
raise util.Abort("[ui] username is not configured in .hgrc")
user = user.lower()
m = re.match(r".*<(.*)>", user)
if m:
user = m.group(1)
if user not in contributors:
if warn:
ui.warn("warning: cannot find %s in CONTRIBUTORS\n" % (user,))
return user, None
user, email = contributors[user] def need_sync():
return email, "%s <%s>" % (user, email) raise hg_util.Abort("local repository out of date; must sync before submit")
def submit(ui, repo, *pats, **opts): def submit(ui, repo, *pats, **opts):
"""submit change to remote repository """submit change to remote repository
...@@ -1687,16 +1829,14 @@ def submit(ui, repo, *pats, **opts): ...@@ -1687,16 +1829,14 @@ def submit(ui, repo, *pats, **opts):
Submits change to remote repository. Submits change to remote repository.
Bails out if the local repository is not in sync with the remote one. Bails out if the local repository is not in sync with the remote one.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
# We already called this on startup but sometimes Mercurial forgets. # We already called this on startup but sometimes Mercurial forgets.
set_mercurial_encoding_to_utf8() set_mercurial_encoding_to_utf8()
other = getremote(ui, repo, opts) if not opts["no_incoming"] and hg_incoming(ui, repo):
repo.ui.quiet = True need_sync()
if not opts["no_incoming"] and incoming(repo, other):
return "local repository out of date; must sync before submit"
cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc) cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc)
if err != "": if err != "":
...@@ -1742,58 +1882,45 @@ def submit(ui, repo, *pats, **opts): ...@@ -1742,58 +1882,45 @@ def submit(ui, repo, *pats, **opts):
cl.Mail(ui, repo) cl.Mail(ui, repo)
# submit changes locally # submit changes locally
date = opts.get('date') message = cl.desc.rstrip() + "\n\n" + about
if date: typecheck(message, str)
opts['date'] = util.parsedate(date)
typecheck(opts['date'], str)
opts['message'] = cl.desc.rstrip() + "\n\n" + about
typecheck(opts['message'], str)
if opts['dryrun']:
print "NOT SUBMITTING:"
print "User: ", userline
print "Message:"
print Indent(opts['message'], "\t")
print "Files:"
print Indent('\n'.join(cl.files), "\t")
return "dry run; not submitted"
set_status("pushing " + cl.name + " to remote server") set_status("pushing " + cl.name + " to remote server")
other = getremote(ui, repo, opts) if hg_outgoing(ui, repo):
if outgoing(repo): raise hg_util.Abort("local repository corrupt or out-of-phase with remote: found outgoing changes")
raise util.Abort("local repository corrupt or out-of-phase with remote: found outgoing changes")
m = match.exact(repo.root, repo.getcwd(), cl.files) old_heads = len(hg_heads(ui, repo).split())
node = repo.commit(ustr(opts['message']), ustr(userline), opts.get('date'), m)
if not node:
return "nothing changed"
global commit_okay
commit_okay = True
ret = hg_commit(ui, repo, *['path:'+f for f in cl.files], message=message, user=userline)
commit_okay = False
if ret:
return "nothing changed"
node = repo["-1"].node()
# push to remote; if it fails for any reason, roll back # push to remote; if it fails for any reason, roll back
try: try:
log = repo.changelog new_heads = len(hg_heads(ui, repo).split())
rev = log.rev(node) if old_heads != new_heads:
parents = log.parentrevs(rev) # Created new head, so we weren't up to date.
if (rev-1 not in parents and need_sync()
(parents == (nullrev, nullrev) or
len(log.heads(log.node(parents[0]))) > 1 and # Push changes to remote. If it works, we're committed. If not, roll back.
(parents[1] == nullrev or len(log.heads(log.node(parents[1]))) > 1))): try:
# created new head hg_push(ui, repo)
raise util.Abort("local repository out of date; must sync before submit") except hg_error.Abort, e:
if e.message.find("push creates new heads") >= 0:
# push changes to remote. # Remote repository had changes we missed.
# if it works, we're committed. need_sync()
# if not, roll back raise
r = repo.push(other, False, None)
if r == 0:
raise util.Abort("local repository out of date; must sync before submit")
except: except:
real_rollback() real_rollback()
raise raise
# we're committed. upload final patch, close review, add commit message # We're committed. Upload final patch, close review, add commit message.
changeURL = short(node) changeURL = hg_node.short(node)
url = other.url() url = ui.expandpath("default")
m = re.match("^https?://([^@/]+@)?([^.]+)\.googlecode\.com/hg/?", url) m = re.match("^https?://([^@/]+@)?([^.]+)\.googlecode\.com/hg/?", url)
if m: if m:
changeURL = "http://code.google.com/p/%s/source/detail?r=%s" % (m.group(2), changeURL) changeURL = "http://code.google.com/p/%s/source/detail?r=%s" % (m.group(2), changeURL)
...@@ -1812,49 +1939,34 @@ def submit(ui, repo, *pats, **opts): ...@@ -1812,49 +1939,34 @@ def submit(ui, repo, *pats, **opts):
c = repo[None] c = repo[None]
if c.branch() == releaseBranch and not c.modified() and not c.added() and not c.removed(): if c.branch() == releaseBranch and not c.modified() and not c.added() and not c.removed():
ui.write("switching from %s to default branch.\n" % releaseBranch) ui.write("switching from %s to default branch.\n" % releaseBranch)
err = hg.clean(repo, "default") err = hg_clean(repo, "default")
if err: if err:
return err return err
return None return None
#######################################################################
# hg sync
def sync(ui, repo, **opts): def sync(ui, repo, **opts):
"""synchronize with remote repository """synchronize with remote repository
Incorporates recent changes from the remote repository Incorporates recent changes from the remote repository
into the local repository. into the local repository.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
if not opts["local"]: if not opts["local"]:
ui.status = sync_note err = hg_pull(ui, repo, update=True)
ui.note = sync_note
other = getremote(ui, repo, opts)
modheads = repo.pull(other)
err = commands.postincoming(ui, repo, modheads, True, "tip")
if err: if err:
return err return err
commands.update(ui, repo, rev="default")
sync_changes(ui, repo) sync_changes(ui, repo)
def sync_note(msg):
# we run sync (pull -u) in verbose mode to get the
# list of files being updated, but that drags along
# a bunch of messages we don't care about.
# omit them.
if msg == 'resolving manifests\n':
return
if msg == 'searching for changes\n':
return
if msg == "couldn't find merge tool hgmerge\n":
return
sys.stdout.write(msg)
def sync_changes(ui, repo): def sync_changes(ui, repo):
# Look through recent change log descriptions to find # Look through recent change log descriptions to find
# potential references to http://.*/our-CL-number. # potential references to http://.*/our-CL-number.
# Double-check them by looking at the Rietveld log. # Double-check them by looking at the Rietveld log.
def Rev(rev): for rev in hg_log(ui, repo, limit=100, template="{node}\n").split():
desc = repo[rev].description().strip() desc = repo[rev].description().strip()
for clname in re.findall('(?m)^http://(?:[^\n]+)/([0-9]+)$', desc): for clname in re.findall('(?m)^http://(?:[^\n]+)/([0-9]+)$', desc):
if IsLocalCL(ui, repo, clname) and IsRietveldSubmitted(ui, clname, repo[rev].hex()): if IsLocalCL(ui, repo, clname) and IsRietveldSubmitted(ui, clname, repo[rev].hex()):
...@@ -1867,28 +1979,10 @@ def sync_changes(ui, repo): ...@@ -1867,28 +1979,10 @@ def sync_changes(ui, repo):
EditDesc(cl.name, closed=True, private=cl.private) EditDesc(cl.name, closed=True, private=cl.private)
cl.Delete(ui, repo) cl.Delete(ui, repo)
if hgversion < '1.4':
get = util.cachefunc(lambda r: repo[r].changeset())
changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, [], get, {'rev': None})
n = 0
for st, rev, fns in changeiter:
if st != 'iter':
continue
n += 1
if n > 100:
break
Rev(rev)
else:
matchfn = scmutil.match(repo, [], {'rev': None})
def prep(ctx, fns):
pass
for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev': None}, prep):
Rev(ctx.rev())
# Remove files that are not modified from the CLs in which they appear. # Remove files that are not modified from the CLs in which they appear.
all = LoadAllCL(ui, repo, web=False) all = LoadAllCL(ui, repo, web=False)
changed = ChangedFiles(ui, repo, [], {}) changed = ChangedFiles(ui, repo, [])
for _, cl in all.items(): for cl in all.values():
extra = Sub(cl.files, changed) extra = Sub(cl.files, changed)
if extra: if extra:
ui.warn("Removing unmodified files from CL %s:\n" % (cl.name,)) ui.warn("Removing unmodified files from CL %s:\n" % (cl.name,))
...@@ -1903,13 +1997,16 @@ def sync_changes(ui, repo): ...@@ -1903,13 +1997,16 @@ def sync_changes(ui, repo):
ui.warn("CL %s has no files; delete locally with hg change -D %s\n" % (cl.name, cl.name)) ui.warn("CL %s has no files; delete locally with hg change -D %s\n" % (cl.name, cl.name))
return return
#######################################################################
# hg upload
def upload(ui, repo, name, **opts): def upload(ui, repo, name, **opts):
"""upload diffs to the code review server """upload diffs to the code review server
Uploads the current modifications for a given change to the server. Uploads the current modifications for a given change to the server.
""" """
if missing_codereview: if codereview_disabled:
return missing_codereview return codereview_disabled
repo.ui.quiet = True repo.ui.quiet = True
cl, err = LoadCL(ui, repo, name, web=True) cl, err = LoadCL(ui, repo, name, web=True)
...@@ -1921,6 +2018,9 @@ def upload(ui, repo, name, **opts): ...@@ -1921,6 +2018,9 @@ def upload(ui, repo, name, **opts):
print "%s%s\n" % (server_url_base, cl.name) print "%s%s\n" % (server_url_base, cl.name)
return return
#######################################################################
# Table of commands, supplied to Mercurial for installation.
review_opts = [ review_opts = [
('r', 'reviewer', '', 'add reviewer'), ('r', 'reviewer', '', 'add reviewer'),
('', 'cc', '', 'add cc'), ('', 'cc', '', 'add cc'),
...@@ -1979,13 +2079,26 @@ cmdtable = { ...@@ -1979,13 +2079,26 @@ cmdtable = {
), ),
"^pending|p": ( "^pending|p": (
pending, pending,
[
('s', 'short', False, 'show short result form'),
('', 'quick', False, 'do not consult codereview server'),
],
"[FILE ...]"
),
"^ps": (
ps,
[],
"[FILE ...]"
),
"^pq": (
pq,
[], [],
"[FILE ...]" "[FILE ...]"
), ),
"^mail": ( "^mail": (
mail, mail,
review_opts + [ review_opts + [
] + commands.walkopts, ] + hg_commands.walkopts,
"[-r reviewer] [--cc cc] [change# | file ...]" "[-r reviewer] [--cc cc] [change# | file ...]"
), ),
"^release-apply": ( "^release-apply": (
...@@ -2001,8 +2114,7 @@ cmdtable = { ...@@ -2001,8 +2114,7 @@ cmdtable = {
submit, submit,
review_opts + [ review_opts + [
('', 'no_incoming', None, 'disable initial incoming check (for testing)'), ('', 'no_incoming', None, 'disable initial incoming check (for testing)'),
('n', 'dryrun', None, 'make change only locally (for testing)'), ] + hg_commands.walkopts + hg_commands.commitopts + hg_commands.commitopts2,
] + commands.walkopts + commands.commitopts + commands.commitopts2,
"[-r reviewer] [--cc cc] [change# | file ...]" "[-r reviewer] [--cc cc] [change# | file ...]"
), ),
"^sync": ( "^sync": (
...@@ -2027,10 +2139,55 @@ cmdtable = { ...@@ -2027,10 +2139,55 @@ cmdtable = {
), ),
} }
#######################################################################
# Mercurial extension initialization
def norollback(*pats, **opts):
"""(disabled when using this extension)"""
raise hg_util.Abort("codereview extension enabled; use undo instead of rollback")
def reposetup(ui, repo):
global codereview_disabled
global defaultcc
repo_config_path = ''
# Read repository-specific options from lib/codereview/codereview.cfg
try:
repo_config_path = repo.root + '/lib/codereview/codereview.cfg'
f = open(repo_config_path)
for line in f:
if line.startswith('defaultcc: '):
defaultcc = SplitCommaSpace(line[10:])
except:
# If there are no options, chances are good this is not
# a code review repository; stop now before we foul
# things up even worse. Might also be that repo doesn't
# even have a root. See issue 959.
if repo_config_path == '':
codereview_disabled = 'codereview disabled: repository has no root'
else:
codereview_disabled = 'codereview disabled: cannot open ' + repo_config_path
return
InstallMatch(ui, repo)
ReadContributors(ui, repo)
RietveldSetup(ui, repo)
# Disable the Mercurial commands that might change the repository.
# Only commands in this extension are supposed to do that.
ui.setconfig("hooks", "precommit.codereview", precommithook)
# Rollback removes an existing commit. Don't do that either.
global real_rollback
real_rollback = repo.rollback
repo.rollback = norollback
####################################################################### #######################################################################
# Wrappers around upload.py for interacting with Rietveld # Wrappers around upload.py for interacting with Rietveld
from HTMLParser import HTMLParser
# HTML form parser # HTML form parser
class FormParser(HTMLParser): class FormParser(HTMLParser):
def __init__(self): def __init__(self):
...@@ -2106,7 +2263,7 @@ def fix_json(x): ...@@ -2106,7 +2263,7 @@ def fix_json(x):
for k in todel: for k in todel:
del x[k] del x[k]
else: else:
raise util.Abort("unknown type " + str(type(x)) + " in fix_json") raise hg_util.Abort("unknown type " + str(type(x)) + " in fix_json")
if type(x) is str: if type(x) is str:
x = x.replace('\r\n', '\n') x = x.replace('\r\n', '\n')
return x return x
...@@ -2309,68 +2466,13 @@ def PostMessage(ui, issue, message, reviewers=None, cc=None, send_mail=True, sub ...@@ -2309,68 +2466,13 @@ def PostMessage(ui, issue, message, reviewers=None, cc=None, send_mail=True, sub
class opt(object): class opt(object):
pass pass
def nocommit(*pats, **opts):
"""(disabled when using this extension)"""
raise util.Abort("codereview extension enabled; use mail, upload, or submit instead of commit")
def nobackout(*pats, **opts):
"""(disabled when using this extension)"""
raise util.Abort("codereview extension enabled; use undo instead of backout")
def norollback(*pats, **opts):
"""(disabled when using this extension)"""
raise util.Abort("codereview extension enabled; use undo instead of rollback")
def RietveldSetup(ui, repo): def RietveldSetup(ui, repo):
global defaultcc, upload_options, rpc, server, server_url_base, force_google_account, verbosity, contributors global force_google_account
global missing_codereview global rpc
global server
repo_config_path = '' global server_url_base
# Read repository-specific options from lib/codereview/codereview.cfg global upload_options
try: global verbosity
repo_config_path = repo.root + '/lib/codereview/codereview.cfg'
f = open(repo_config_path)
for line in f:
if line.startswith('defaultcc: '):
defaultcc = SplitCommaSpace(line[10:])
except:
# If there are no options, chances are good this is not
# a code review repository; stop now before we foul
# things up even worse. Might also be that repo doesn't
# even have a root. See issue 959.
if repo_config_path == '':
missing_codereview = 'codereview disabled: repository has no root'
else:
missing_codereview = 'codereview disabled: cannot open ' + repo_config_path
return
# Should only modify repository with hg submit.
# Disable the built-in Mercurial commands that might
# trip things up.
cmdutil.commit = nocommit
global real_rollback
real_rollback = repo.rollback
repo.rollback = norollback
# would install nobackout if we could; oh well
try:
f = open(repo.root + '/CONTRIBUTORS', 'r')
except:
raise util.Abort("cannot open %s: %s" % (repo.root+'/CONTRIBUTORS', ExceptionDetail()))
for line in f:
# CONTRIBUTORS is a list of lines like:
# Person <email>
# Person <email> <alt-email>
# The first email address is the one used in commit logs.
if line.startswith('#'):
continue
m = re.match(r"([^<>]+\S)\s+(<[^<>\s]+>)((\s+<[^<>\s]+>)*)\s*$", line)
if m:
name = m.group(1)
email = m.group(2)[1:-1]
contributors[email.lower()] = (name, email)
for extra in m.group(3).split():
contributors[extra[1:-1].lower()] = (name, email)
if not ui.verbose: if not ui.verbose:
verbosity = 0 verbosity = 0
...@@ -2421,7 +2523,7 @@ def RietveldSetup(ui, repo): ...@@ -2421,7 +2523,7 @@ def RietveldSetup(ui, repo):
# answer when comparing release-branch.r99 with # answer when comparing release-branch.r99 with
# release-branch.r100. If we do ten releases a year # release-branch.r100. If we do ten releases a year
# that gives us 4 years before we have to worry about this. # that gives us 4 years before we have to worry about this.
raise util.Abort('tags.sort needs to be fixed for release-branch.r100') raise hg_util.Abort('tags.sort needs to be fixed for release-branch.r100')
tags.sort() tags.sort()
for t in tags: for t in tags:
if t.startswith('release-branch.'): if t.startswith('release-branch.'):
...@@ -3238,9 +3340,9 @@ class MercurialVCS(VersionControlSystem): ...@@ -3238,9 +3340,9 @@ class MercurialVCS(VersionControlSystem):
out = RunShell(["hg", "status", "-C", "--rev", rev]) out = RunShell(["hg", "status", "-C", "--rev", rev])
else: else:
fui = FakeMercurialUI() fui = FakeMercurialUI()
ret = commands.status(fui, self.repo, *[], **{'rev': [rev], 'copies': True}) ret = hg_commands.status(fui, self.repo, *[], **{'rev': [rev], 'copies': True})
if ret: if ret:
raise util.Abort(ret) raise hg_util.Abort(ret)
out = fui.output out = fui.output
self.status = out.splitlines() self.status = out.splitlines()
for i in range(len(self.status)): for i in range(len(self.status)):
...@@ -3253,7 +3355,7 @@ class MercurialVCS(VersionControlSystem): ...@@ -3253,7 +3355,7 @@ class MercurialVCS(VersionControlSystem):
if i+1 < len(self.status) and self.status[i+1][:2] == ' ': if i+1 < len(self.status) and self.status[i+1][:2] == ' ':
return self.status[i:i+2] return self.status[i:i+2]
return self.status[i:i+1] return self.status[i:i+1]
raise util.Abort("no status for " + path) raise hg_util.Abort("no status for " + path)
def GetBaseFile(self, filename): def GetBaseFile(self, filename):
set_status("inspecting " + filename) set_status("inspecting " + filename)
......
#!/bin/bash
# Copyright 2011 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
# Test the code review plugin.
# Assumes a local Rietveld is running using the App Engine SDK
# at http://localhost:7777/
#
# dev_appserver.py -p 7777 $HOME/pub/rietveld
codereview_script=$(pwd)/codereview.py
server=localhost:7777
master=/tmp/go.test
clone1=/tmp/go.test1
clone2=/tmp/go.test2
export HGEDITOR=true
must() {
if ! "$@"; then
echo "$@" failed >&2
exit 1
fi
}
not() {
if "$@"; then
false
else
true
fi
}
status() {
echo '+++' "$@" >&2
}
firstcl() {
hg pending | sed 1q | tr -d ':'
}
# Initial setup.
status Create repositories.
rm -rf $master $clone1 $clone2
mkdir $master
cd $master
must hg init .
echo Initial state >file
must hg add file
must hg ci -m 'first commit' file
must hg clone $master $clone1
must hg clone $master $clone2
echo "
[ui]
username=Grace R Emlin <gre@golang.org>
[extensions]
codereview=$codereview_script
[codereview]
server=$server
" >>$clone1/.hg/hgrc
cp $clone1/.hg/hgrc $clone2/.hg/hgrc
status Codereview should be disabled.
cd $clone1
must hg status
must not hg pending
status Enabling code review.
must mkdir lib lib/codereview
must touch lib/codereview/codereview.cfg
status Code review should work even without CONTRIBUTORS.
must hg pending
status Add CONTRIBUTORS.
echo 'Grace R Emlin <gre@golang.org>' >CONTRIBUTORS
must hg add lib/codereview/codereview.cfg CONTRIBUTORS
status First submit.
must hg submit -r gre@golang.org -m codereview \
lib/codereview/codereview.cfg CONTRIBUTORS
status Should see change in other client.
cd $clone2
must hg pull -u
must test -f lib/codereview/codereview.cfg
must test -f CONTRIBUTORS
test_clpatch() {
# The email address must be test@example.com to match
# the test code review server's default user.
# Clpatch will check.
cd $clone1
# Tried to use UTF-8 here to test that, but dev_appserver.py crashes. Ha ha.
if false; then
status Using UTF-8.
name="Grácè T Emlïn <test@example.com>"
else
status Using ASCII.
name="Grace T Emlin <test@example.com>"
fi
echo "$name" >>CONTRIBUTORS
cat .hg/hgrc | sed "s/Grace.*/$name/" >/tmp/x && mv /tmp/x .hg/hgrc
echo '
Reviewer: gre@golang.org
Description:
CONTRIBUTORS: add $name
Files:
CONTRIBUTORS
' | must hg change -i
num=$(hg pending | sed 1q | tr -d :)
status Patch CL.
cd $clone2
must hg clpatch $num
must [ "$num" = "$(firstcl)" ]
must hg submit $num
status Issue should be open with no reviewers.
must curl http://$server/api/$num >/tmp/x
must not grep '"closed":true' /tmp/x
must grep '"reviewers":\[\]' /tmp/x
status Sync should close issue.
cd $clone1
must hg sync
must curl http://$server/api/$num >/tmp/x
must grep '"closed":true' /tmp/x
must grep '"reviewers":\[\]' /tmp/x
must [ "$(firstcl)" = "" ]
}
test_reviewer() {
status Submit without reviewer should fail.
cd $clone1
echo dummy >dummy
must hg add dummy
echo '
Description:
no reviewer
Files:
dummy
' | must hg change -i
num=$(firstcl)
must not hg submit $num
must hg revert dummy
must rm dummy
must hg change -d $num
}
test_linearity() {
status Linearity of changes.
cd $clone1
echo file1 >file1
must hg add file1
echo '
Reviewer: gre@golang.org
Description: file1
Files: file1
' | must hg change -i
must hg submit $(firstcl)
cd $clone2
echo file2 >file2
must hg add file2
echo '
Reviewer: gre@golang.org
Description: file2
Files: file2
' | must hg change -i
must not hg submit $(firstcl)
must hg sync
must hg submit $(firstcl)
}
test_restrict() {
status Cannot use hg ci.
cd $clone1
echo file1a >file1a
hg add file1a
must not hg ci -m commit file1a
must rm file1a
must hg revert file1a
status Cannot use hg rollback.
must not hg rollback
status Cannot use hg backout
must not hg backout -r -1
}
test_reviewer
test_clpatch
test_linearity
test_restrict
status ALL TESTS PASSED.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment