Commit 5ca38eda authored by Andrew Gerrand's avatar Andrew Gerrand

undo CL 5395044 / d88e746d3785

Breaks with hg 1.6.4. Roll back until it's fixed.

««« original CL description
codereview: cleanup + basic tests

R=adg, bradfitz
CC=golang-dev
https://golang.org/cl/5395044
»»»

R=golang-dev
CC=golang-dev
https://golang.org/cl/5410047
parent 25d2987d
...@@ -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,60 +38,110 @@ For example, if change 123456 contains the files x.go and y.go, ...@@ -38,60 +38,110 @@ 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".
''' '''
import sys from mercurial import cmdutil, commands, hg, util, error, match, discovery
from mercurial.node import nullrev, hex, nullid, short
if __name__ == "__main__": import os, re, time
print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly."
sys.exit(2)
# We require Python 2.6 for the json package.
if sys.version < '2.6':
print >>sys.stderr, "The codereview extension requires Python 2.6 or newer."
print >>sys.stderr, "You are running Python " + sys.version
sys.exit(2)
import json
import os
import re
import stat import stat
import subprocess import subprocess
import threading import threading
import time from HTMLParser import HTMLParser
from mercurial import commands as hg_commands # The standard 'json' package is new in Python 2.6.
from mercurial import util as hg_util # 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
defaultcc = None try:
codereview_disabled = None hgversion = util.version()
real_rollback = None except:
releaseBranch = None from mercurial.version import version as v
server = "codereview.appspot.com" hgversion = v.get_version()
server_url_base = None
####################################################################### # in Mercurial 1.9 the cmdutil.match and cmdutil.revpair moved to scmutil
# Normally I would split this into multiple files, but it simplifies if hgversion >= '1.9':
# import path headaches to keep it all in one file. Sorry. from mercurial import scmutil
# The different parts of the file are separated by banners like this one. else:
scmutil = cmdutil
####################################################################### oldMessage = """
# Helpers The code review extension requires Mercurial 1.3 or newer.
def RelativePath(path, cwd): To install a new Mercurial,
n = len(cwd)
if path.startswith(cwd) and path[n] == '/':
return path[n+1:]
return path
def Sub(l1, l2): sudo easy_install mercurial
return [l for l in l1 if l not in l2]
def Add(l1, l2): works on most systems.
l = l1 + Sub(l2, l1) """
l.sort()
return l
def Intersect(l1, l2): linuxMessage = """
return [l for l in l1 if l in l2] 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':
msg = oldMessage
if os.access("/etc/mercurial", 0):
msg += linuxMessage
raise util.Abort(msg)
def promptyesno(ui, msg):
# Arguments to ui.prompt changed between 1.3 and 1.3.1.
# Even so, some 1.3.1 distributions seem to have the old prompt!?!?
# What a terrible way to maintain software.
try:
return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0
except AttributeError:
return ui.prompt(msg, ["&yes", "&no"], "y") != "n"
def incoming(repo, other):
fui = FakeMercurialUI()
ret = commands.incoming(fui, repo, *[other.path], **{'bundle': '', 'force': False})
if ret and ret != 1:
raise util.Abort(ret)
out = fui.output
return out
def outgoing(repo):
fui = FakeMercurialUI()
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:
# >>> repo = hg.repository(ui.ui(), path = ".")
#######################################################################
# Normally I would split this into multiple files, but it simplifies
# import path headaches to keep it all in one file. Sorry.
import sys
if __name__ == "__main__":
print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly."
sys.exit(2)
server = "codereview.appspot.com"
server_url_base = None
defaultcc = None
contributors = {}
missing_codereview = None
real_rollback = None
releaseBranch = None
####################################################################### #######################################################################
# RE: UNICODE STRING HANDLING # RE: UNICODE STRING HANDLING
...@@ -118,7 +168,7 @@ def Intersect(l1, l2): ...@@ -118,7 +168,7 @@ def Intersect(l1, l2):
def typecheck(s, t): def typecheck(s, t):
if type(s) != t: if type(s) != t:
raise hg_util.Abort("type check failed: %s has type %s != %s" % (repr(s), type(s), t)) raise 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):
...@@ -149,40 +199,6 @@ def default_to_utf8(): ...@@ -149,40 +199,6 @@ 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.
# #
...@@ -259,18 +275,17 @@ class CL(object): ...@@ -259,18 +275,17 @@ class CL(object):
typecheck(s, str) typecheck(s, str)
return s return s
def PendingText(self, quick=False): def PendingText(self):
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" s += "\tCC: " + JoinComma(cl.cc) + "\n"
s += "\tCC: " + JoinComma(cl.cc) + "\n"
s += "\tFiles:\n" s += "\tFiles:\n"
for f in cl.files: for f in cl.files:
s += "\t\t" + f + "\n" s += "\t\t" + f + "\n"
...@@ -345,7 +360,7 @@ class CL(object): ...@@ -345,7 +360,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 + " " + ui.expandpath("default"))) form_fields.append(("subject", "diff -r " + vcs.base_rev + " " + getremote(ui, repo, {}).path))
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()))
...@@ -364,7 +379,7 @@ class CL(object): ...@@ -364,7 +379,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 hg_util.Abort("failed to update issue: " + response_body) raise 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:
...@@ -389,7 +404,7 @@ class CL(object): ...@@ -389,7 +404,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 = ui.expandpath("default") repourl = getremote(ui, repo, {}).path
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:
...@@ -552,6 +567,37 @@ def LoadCL(ui, repo, name, web=True): ...@@ -552,6 +567,37 @@ 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)
...@@ -689,6 +735,101 @@ _change_prolog = """# Change list. ...@@ -689,6 +735,101 @@ _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.
...@@ -710,17 +851,15 @@ Examples: ...@@ -710,17 +851,15 @@ 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 hg_commands.remove(ui, repo, 'path:'+f) != 0: if 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 hg_commands.add(ui, repo, 'path:'+f) != 0: if 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):
...@@ -760,14 +899,10 @@ def EditCL(ui, repo, cl): ...@@ -760,14 +899,10 @@ 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]
changed = hg_matchPattern(ui, repo, *pats, modified=True, added=True, removed=True) modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, pats, {})
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 changed: if f in modified or f in added or f in removed:
files.append(f) files.append(f)
continue continue
if f in deleted: if f in deleted:
...@@ -807,392 +942,43 @@ def EditCL(ui, repo, cl): ...@@ -807,392 +942,43 @@ def EditCL(ui, repo, cl):
# For use by submit, etc. (NOT by change) # For use by submit, etc. (NOT by change)
# Get change list number or list of files from command line. # Get change list number or list of files from command line.
# If files are given, make a new change list. # If files are given, make a new change list.
def CommandLineCL(ui, repo, pats, opts, defaultcc=None): def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
if len(pats) > 0 and GoodCLName(pats[0]): if len(pats) > 0 and GoodCLName(pats[0]):
if len(pats) != 1: if len(pats) != 1:
return None, "cannot specify change number and file names" return None, "cannot specify change number and file names"
if opts.get('message'): if opts.get('message'):
return None, "cannot use -m with existing CL" return None, "cannot use -m with existing CL"
cl, err = LoadCL(ui, repo, pats[0], web=True) cl, err = LoadCL(ui, repo, pats[0], web=True)
if err != "": if err != "":
return None, err return None, err
else: else:
cl = CL("new") cl = CL("new")
cl.local = True cl.local = True
cl.files = ChangedFiles(ui, repo, pats, taken=Taken(ui, repo)) cl.files = ChangedFiles(ui, repo, pats, opts, 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'):
cl.reviewer = Add(cl.reviewer, SplitCommaSpace(opts.get('reviewer'))) cl.reviewer = Add(cl.reviewer, SplitCommaSpace(opts.get('reviewer')))
if opts.get('cc'): if opts.get('cc'):
cl.cc = Add(cl.cc, SplitCommaSpace(opts.get('cc'))) cl.cc = Add(cl.cc, SplitCommaSpace(opts.get('cc')))
if defaultcc: if defaultcc:
cl.cc = Add(cl.cc, defaultcc) cl.cc = Add(cl.cc, defaultcc)
if cl.name == "new": if cl.name == "new":
if opts.get('message'): if opts.get('message'):
cl.desc = opts.get('message') cl.desc = opts.get('message')
else: else:
err = EditCL(ui, repo, cl) err = EditCL(ui, repo, cl)
if err != '': if err != '':
return None, err return None, err
return cl, "" return cl, ""
#######################################################################
# Change list file management
# 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, 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'): # reposetup replaces cmdutil.match with this wrapper,
# which expands the syntax @clnumber to mean the files
# in that CL.
original_match = None
global_repo = None
global_ui = None
def ReplacementForCmdutilMatch(ctx, pats=None, opts=None, globbed=False, default='relpath'):
taken = [] taken = []
files = [] files = []
pats = pats or [] pats = pats or []
...@@ -1202,29 +988,101 @@ def MatchAt(ctx, pats=None, opts=None, globbed=False, default='relpath'): ...@@ -1202,29 +988,101 @@ def MatchAt(ctx, pats=None, opts=None, globbed=False, default='relpath'):
if p.startswith('@'): if p.startswith('@'):
taken.append(p) taken.append(p)
clname = p[1:] clname = p[1:]
if clname == "default": if not GoodCLName(clname):
files = DefaultFiles(match_ui, match_repo, []) raise util.Abort("invalid CL name " + clname)
else: cl, err = LoadCL(global_repo.ui, global_repo, clname, web=False)
if not GoodCLName(clname): if err != '':
raise hg_util.Abort("invalid CL name " + clname) raise util.Abort("loading CL " + clname + ": " + err)
cl, err = LoadCL(match_repo.ui, match_repo, clname, web=False) if not cl.files:
if err != '': raise util.Abort("no files in CL " + clname)
raise hg_util.Abort("loading CL " + clname + ": " + err) files = Add(files, cl.files)
if not cl.files:
raise hg_util.Abort("no files in CL " + clname)
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 not hasattr(ctx, 'match'): if hgversion >= '1.9' and not hasattr(ctx, 'match'):
ctx = ctx[None] ctx = ctx[None]
return match_orig(ctx, pats=pats, opts=opts, globbed=globbed, default=default) return original_match(ctx, pats=pats, opts=opts, globbed=globbed, default=default)
####################################################################### def RelativePath(path, cwd):
# Commands added by code review extension. 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
####################################################################### #######################################################################
# hg change # Mercurial commands
# every command must take a ui and and repo as arguments.
# opts is a dict where you can find other command line flags
#
# 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
...@@ -1248,8 +1106,8 @@ def change(ui, repo, *pats, **opts): ...@@ -1248,8 +1106,8 @@ def change(ui, repo, *pats, **opts):
before running hg change -d 123456. before running hg change -d 123456.
""" """
if codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
dirty = {} dirty = {}
if len(pats) > 0 and GoodCLName(pats[0]): if len(pats) > 0 and GoodCLName(pats[0]):
...@@ -1263,12 +1121,12 @@ def change(ui, repo, *pats, **opts): ...@@ -1263,12 +1121,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, taken=Taken(ui, repo)) files = ChangedFiles(ui, repo, pats, opts, 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"]:
...@@ -1336,24 +1194,17 @@ def change(ui, repo, *pats, **opts): ...@@ -1336,24 +1194,17 @@ 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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
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
...@@ -1423,16 +1274,16 @@ def release_apply(ui, repo, clname, **opts): ...@@ -1423,16 +1274,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 hg_util.Abort("uncommitted local changes - cannot switch branches") raise 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 hg_util.Abort(err) raise 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
...@@ -1467,10 +1318,14 @@ backportFooter = """ ...@@ -1467,10 +1318,14 @@ 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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
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),
...@@ -1478,8 +1333,12 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1478,8 +1333,12 @@ 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):
for r in hg_log(ui, repo, keyword="codereview.appspot.com/"+clname, limit=100, template="{node}\n").split(): found = False
rev = repo[r] matchfn = scmutil.match(repo, [], {'rev': None})
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)
...@@ -1497,7 +1356,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1497,7 +1356,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 = hg_node.short(rev.node()) vers = short(rev.node())
cl = CL("new") cl = CL("new")
desc = str(rev.description()) desc = str(rev.description())
if mode == "undo": if mode == "undo":
...@@ -1505,7 +1364,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1505,7 +1364,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 = hg_node.short(rev.parents()[0].node()) v0 = short(rev.parents()[0].node())
if mode == "undo": if mode == "undo":
arg = v1 + ":" + v0 arg = v1 + ":" + v0
else: else:
...@@ -1523,7 +1382,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1523,7 +1382,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([hg_node.short(p.node()) for p in parents]) id = '+'.join([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.
...@@ -1556,7 +1415,7 @@ def clpatch_or_undo(ui, repo, clname, opts, mode): ...@@ -1556,7 +1415,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, []) files = ChangedFiles(ui, repo, [], opts)
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")
...@@ -1636,8 +1495,8 @@ def download(ui, repo, clname, **opts): ...@@ -1636,8 +1495,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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
cl, vers, patch, err = DownloadCL(ui, repo, clname) cl, vers, patch, err = DownloadCL(ui, repo, clname)
if err != "": if err != "":
...@@ -1646,9 +1505,6 @@ def download(ui, repo, clname, **opts): ...@@ -1646,9 +1505,6 @@ 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
...@@ -1657,8 +1513,8 @@ def file(ui, repo, clname, pat, *pats, **opts): ...@@ -1657,8 +1513,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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
pats = tuple([pat] + list(pats)) pats = tuple([pat] + list(pats))
if not GoodCLName(clname): if not GoodCLName(clname):
...@@ -1671,7 +1527,7 @@ def file(ui, repo, clname, pat, *pats, **opts): ...@@ -1671,7 +1527,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) files = ChangedFiles(ui, repo, pats, opts)
if opts["delete"]: if opts["delete"]:
oldfiles = Intersect(files, cl.files) oldfiles = Intersect(files, cl.files)
...@@ -1711,17 +1567,14 @@ def file(ui, repo, clname, pat, *pats, **opts): ...@@ -1711,17 +1567,14 @@ 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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
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")]
...@@ -1734,24 +1587,21 @@ def gofmt(ui, repo, *pats, **opts): ...@@ -1734,24 +1587,21 @@ 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 hg_util.Abort("gofmt did not exit cleanly") raise util.Abort("gofmt did not exit cleanly")
except hg_error.Abort, e: except error.Abort, e:
raise raise
except: except:
raise hg_util.Abort("gofmt: " + ExceptionDetail()) raise 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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc) cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc)
if err != "": if err != "":
...@@ -1773,55 +1623,63 @@ def mail(ui, repo, *pats, **opts): ...@@ -1773,55 +1623,63 @@ 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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
quick = opts.get('quick', False) m = LoadAllCL(ui, repo, web=True)
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]
if short: ui.write(cl.PendingText() + "\n")
ui.write(name + "\t" + line1(cl.desc) + "\n")
else:
ui.write(cl.PendingText(quick=quick) + "\n")
if short: files = DefaultFiles(ui, repo, [], opts)
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):
# hg submit global original_match
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)
def need_sync(): if user not in contributors:
raise hg_util.Abort("local repository out of date; must sync before submit") 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)
def submit(ui, repo, *pats, **opts): def submit(ui, repo, *pats, **opts):
"""submit change to remote repository """submit change to remote repository
...@@ -1829,14 +1687,16 @@ def submit(ui, repo, *pats, **opts): ...@@ -1829,14 +1687,16 @@ 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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
# 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()
if not opts["no_incoming"] and hg_incoming(ui, repo): other = getremote(ui, repo, opts)
need_sync() repo.ui.quiet = True
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 != "":
...@@ -1882,45 +1742,58 @@ def submit(ui, repo, *pats, **opts): ...@@ -1882,45 +1742,58 @@ def submit(ui, repo, *pats, **opts):
cl.Mail(ui, repo) cl.Mail(ui, repo)
# submit changes locally # submit changes locally
message = cl.desc.rstrip() + "\n\n" + about date = opts.get('date')
typecheck(message, str) if date:
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")
if hg_outgoing(ui, repo): other = getremote(ui, repo, opts)
raise hg_util.Abort("local repository corrupt or out-of-phase with remote: found outgoing changes") if outgoing(repo):
raise util.Abort("local repository corrupt or out-of-phase with remote: found outgoing changes")
old_heads = len(hg_heads(ui, repo).split())
global commit_okay m = match.exact(repo.root, repo.getcwd(), cl.files)
commit_okay = True node = repo.commit(ustr(opts['message']), ustr(userline), opts.get('date'), m)
ret = hg_commit(ui, repo, *['path:'+f for f in cl.files], message=message, user=userline) if not node:
commit_okay = False
if ret:
return "nothing changed" 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:
new_heads = len(hg_heads(ui, repo).split()) log = repo.changelog
if old_heads != new_heads: rev = log.rev(node)
# Created new head, so we weren't up to date. parents = log.parentrevs(rev)
need_sync() if (rev-1 not in parents and
(parents == (nullrev, nullrev) or
# Push changes to remote. If it works, we're committed. If not, roll back. len(log.heads(log.node(parents[0]))) > 1 and
try: (parents[1] == nullrev or len(log.heads(log.node(parents[1]))) > 1))):
hg_push(ui, repo) # created new head
except hg_error.Abort, e: raise util.Abort("local repository out of date; must sync before submit")
if e.message.find("push creates new heads") >= 0:
# Remote repository had changes we missed. # push changes to remote.
need_sync() # if it works, we're committed.
raise # if not, roll back
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 = hg_node.short(node) changeURL = short(node)
url = ui.expandpath("default") url = other.url()
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)
...@@ -1935,38 +1808,53 @@ def submit(ui, repo, *pats, **opts): ...@@ -1935,38 +1808,53 @@ def submit(ui, repo, *pats, **opts):
if not cl.copied_from: if not cl.copied_from:
EditDesc(cl.name, closed=True, private=cl.private) EditDesc(cl.name, closed=True, private=cl.private)
cl.Delete(ui, repo) cl.Delete(ui, repo)
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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
if not opts["local"]: if not opts["local"]:
err = hg_pull(ui, repo, update=True) ui.status = sync_note
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.
for rev in hg_log(ui, repo, limit=100, template="{node}\n").split(): def Rev(rev):
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()):
...@@ -1979,10 +1867,28 @@ def sync_changes(ui, repo): ...@@ -1979,10 +1867,28 @@ 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.values(): for _, cl in all.items():
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,))
...@@ -1997,16 +1903,13 @@ def sync_changes(ui, repo): ...@@ -1997,16 +1903,13 @@ 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 codereview_disabled: if missing_codereview:
return codereview_disabled return missing_codereview
repo.ui.quiet = True repo.ui.quiet = True
cl, err = LoadCL(ui, repo, name, web=True) cl, err = LoadCL(ui, repo, name, web=True)
...@@ -2018,9 +1921,6 @@ def upload(ui, repo, name, **opts): ...@@ -2018,9 +1921,6 @@ 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'),
...@@ -2079,26 +1979,13 @@ cmdtable = { ...@@ -2079,26 +1979,13 @@ 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 + [
] + hg_commands.walkopts, ] + commands.walkopts,
"[-r reviewer] [--cc cc] [change# | file ...]" "[-r reviewer] [--cc cc] [change# | file ...]"
), ),
"^release-apply": ( "^release-apply": (
...@@ -2114,7 +2001,8 @@ cmdtable = { ...@@ -2114,7 +2001,8 @@ 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)'),
] + hg_commands.walkopts + hg_commands.commitopts + hg_commands.commitopts2, ('n', 'dryrun', None, 'make change only locally (for testing)'),
] + commands.walkopts + commands.commitopts + commands.commitopts2,
"[-r reviewer] [--cc cc] [change# | file ...]" "[-r reviewer] [--cc cc] [change# | file ...]"
), ),
"^sync": ( "^sync": (
...@@ -2139,55 +2027,10 @@ cmdtable = { ...@@ -2139,55 +2027,10 @@ 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):
...@@ -2263,7 +2106,7 @@ def fix_json(x): ...@@ -2263,7 +2106,7 @@ def fix_json(x):
for k in todel: for k in todel:
del x[k] del x[k]
else: else:
raise hg_util.Abort("unknown type " + str(type(x)) + " in fix_json") raise 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
...@@ -2466,13 +2309,68 @@ def PostMessage(ui, issue, message, reviewers=None, cc=None, send_mail=True, sub ...@@ -2466,13 +2309,68 @@ 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 force_google_account global defaultcc, upload_options, rpc, server, server_url_base, force_google_account, verbosity, contributors
global rpc global missing_codereview
global server
global server_url_base repo_config_path = ''
global upload_options # Read repository-specific options from lib/codereview/codereview.cfg
global verbosity 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 == '':
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
...@@ -2523,7 +2421,7 @@ def RietveldSetup(ui, repo): ...@@ -2523,7 +2421,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 hg_util.Abort('tags.sort needs to be fixed for release-branch.r100') raise 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.'):
...@@ -3340,9 +3238,9 @@ class MercurialVCS(VersionControlSystem): ...@@ -3340,9 +3238,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 = hg_commands.status(fui, self.repo, *[], **{'rev': [rev], 'copies': True}) ret = commands.status(fui, self.repo, *[], **{'rev': [rev], 'copies': True})
if ret: if ret:
raise hg_util.Abort(ret) raise 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)):
...@@ -3355,7 +3253,7 @@ class MercurialVCS(VersionControlSystem): ...@@ -3355,7 +3253,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 hg_util.Abort("no status for " + path) raise 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