codereview: sync from Go.

LGTM=rsc
R=rsc
https://codereview.appspot.com/67820044
This commit is contained in:
Shenghou Ma 2014-02-24 01:21:48 -05:00
parent 66ad987412
commit db800afb4e

View file

@ -61,6 +61,14 @@ import time
from mercurial import commands as hg_commands from mercurial import commands as hg_commands
from mercurial import util as hg_util from mercurial import util as hg_util
# bind Plan 9 preferred dotfile location
if os.sys.platform == 'plan9':
try:
import plan9
n = plan9.bind(os.path.expanduser("~/lib"), os.path.expanduser("~"), plan9.MBEFORE|plan9.MCREATE)
except ImportError:
pass
defaultcc = None defaultcc = None
codereview_disabled = None codereview_disabled = None
real_rollback = None real_rollback = None
@ -155,7 +163,8 @@ default_to_utf8()
global_status = None global_status = None
def set_status(s): def set_status(s):
# print >>sys.stderr, "\t", time.asctime(), s if verbosity > 0:
print >>sys.stderr, time.asctime(), s
global global_status global global_status
global_status = s global_status = s
@ -268,7 +277,7 @@ class CL(object):
s += "\tAuthor: " + cl.copied_from + "\n" s += "\tAuthor: " + cl.copied_from + "\n"
if not quick: 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"
@ -358,6 +367,8 @@ class CL(object):
msg = lines[0] msg = lines[0]
patchset = lines[1].strip() patchset = lines[1].strip()
patches = [x.split(" ", 1) for x in lines[2:]] patches = [x.split(" ", 1) for x in lines[2:]]
else:
print >>sys.stderr, "Server says there is nothing to upload (probably wrong):\n" + msg
if response_body.startswith("Issue updated.") and quiet: if response_body.startswith("Issue updated.") and quiet:
pass pass
else: else:
@ -484,9 +495,15 @@ def CutDomain(s):
return s return s
def JoinComma(l): def JoinComma(l):
seen = {}
uniq = []
for s in l: for s in l:
typecheck(s, str) typecheck(s, str)
return ", ".join(l) if s not in seen:
seen[s] = True
uniq.append(s)
return ", ".join(uniq)
def ExceptionDetail(): def ExceptionDetail():
s = str(sys.exc_info()[0]) s = str(sys.exc_info()[0])
@ -544,10 +561,10 @@ def LoadCL(ui, repo, name, web=True):
cl.private = d.get('private', False) != False cl.private = d.get('private', False) != False
cl.lgtm = [] cl.lgtm = []
for m in d.get('messages', []): for m in d.get('messages', []):
if m.get('approval', False) == True: if m.get('approval', False) == True or m.get('disapproval', False) == True:
who = re.sub('@.*', '', m.get('sender', '')) who = re.sub('@.*', '', m.get('sender', ''))
text = re.sub("\n(.|\n)*", '', m.get('text', '')) text = re.sub("\n(.|\n)*", '', m.get('text', ''))
cl.lgtm.append((who, text)) cl.lgtm.append((who, text, m.get('approval', False)))
set_status("loaded CL " + name) set_status("loaded CL " + name)
return cl, '' return cl, ''
@ -711,7 +728,10 @@ Examples:
''' '''
def promptyesno(ui, msg): def promptyesno(ui, msg):
return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0 if hgversion >= "2.7":
return ui.promptchoice(msg + " $$ &yes $$ &no", 0) == 0
else:
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,)):
@ -807,7 +827,7 @@ 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, op="verb", 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"
@ -821,7 +841,7 @@ def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
cl.local = True cl.local = True
cl.files = ChangedFiles(ui, repo, pats, 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 (use hg %s <number> to use existing CL)" % op
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'):
@ -972,7 +992,7 @@ def ReadContributors(ui, repo):
f = open(repo.root + '/CONTRIBUTORS', 'r') f = open(repo.root + '/CONTRIBUTORS', 'r')
except: except:
ui.write("warning: cannot open %s: %s\n" % (opening, ExceptionDetail())) ui.write("warning: cannot open %s: %s\n" % (opening, ExceptionDetail()))
return return {}
contributors = {} contributors = {}
for line in f: for line in f:
@ -1027,23 +1047,19 @@ def FindContributor(ui, repo, user=None, warn=True):
hgversion = hg_util.version() hgversion = hg_util.version()
# We require Mercurial 1.9 and suggest Mercurial 2.0. # We require Mercurial 1.9 and suggest Mercurial 2.1.
# The details of the scmutil package changed then, # The details of the scmutil package changed then,
# so allowing earlier versions would require extra band-aids below. # so allowing earlier versions would require extra band-aids below.
# Ubuntu 11.10 ships with Mercurial 1.9.1 as the default version. # Ubuntu 11.10 ships with Mercurial 1.9.1 as the default version.
hg_required = "1.9" hg_required = "1.9"
hg_suggested = "2.0" hg_suggested = "2.1"
old_message = """ old_message = """
The code review extension requires Mercurial """+hg_required+""" or newer. The code review extension requires Mercurial """+hg_required+""" or newer.
You are using Mercurial """+hgversion+""". You are using Mercurial """+hgversion+""".
To install a new Mercurial, use To install a new Mercurial, visit http://mercurial.selenic.com/downloads/.
sudo easy_install mercurial=="""+hg_suggested+"""
or visit http://mercurial.selenic.com/downloads/.
""" """
linux_message = """ linux_message = """
@ -1171,6 +1187,25 @@ def hg_pull(ui, repo, **opts):
ui.write(line + '\n') ui.write(line + '\n')
return err return err
def hg_update(ui, repo, **opts):
w = uiwrap(ui)
ui.quiet = False
ui.verbose = True # for file list
err = hg_commands.update(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): def hg_push(ui, repo, **opts):
w = uiwrap(ui) w = uiwrap(ui)
ui.quiet = False ui.quiet = False
@ -1190,6 +1225,10 @@ def hg_commit(ui, repo, *pats, **opts):
commit_okay = False commit_okay = False
def precommithook(ui, repo, **opts): def precommithook(ui, repo, **opts):
if hgversion >= "2.1":
from mercurial import phases
if repo.ui.config('phases', 'new-commit') >= phases.secret:
return False
if commit_okay: if commit_okay:
return False # False means okay. return False # False means okay.
ui.write("\ncodereview extension enabled; use mail, upload, or submit instead of commit\n\n") ui.write("\ncodereview extension enabled; use mail, upload, or submit instead of commit\n\n")
@ -1247,24 +1286,8 @@ def MatchAt(ctx, pats=None, opts=None, globbed=False, default='relpath'):
####################################################################### #######################################################################
# Commands added by code review extension. # Commands added by code review extension.
# As of Mercurial 2.1 the commands are all required to return integer
# exit codes, whereas earlier versions allowed returning arbitrary strings
# to be printed as errors. We wrap the old functions to make sure we
# always return integer exit codes now. Otherwise Mercurial dies
# with a TypeError traceback (unsupported operand type(s) for &: 'str' and 'int').
# Introduce a Python decorator to convert old functions to the new
# stricter convention.
def hgcommand(f): def hgcommand(f):
def wrapped(ui, repo, *pats, **opts): return f
err = f(ui, repo, *pats, **opts)
if type(err) is int:
return err
if not err:
return 0
raise hg_util.Abort(err)
wrapped.__doc__ = f.__doc__
return wrapped
####################################################################### #######################################################################
# hg change # hg change
@ -1293,42 +1316,42 @@ def change(ui, repo, *pats, **opts):
""" """
if codereview_disabled: if codereview_disabled:
return codereview_disabled raise hg_util.Abort(codereview_disabled)
dirty = {} dirty = {}
if len(pats) > 0 and GoodCLName(pats[0]): if len(pats) > 0 and GoodCLName(pats[0]):
name = pats[0] name = pats[0]
if len(pats) != 1: if len(pats) != 1:
return "cannot specify CL name and file patterns" raise hg_util.Abort("cannot specify CL name and file patterns")
pats = pats[1:] pats = pats[1:]
cl, err = LoadCL(ui, repo, name, web=True) cl, err = LoadCL(ui, repo, name, web=True)
if err != '': if err != '':
return err raise hg_util.Abort(err)
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 raise hg_util.Abort("cannot change non-local CL " + name)
else: else:
name = "new" name = "new"
cl = CL("new") cl = CL("new")
if repo[None].branch() != "default": if repo[None].branch() != "default":
return "cannot create CL outside default branch; switch with 'hg update default'" raise hg_util.Abort("cannot create CL outside default branch; switch with 'hg update default'")
dirty[cl] = True dirty[cl] = True
files = ChangedFiles(ui, repo, pats, 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"]:
return "cannot use -d and -D together" raise hg_util.Abort("cannot use -d and -D together")
flag = "-d" flag = "-d"
if opts["deletelocal"]: if opts["deletelocal"]:
flag = "-D" flag = "-D"
if name == "new": if name == "new":
return "cannot use "+flag+" with file patterns" raise hg_util.Abort("cannot use "+flag+" with file patterns")
if opts["stdin"] or opts["stdout"]: if opts["stdin"] or opts["stdout"]:
return "cannot use "+flag+" with -i or -o" raise hg_util.Abort("cannot use "+flag+" with -i or -o")
if not cl.local: if not cl.local:
return "cannot change non-local CL " + name raise hg_util.Abort("cannot change non-local CL " + name)
if opts["delete"]: if opts["delete"]:
if cl.copied_from: if cl.copied_from:
return "original author must delete CL; hg change -D will remove locally" raise hg_util.Abort("original author must delete CL; hg change -D will remove locally")
PostMessage(ui, cl.name, "*** Abandoned ***", send_mail=cl.mailed) PostMessage(ui, cl.name, "*** Abandoned ***", send_mail=cl.mailed)
EditDesc(cl.name, closed=True, private=cl.private) EditDesc(cl.name, closed=True, private=cl.private)
cl.Delete(ui, repo) cl.Delete(ui, repo)
@ -1338,7 +1361,7 @@ def change(ui, repo, *pats, **opts):
s = sys.stdin.read() s = sys.stdin.read()
clx, line, err = ParseCL(s, name) clx, line, err = ParseCL(s, name)
if err != '': if err != '':
return "error parsing change list: line %d: %s" % (line, err) raise hg_util.Abort("error parsing change list: line %d: %s" % (line, err))
if clx.desc is not None: if clx.desc is not None:
cl.desc = clx.desc; cl.desc = clx.desc;
dirty[cl] = True dirty[cl] = True
@ -1360,7 +1383,7 @@ def change(ui, repo, *pats, **opts):
cl.files = files cl.files = files
err = EditCL(ui, repo, cl) err = EditCL(ui, repo, cl)
if err != "": if err != "":
return err raise hg_util.Abort(err)
dirty[cl] = True dirty[cl] = True
for d, _ in dirty.items(): for d, _ in dirty.items():
@ -1391,7 +1414,7 @@ def code_login(ui, repo, **opts):
a file in your home directory. a file in your home directory.
""" """
if codereview_disabled: if codereview_disabled:
return codereview_disabled raise hg_util.Abort(codereview_disabled)
MySend(None) MySend(None)
@ -1411,8 +1434,10 @@ def clpatch(ui, repo, clname, **opts):
name as the Author: line but add your own name to a Committer: line. name as the Author: line but add your own name to a Committer: line.
""" """
if repo[None].branch() != "default": if repo[None].branch() != "default":
return "cannot run hg clpatch outside default branch" raise hg_util.Abort("cannot run hg clpatch outside default branch")
return clpatch_or_undo(ui, repo, clname, opts, mode="clpatch") err = clpatch_or_undo(ui, repo, clname, opts, mode="clpatch")
if err:
raise hg_util.Abort(err)
@hgcommand @hgcommand
def undo(ui, repo, clname, **opts): def undo(ui, repo, clname, **opts):
@ -1423,8 +1448,10 @@ def undo(ui, repo, clname, **opts):
you can add the reason for the undo to the description. you can add the reason for the undo to the description.
""" """
if repo[None].branch() != "default": if repo[None].branch() != "default":
return "cannot run hg undo outside default branch" raise hg_util.Abort("cannot run hg undo outside default branch")
return clpatch_or_undo(ui, repo, clname, opts, mode="undo") err = clpatch_or_undo(ui, repo, clname, opts, mode="undo")
if err:
raise hg_util.Abort(err)
@hgcommand @hgcommand
def release_apply(ui, repo, clname, **opts): def release_apply(ui, repo, clname, **opts):
@ -1468,13 +1495,13 @@ def release_apply(ui, repo, clname, **opts):
""" """
c = repo[None] c = repo[None]
if not releaseBranch: if not releaseBranch:
return "no active release branches" raise hg_util.Abort("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 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 raise hg_util.Abort(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:
@ -1482,13 +1509,12 @@ def release_apply(ui, repo, clname, **opts):
except Exception, e: except Exception, e:
hg_clean(repo, "default") hg_clean(repo, "default")
raise e raise e
return None
def rev2clname(rev): def rev2clname(rev):
# Extract CL name from revision description. # Extract CL name from revision description.
# The last line in the description that is a codereview URL is the real one. # The last line in the description that is a codereview URL is the real one.
# Earlier lines might be part of the user-written description. # Earlier lines might be part of the user-written description.
all = re.findall('(?m)^http://codereview.appspot.com/([0-9]+)$', rev.description()) all = re.findall('(?m)^https?://codereview.appspot.com/([0-9]+)$', rev.description())
if len(all) > 0: if len(all) > 0:
return all[-1] return all[-1]
return "" return ""
@ -1586,24 +1612,24 @@ def clpatch_or_undo(ui, repo, clname, opts, mode):
return "local repository is out of date; sync to get %s" % (vers) return "local repository is out of date; sync to get %s" % (vers)
patch1, err = portPatch(repo, patch, vers, id) patch1, err = portPatch(repo, patch, vers, id)
if err != "": if err != "":
if not opts["ignore_hgpatch_failure"]: if not opts["ignore_hgapplydiff_failure"]:
return "codereview issue %s is out of date: %s (%s->%s)" % (clname, err, vers, id) return "codereview issue %s is out of date: %s (%s->%s)" % (clname, err, vers, id)
else: else:
patch = patch1 patch = patch1
argv = ["hgpatch"] argv = ["hgapplydiff"]
if opts["no_incoming"] or mode == "backport": if opts["no_incoming"] or mode == "backport":
argv += ["--checksync=false"] argv += ["--checksync=false"]
try: try:
cmd = subprocess.Popen(argv, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, close_fds=sys.platform != "win32") cmd = subprocess.Popen(argv, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, close_fds=sys.platform != "win32")
except: except:
return "hgpatch: " + ExceptionDetail() + "\nInstall hgpatch with:\n$ go get code.google.com/p/go.codereview/cmd/hgpatch\n" return "hgapplydiff: " + ExceptionDetail() + "\nInstall hgapplydiff with:\n$ go get code.google.com/p/go.codereview/cmd/hgapplydiff\n"
out, err = cmd.communicate(patch) out, err = cmd.communicate(patch)
if cmd.returncode != 0 and not opts["ignore_hgpatch_failure"]: if cmd.returncode != 0 and not opts["ignore_hgapplydiff_failure"]:
return "hgpatch failed" return "hgapplydiff failed"
cl.local = True cl.local = True
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_hgapplydiff_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, [])
extra = Sub(cl.files, files) extra = Sub(cl.files, files)
@ -1618,6 +1644,17 @@ def clpatch_or_undo(ui, repo, clname, opts, mode):
else: else:
ui.write(cl.PendingText() + "\n") ui.write(cl.PendingText() + "\n")
# warn if clpatch will modify file already in another CL (it's unsafe to submit them)
if mode == "clpatch":
msgs = []
cls = LoadAllCL(ui, repo, web=False)
for k, v in cls.iteritems():
isec = Intersect(v.files, cl.files)
if isec and k != clname:
msgs.append("CL " + k + ", because it also modifies " + ", ".join(isec) + ".")
if msgs:
ui.warn("warning: please double check before submitting this CL and:\n\t" + "\n\t".join(msgs) + "\n")
# portPatch rewrites patch from being a patch against # portPatch rewrites patch from being a patch against
# oldver to being a patch against newver. # oldver to being a patch against newver.
def portPatch(repo, patch, oldver, newver): def portPatch(repo, patch, oldver, newver):
@ -1687,7 +1724,7 @@ def download(ui, repo, clname, **opts):
followed by its diff, downloaded from the code review server. followed by its diff, downloaded from the code review server.
""" """
if codereview_disabled: if codereview_disabled:
return codereview_disabled raise hg_util.Abort(codereview_disabled)
cl, vers, patch, err = DownloadCL(ui, repo, clname) cl, vers, patch, err = DownloadCL(ui, repo, clname)
if err != "": if err != "":
@ -1709,7 +1746,7 @@ def file(ui, repo, clname, pat, *pats, **opts):
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 codereview_disabled:
return codereview_disabled raise hg_util.Abort(codereview_disabled)
pats = tuple([pat] + list(pats)) pats = tuple([pat] + list(pats))
if not GoodCLName(clname): if not GoodCLName(clname):
@ -1773,19 +1810,20 @@ def gofmt(ui, repo, *pats, **opts):
the given patterns. the given patterns.
""" """
if codereview_disabled: if codereview_disabled:
return codereview_disabled raise hg_util.Abort(codereview_disabled)
files = ChangedExistingFiles(ui, repo, pats, opts) files = ChangedExistingFiles(ui, repo, pats, opts)
files = gofmt_required(files) files = gofmt_required(files)
if not files: if not files:
return "no modified go files" ui.status("no modified go files\n")
return
cwd = os.getcwd() cwd = os.getcwd()
files = [RelativePath(repo.root + '/' + f, cwd) for f in files] files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
try: try:
cmd = ["gofmt", "-l"] cmd = ["gofmt", "-l"]
if not opts["list"]: if not opts["list"]:
cmd += ["-w"] cmd += ["-w"]
if os.spawnvp(os.P_WAIT, "gofmt", cmd + files) != 0: if subprocess.call(cmd + files) != 0:
raise hg_util.Abort("gofmt did not exit cleanly") raise hg_util.Abort("gofmt did not exit cleanly")
except hg_error.Abort, e: except hg_error.Abort, e:
raise raise
@ -1807,11 +1845,11 @@ def mail(ui, repo, *pats, **opts):
to the reviewer and CC list asking for a review. to the reviewer and CC list asking for a review.
""" """
if codereview_disabled: if codereview_disabled:
return codereview_disabled raise hg_util.Abort(codereview_disabled)
cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc) cl, err = CommandLineCL(ui, repo, pats, opts, op="mail", defaultcc=defaultcc)
if err != "": if err != "":
return err raise hg_util.Abort(err)
cl.Upload(ui, repo, gofmt_just_warn=True) cl.Upload(ui, repo, gofmt_just_warn=True)
if not cl.reviewer: if not cl.reviewer:
# If no reviewer is listed, assign the review to defaultcc. # If no reviewer is listed, assign the review to defaultcc.
@ -1819,15 +1857,15 @@ def mail(ui, repo, *pats, **opts):
# codereview.appspot.com/user/defaultcc # codereview.appspot.com/user/defaultcc
# page, so that it doesn't get dropped on the floor. # page, so that it doesn't get dropped on the floor.
if not defaultcc: if not defaultcc:
return "no reviewers listed in CL" raise hg_util.Abort("no reviewers listed in CL")
cl.cc = Sub(cl.cc, defaultcc) cl.cc = Sub(cl.cc, defaultcc)
cl.reviewer = defaultcc cl.reviewer = defaultcc
cl.Flush(ui, repo) cl.Flush(ui, repo)
if cl.files == []: if cl.files == []:
return "no changed files, not sending mail" raise hg_util.Abort("no changed files, not sending mail")
cl.Mail(ui, repo) cl.Mail(ui, repo)
####################################################################### #######################################################################
# hg p / hg pq / hg ps / hg pending # hg p / hg pq / hg ps / hg pending
@ -1853,7 +1891,7 @@ def pending(ui, repo, *pats, **opts):
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 codereview_disabled:
return codereview_disabled raise hg_util.Abort(codereview_disabled)
quick = opts.get('quick', False) quick = opts.get('quick', False)
short = opts.get('short', False) short = opts.get('short', False)
@ -1868,7 +1906,7 @@ def pending(ui, repo, *pats, **opts):
ui.write(cl.PendingText(quick=quick) + "\n") ui.write(cl.PendingText(quick=quick) + "\n")
if short: if short:
return return 0
files = DefaultFiles(ui, repo, []) 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"
@ -1890,7 +1928,7 @@ def submit(ui, repo, *pats, **opts):
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 codereview_disabled:
return codereview_disabled raise hg_util.Abort(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()
@ -1898,9 +1936,9 @@ def submit(ui, repo, *pats, **opts):
if not opts["no_incoming"] and hg_incoming(ui, repo): if not opts["no_incoming"] and hg_incoming(ui, repo):
need_sync() need_sync()
cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc) cl, err = CommandLineCL(ui, repo, pats, opts, op="submit", defaultcc=defaultcc)
if err != "": if err != "":
return err raise hg_util.Abort(err)
user = None user = None
if cl.copied_from: if cl.copied_from:
@ -1909,20 +1947,29 @@ def submit(ui, repo, *pats, **opts):
typecheck(userline, str) typecheck(userline, str)
about = "" about = ""
if cl.reviewer:
about += "R=" + JoinComma([CutDomain(s) for s in cl.reviewer]) + "\n" if not cl.lgtm and not opts.get('tbr') and not isAddca(cl):
raise hg_util.Abort("this CL has not been LGTM'ed")
if cl.lgtm:
about += "LGTM=" + JoinComma([CutDomain(who) for (who, line, approval) in cl.lgtm if approval]) + "\n"
reviewer = cl.reviewer
if opts.get('tbr'): if opts.get('tbr'):
tbr = SplitCommaSpace(opts.get('tbr')) tbr = SplitCommaSpace(opts.get('tbr'))
for name in tbr:
if name.startswith('golang-'):
raise hg_util.Abort("--tbr requires a person, not a mailing list")
cl.reviewer = Add(cl.reviewer, tbr) cl.reviewer = Add(cl.reviewer, tbr)
about += "TBR=" + JoinComma([CutDomain(s) for s in tbr]) + "\n" about += "TBR=" + JoinComma([CutDomain(s) for s in tbr]) + "\n"
if reviewer:
about += "R=" + JoinComma([CutDomain(s) for s in reviewer]) + "\n"
if cl.cc: if cl.cc:
about += "CC=" + JoinComma([CutDomain(s) for s in cl.cc]) + "\n" about += "CC=" + JoinComma([CutDomain(s) for s in cl.cc]) + "\n"
if not cl.reviewer: if not cl.reviewer:
return "no reviewers listed in CL" raise hg_util.Abort("no reviewers listed in CL")
if not cl.local: if not cl.local:
return "cannot submit non-local CL" raise hg_util.Abort("cannot submit non-local CL")
# upload, to sync current patch and also get change number if CL is new. # upload, to sync current patch and also get change number if CL is new.
if not cl.copied_from: if not cl.copied_from:
@ -1957,7 +2004,7 @@ def submit(ui, repo, *pats, **opts):
ret = hg_commit(ui, repo, *['path:'+f for f in cl.files], message=message, user=userline) ret = hg_commit(ui, repo, *['path:'+f for f in cl.files], message=message, user=userline)
commit_okay = False commit_okay = False
if ret: if ret:
return "nothing changed" raise hg_util.Abort("nothing changed")
node = repo["-1"].node() 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:
@ -1968,12 +2015,16 @@ def submit(ui, repo, *pats, **opts):
# Push changes to remote. If it works, we're committed. If not, roll back. # Push changes to remote. If it works, we're committed. If not, roll back.
try: try:
hg_push(ui, repo) if hg_push(ui, repo):
raise hg_util.Abort("push error")
except hg_error.Abort, e: except hg_error.Abort, e:
if e.message.find("push creates new heads") >= 0: if e.message.find("push creates new heads") >= 0:
# Remote repository had changes we missed. # Remote repository had changes we missed.
need_sync() need_sync()
raise raise
except urllib2.HTTPError, e:
print >>sys.stderr, "pushing to remote server failed; do you have commit permissions?"
raise
except: except:
real_rollback() real_rollback()
raise raise
@ -1985,11 +2036,11 @@ def submit(ui, repo, *pats, **opts):
"(^https?://([^@/]+@)?code\.google\.com/p/([^/.]+)(\.[^./]+)?/?)", url) "(^https?://([^@/]+@)?code\.google\.com/p/([^/.]+)(\.[^./]+)?/?)", url)
if m: if m:
if m.group(1): # prj.googlecode.com/hg/ case if m.group(1): # prj.googlecode.com/hg/ case
changeURL = "http://code.google.com/p/%s/source/detail?r=%s" % (m.group(3), changeURL) changeURL = "https://code.google.com/p/%s/source/detail?r=%s" % (m.group(3), changeURL)
elif m.group(4) and m.group(7): # code.google.com/p/prj.subrepo/ case elif m.group(4) and m.group(7): # code.google.com/p/prj.subrepo/ case
changeURL = "http://code.google.com/p/%s/source/detail?r=%s&repo=%s" % (m.group(6), changeURL, m.group(7)[1:]) changeURL = "https://code.google.com/p/%s/source/detail?r=%s&repo=%s" % (m.group(6), changeURL, m.group(7)[1:])
elif m.group(4): # code.google.com/p/prj/ case elif m.group(4): # code.google.com/p/prj/ case
changeURL = "http://code.google.com/p/%s/source/detail?r=%s" % (m.group(6), changeURL) changeURL = "https://code.google.com/p/%s/source/detail?r=%s" % (m.group(6), changeURL)
else: else:
print >>sys.stderr, "URL: ", url print >>sys.stderr, "URL: ", url
else: else:
@ -2010,7 +2061,12 @@ def submit(ui, repo, *pats, **opts):
err = hg_clean(repo, "default") err = hg_clean(repo, "default")
if err: if err:
return err return err
return None return 0
def isAddca(cl):
rev = cl.reviewer
isGobot = 'gobot' in rev or 'gobot@swtch.com' in rev or 'gobot@golang.org' in rev
return cl.desc.startswith('A+C:') and 'Generated by addca.' in cl.desc and isGobot
####################################################################### #######################################################################
# hg sync # hg sync
@ -2023,10 +2079,22 @@ def sync(ui, repo, **opts):
into the local repository. into the local repository.
""" """
if codereview_disabled: if codereview_disabled:
return codereview_disabled raise hg_util.Abort(codereview_disabled)
if not opts["local"]: if not opts["local"]:
err = hg_pull(ui, repo, update=True) # If there are incoming CLs, pull -u will do the update.
# If there are no incoming CLs, do hg update to make sure
# that an update always happens regardless. This is less
# surprising than update depending on incoming CLs.
# It is important not to do both hg pull -u and hg update
# in the same command, because the hg update will end
# up marking resolve conflicts from the hg pull -u as resolved,
# causing files with <<< >>> markers to not show up in
# hg resolve -l. Yay Mercurial.
if hg_incoming(ui, repo):
err = hg_pull(ui, repo, update=True)
else:
err = hg_update(ui, repo)
if err: if err:
return err return err
sync_changes(ui, repo) sync_changes(ui, repo)
@ -2037,7 +2105,7 @@ def sync_changes(ui, repo):
# 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(): 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)^https?://(?:[^\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()):
ui.warn("CL %s submitted as %s; closing\n" % (clname, repo[rev])) ui.warn("CL %s submitted as %s; closing\n" % (clname, repo[rev]))
cl, err = LoadCL(ui, repo, clname, web=False) cl, err = LoadCL(ui, repo, clname, web=False)
@ -2064,7 +2132,7 @@ def sync_changes(ui, repo):
ui.warn("CL %s has no files; delete (abandon) with hg change -d %s\n" % (cl.name, cl.name)) ui.warn("CL %s has no files; delete (abandon) with hg change -d %s\n" % (cl.name, cl.name))
else: else:
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 0
####################################################################### #######################################################################
# hg upload # hg upload
@ -2076,17 +2144,17 @@ def upload(ui, repo, name, **opts):
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 codereview_disabled:
return codereview_disabled raise hg_util.Abort(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)
if err != "": if err != "":
return err raise hg_util.Abort(err)
if not cl.local: if not cl.local:
return "cannot upload non-local change" raise hg_util.Abort("cannot upload non-local change")
cl.Upload(ui, repo) cl.Upload(ui, repo)
print "%s%s\n" % (server_url_base, cl.name) print "%s%s\n" % (server_url_base, cl.name)
return return 0
####################################################################### #######################################################################
# Table of commands, supplied to Mercurial for installation. # Table of commands, supplied to Mercurial for installation.
@ -2115,7 +2183,7 @@ cmdtable = {
"^clpatch": ( "^clpatch": (
clpatch, clpatch,
[ [
('', 'ignore_hgpatch_failure', None, 'create CL metadata even if hgpatch fails'), ('', 'ignore_hgapplydiff_failure', None, 'create CL metadata even if hgapplydiff fails'),
('', 'no_incoming', None, 'disable check for incoming changes'), ('', 'no_incoming', None, 'disable check for incoming changes'),
], ],
"change#" "change#"
@ -2174,7 +2242,7 @@ cmdtable = {
"^release-apply": ( "^release-apply": (
release_apply, release_apply,
[ [
('', 'ignore_hgpatch_failure', None, 'create CL metadata even if hgpatch fails'), ('', 'ignore_hgapplydiff_failure', None, 'create CL metadata even if hgapplydiff fails'),
('', 'no_incoming', None, 'disable check for incoming changes'), ('', 'no_incoming', None, 'disable check for incoming changes'),
], ],
"change#" "change#"
@ -2197,7 +2265,7 @@ cmdtable = {
"^undo": ( "^undo": (
undo, undo,
[ [
('', 'ignore_hgpatch_failure', None, 'create CL metadata even if hgpatch fails'), ('', 'ignore_hgapplydiff_failure', None, 'create CL metadata even if hgapplydiff fails'),
('', 'no_incoming', None, 'disable check for incoming changes'), ('', 'no_incoming', None, 'disable check for incoming changes'),
], ],
"change#" "change#"
@ -2229,6 +2297,7 @@ def reposetup(ui, repo):
if codereview_init: if codereview_init:
return return
codereview_init = True codereview_init = True
start_status_thread()
# Read repository-specific options from lib/codereview/codereview.cfg or codereview.cfg. # Read repository-specific options from lib/codereview/codereview.cfg or codereview.cfg.
root = '' root = ''
@ -2366,7 +2435,7 @@ def IsRietveldSubmitted(ui, clname, hex):
return False return False
for msg in dict.get("messages", []): for msg in dict.get("messages", []):
text = msg.get("text", "") text = msg.get("text", "")
m = re.match('\*\*\* Submitted as [^*]*?([0-9a-f]+) \*\*\*', text) m = re.match('\*\*\* Submitted as [^*]*?r=([0-9a-f]+)[^ ]* \*\*\*', text)
if m is not None and len(m.group(1)) >= 8 and hex.startswith(m.group(1)): if m is not None and len(m.group(1)) >= 8 and hex.startswith(m.group(1)):
return True return True
return False return False
@ -2460,6 +2529,8 @@ def MySend1(request_path, payload=None,
self._Authenticate() self._Authenticate()
if request_path is None: if request_path is None:
return return
if timeout is None:
timeout = 30 # seconds
old_timeout = socket.getdefaulttimeout() old_timeout = socket.getdefaulttimeout()
socket.setdefaulttimeout(timeout) socket.setdefaulttimeout(timeout)
@ -2468,7 +2539,7 @@ def MySend1(request_path, payload=None,
while True: while True:
tries += 1 tries += 1
args = dict(kwargs) args = dict(kwargs)
url = "http://%s%s" % (self.host, request_path) url = "https://%s%s" % (self.host, request_path)
if args: if args:
url += "?" + urllib.urlencode(args) url += "?" + urllib.urlencode(args)
req = self._CreateRequest(url=url, data=payload) req = self._CreateRequest(url=url, data=payload)
@ -2580,7 +2651,7 @@ def RietveldSetup(ui, repo):
if x is not None: if x is not None:
email = x email = x
server_url_base = "http://" + server + "/" server_url_base = "https://" + server + "/"
testing = ui.config("codereview", "testing") testing = ui.config("codereview", "testing")
force_google_account = ui.configbool("codereview", "force_google_account", False) force_google_account = ui.configbool("codereview", "force_google_account", False)
@ -2609,7 +2680,7 @@ def RietveldSetup(ui, repo):
rpc = None rpc = None
global releaseBranch global releaseBranch
tags = repo.branchtags().keys() tags = repo.branchmap().keys()
if 'release-branch.go10' in tags: if 'release-branch.go10' in tags:
# NOTE(rsc): This tags.sort is going to get the wrong # NOTE(rsc): This tags.sort is going to get the wrong
# answer when comparing release-branch.go9 with # answer when comparing release-branch.go9 with
@ -2755,7 +2826,9 @@ class ClientLoginError(urllib2.HTTPError):
def __init__(self, url, code, msg, headers, args): def __init__(self, url, code, msg, headers, args):
urllib2.HTTPError.__init__(self, url, code, msg, headers, None) urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
self.args = args self.args = args
self.reason = args["Error"] # .reason is now a read-only property based on .msg
# this means we ignore 'msg', but that seems to work fine.
self.msg = args["Error"]
class AbstractRpcServer(object): class AbstractRpcServer(object):
@ -2858,7 +2931,7 @@ class AbstractRpcServer(object):
# This is a dummy value to allow us to identify when we're successful. # This is a dummy value to allow us to identify when we're successful.
continue_location = "http://localhost/" continue_location = "http://localhost/"
args = {"continue": continue_location, "auth": auth_token} args = {"continue": continue_location, "auth": auth_token}
req = self._CreateRequest("http://%s/_ah/login?%s" % (self.host, urllib.urlencode(args))) req = self._CreateRequest("https://%s/_ah/login?%s" % (self.host, urllib.urlencode(args)))
try: try:
response = self.opener.open(req) response = self.opener.open(req)
except urllib2.HTTPError, e: except urllib2.HTTPError, e:
@ -2888,31 +2961,31 @@ class AbstractRpcServer(object):
try: try:
auth_token = self._GetAuthToken(credentials[0], credentials[1]) auth_token = self._GetAuthToken(credentials[0], credentials[1])
except ClientLoginError, e: except ClientLoginError, e:
if e.reason == "BadAuthentication": if e.msg == "BadAuthentication":
print >>sys.stderr, "Invalid username or password." print >>sys.stderr, "Invalid username or password."
continue continue
if e.reason == "CaptchaRequired": if e.msg == "CaptchaRequired":
print >>sys.stderr, ( print >>sys.stderr, (
"Please go to\n" "Please go to\n"
"https://www.google.com/accounts/DisplayUnlockCaptcha\n" "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
"and verify you are a human. Then try again.") "and verify you are a human. Then try again.")
break break
if e.reason == "NotVerified": if e.msg == "NotVerified":
print >>sys.stderr, "Account not verified." print >>sys.stderr, "Account not verified."
break break
if e.reason == "TermsNotAgreed": if e.msg == "TermsNotAgreed":
print >>sys.stderr, "User has not agreed to TOS." print >>sys.stderr, "User has not agreed to TOS."
break break
if e.reason == "AccountDeleted": if e.msg == "AccountDeleted":
print >>sys.stderr, "The user account has been deleted." print >>sys.stderr, "The user account has been deleted."
break break
if e.reason == "AccountDisabled": if e.msg == "AccountDisabled":
print >>sys.stderr, "The user account has been disabled." print >>sys.stderr, "The user account has been disabled."
break break
if e.reason == "ServiceDisabled": if e.msg == "ServiceDisabled":
print >>sys.stderr, "The user's access to the service has been disabled." print >>sys.stderr, "The user's access to the service has been disabled."
break break
if e.reason == "ServiceUnavailable": if e.msg == "ServiceUnavailable":
print >>sys.stderr, "The service is not available; try again later." print >>sys.stderr, "The service is not available; try again later."
break break
raise raise
@ -2948,7 +3021,7 @@ class AbstractRpcServer(object):
while True: while True:
tries += 1 tries += 1
args = dict(kwargs) args = dict(kwargs)
url = "http://%s%s" % (self.host, request_path) url = "https://%s%s" % (self.host, request_path)
if args: if args:
url += "?" + urllib.urlencode(args) url += "?" + urllib.urlencode(args)
req = self._CreateRequest(url=url, data=payload) req = self._CreateRequest(url=url, data=payload)