summaryrefslogtreecommitdiff
path: root/lib/codereview/codereview.py
diff options
context:
space:
mode:
authorOndřej Surý <ondrej@sury.org>2011-02-14 13:23:51 +0100
committerOndřej Surý <ondrej@sury.org>2011-02-14 13:23:51 +0100
commit758ff64c69e34965f8af5b2d6ffd65e8d7ab2150 (patch)
tree6d6b34f8c678862fe9b56c945a7b63f68502c245 /lib/codereview/codereview.py
parent3e45412327a2654a77944249962b3652e6142299 (diff)
downloadgolang-758ff64c69e34965f8af5b2d6ffd65e8d7ab2150.tar.gz
Imported Upstream version 2011-02-01.1upstream/2011-02-01.1
Diffstat (limited to 'lib/codereview/codereview.py')
-rw-r--r--lib/codereview/codereview.py205
1 files changed, 180 insertions, 25 deletions
diff --git a/lib/codereview/codereview.py b/lib/codereview/codereview.py
index e8c84abec..095270577 100644
--- a/lib/codereview/codereview.py
+++ b/lib/codereview/codereview.py
@@ -139,6 +139,32 @@ def typecheck(s, t):
if 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.
+def ustr(s):
+ typecheck(s, str)
+ return s.decode("utf-8")
+
+# Even with those, Mercurial still sometimes turns unicode into str
+# and then tries to use it as ascii. Change Mercurial's default.
+def set_mercurial_encoding_to_utf8():
+ from mercurial import encoding
+ encoding.encoding = 'utf-8'
+
+set_mercurial_encoding_to_utf8()
+
+# Even with those we still run into problems.
+# I tried to do things by the book but could not convince
+# Mercurial to let me check in a change with UTF-8 in the
+# CL description or author field, no matter how many conversions
+# between str and unicode I inserted and despite changing the
+# default encoding. I'm tired of this game, so set the default
+# encoding for all of Python to 'utf-8', not 'ascii'.
+def default_to_utf8():
+ import sys
+ reload(sys) # site.py deleted setdefaultencoding; get it back
+ sys.setdefaultencoding('utf-8')
+
+default_to_utf8()
#######################################################################
# Change list parsing.
@@ -573,6 +599,16 @@ def CodeReviewDir(ui, repo):
typecheck(dir, str)
return dir
+# Turn leading tabs into spaces, so that the common white space
+# prefix doesn't get confused when people's editors write out
+# some lines with spaces, some with tabs. Only a heuristic
+# (some editors don't use 8 spaces either) but a useful one.
+def TabsToSpaces(line):
+ i = 0
+ while i < len(line) and line[i] == '\t':
+ i += 1
+ return ' '*(8*i) + line[i:]
+
# Strip maximal common leading white space prefix from text
def StripCommon(text):
typecheck(text, str)
@@ -581,6 +617,7 @@ def StripCommon(text):
line = line.rstrip()
if line == '':
continue
+ line = TabsToSpaces(line)
white = line[:len(line)-len(line.lstrip())]
if ws == None:
ws = white
@@ -597,6 +634,7 @@ def StripCommon(text):
t = ''
for line in text.split('\n'):
line = line.rstrip()
+ line = TabsToSpaces(line)
if line.startswith(ws):
line = line[len(ws):]
if line == '' and t == '':
@@ -638,28 +676,53 @@ def effective_revpair(repo):
return cmdutil.revpair(repo, None)
# Return list of changed files in repository that match pats.
-def ChangedFiles(ui, repo, pats, opts):
- # Find list of files being operated on.
+# Warn about patterns that did not match.
+def matchpats(ui, repo, pats, opts):
matcher = cmdutil.match(repo, pats, opts)
node1, node2 = effective_revpair(repo)
- modified, added, removed = repo.status(node1, node2, matcher)[:3]
+ 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):
- matcher = cmdutil.match(repo, pats, opts)
- node1, node2 = effective_revpair(repo)
- modified, added, _ = repo.status(node1, node2, matcher)[:3]
+ modified, added = matchpats(ui, repo, pats, opts)[:2]
l = modified + added
l.sort()
return l
# Return list of files claimed by existing CLs
-def TakenFiles(ui, repo):
- return Taken(ui, repo).keys()
-
def Taken(ui, repo):
all = LoadAllCL(ui, repo, web=False)
taken = {}
@@ -670,7 +733,7 @@ def Taken(ui, repo):
# Return list of changed files that are not claimed by other CLs
def DefaultFiles(ui, repo, pats, opts):
- return Sub(ChangedFiles(ui, repo, pats, opts), TakenFiles(ui, repo))
+ 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]
@@ -701,6 +764,39 @@ def Incoming(ui, repo, opts):
_, incoming, _ = findcommonincoming(repo, getremote(ui, repo, opts))
return incoming
+desc_re = '^(.+: |tag release\.|release\.|fix build)'
+
+desc_msg = '''Your CL description appears not to use the standard form.
+
+The first line of your change description is conventionally a
+one-line summary of the change, prefixed by the primary affected package,
+and is used as the subject for code review mail; the rest of the description
+elaborates.
+
+Examples:
+
+ encoding/rot13: new package
+
+ math: add IsInf, IsNaN
+
+ net: fix cname in LookupHost
+
+ unicode: update to Unicode 5.0.2
+
+'''
+
+
+
+def promptremove(ui, repo, f):
+ if promptyesno(ui, "hg remove %s (y/n)?" % (f,)):
+ if commands.remove(ui, repo, 'path:'+f) != 0:
+ ui.warn("error removing %s" % (f,))
+
+def promptadd(ui, repo, f):
+ if promptyesno(ui, "hg add %s (y/n)?" % (f,)):
+ if commands.add(ui, repo, 'path:'+f) != 0:
+ ui.warn("error adding %s" % (f,))
+
def EditCL(ui, repo, cl):
set_status(None) # do not show status
s = cl.EditorText()
@@ -711,13 +807,54 @@ def EditCL(ui, repo, cl):
if not promptyesno(ui, "error parsing change list: line %d: %s\nre-edit (y/n)?" % (line, err)):
return "change list not modified"
continue
- cl.desc = clx.desc;
+
+ # Check description.
+ if clx.desc == '':
+ if promptyesno(ui, "change list should have a description\nre-edit (y/n)?"):
+ continue
+ elif not re.match(desc_re, clx.desc.split('\n')[0]):
+ if promptyesno(ui, desc_msg + "re-edit (y/n)?"):
+ continue
+
+ # Check file list for files that need to be hg added or hg removed
+ # or simply aren't understood.
+ pats = ['path:'+f for f in clx.files]
+ modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, pats, {})
+ files = []
+ for f in clx.files:
+ if f in modified or f in added or f in removed:
+ files.append(f)
+ continue
+ if f in deleted:
+ promptremove(ui, repo, f)
+ files.append(f)
+ continue
+ if f in unknown:
+ promptadd(ui, repo, f)
+ files.append(f)
+ continue
+ if f in ignored:
+ ui.warn("error: %s is excluded by .hgignore; omitting\n" % (f,))
+ continue
+ if f in clean:
+ ui.warn("warning: %s is listed in the CL but unchanged\n" % (f,))
+ files.append(f)
+ continue
+ p = repo.root + '/' + f
+ if os.path.isfile(p):
+ ui.warn("warning: %s is a file but not known to hg\n" % (f,))
+ files.append(f)
+ continue
+ if os.path.isdir(p):
+ ui.warn("error: %s is a directory, not a file; omitting\n" % (f,))
+ continue
+ ui.warn("error: %s does not exist; omitting\n" % (f,))
+ clx.files = files
+
+ cl.desc = clx.desc
cl.reviewer = clx.reviewer
cl.cc = clx.cc
cl.files = clx.files
- if cl.desc == '':
- if promptyesno(ui, "change list should have description\nre-edit (y/n)?"):
- continue
break
return ""
@@ -736,7 +873,7 @@ def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
else:
cl = CL("new")
cl.local = True
- cl.files = Sub(ChangedFiles(ui, repo, pats, opts), TakenFiles(ui, repo))
+ cl.files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
if not cl.files:
return None, "no files changed"
if opts.get('reviewer'):
@@ -758,10 +895,11 @@ def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
# which expands the syntax @clnumber to mean the files
# in that CL.
original_match = None
-def ReplacementForCmdutilMatch(repo, pats=[], opts={}, globbed=False, default='relpath'):
+def ReplacementForCmdutilMatch(repo, pats=None, opts=None, globbed=False, default='relpath'):
taken = []
files = []
pats = pats or []
+ opts = opts or {}
for p in pats:
if p.startswith('@'):
taken.append(p)
@@ -799,7 +937,7 @@ def CheckGofmt(ui, repo, files, just_warn):
if not files:
return
try:
- cmd = subprocess.Popen(["gofmt", "-l"] + files, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
+ 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())
@@ -895,9 +1033,7 @@ def change(ui, repo, *pats, **opts):
name = "new"
cl = CL("new")
dirty[cl] = True
- files = ChangedFiles(ui, repo, pats, opts)
- taken = TakenFiles(ui, repo)
- files = Sub(files, taken)
+ files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
if opts["delete"] or opts["deletelocal"]:
if opts["delete"] and opts["deletelocal"]:
@@ -1134,8 +1270,12 @@ def mail(ui, repo, *pats, **opts):
return "no reviewers listed in CL"
cl.cc = Sub(cl.cc, defaultcc)
cl.reviewer = defaultcc
- cl.Flush(ui, repo)
- cl.Mail(ui, repo)
+ cl.Flush(ui, repo)
+
+ if cl.files == []:
+ return "no changed files, not sending mail"
+
+ cl.Mail(ui, repo)
def nocommit(ui, repo, *pats, **opts):
"""(disabled when using this extension)"""
@@ -1205,6 +1345,9 @@ def submit(ui, repo, *pats, **opts):
if missing_codereview:
return missing_codereview
+ # We already called this on startup but sometimes Mercurial forgets.
+ set_mercurial_encoding_to_utf8()
+
repo.ui.quiet = True
if not opts["no_incoming"] and Incoming(ui, repo, opts):
return "local repository out of date; must sync before submit"
@@ -1217,6 +1360,7 @@ def submit(ui, repo, *pats, **opts):
if cl.copied_from:
user = cl.copied_from
userline = CheckContributor(ui, repo, user)
+ typecheck(userline, str)
about = ""
if cl.reviewer:
@@ -1246,6 +1390,7 @@ def submit(ui, repo, *pats, **opts):
if cl.copied_from:
about += "\nCommitter: " + CheckContributor(ui, repo, None) + "\n"
+ typecheck(about, str)
if not cl.mailed and not cl.copied_from: # in case this is TBR
cl.Mail(ui, repo)
@@ -1254,7 +1399,9 @@ def submit(ui, repo, *pats, **opts):
date = opts.get('date')
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:"
@@ -1266,7 +1413,7 @@ def submit(ui, repo, *pats, **opts):
return "dry run; not submitted"
m = match.exact(repo.root, repo.getcwd(), cl.files)
- node = repo.commit(opts['message'], userline, opts.get('date'), m)
+ node = repo.commit(ustr(opts['message']), ustr(userline), opts.get('date'), m)
if not node:
return "nothing changed"
@@ -1707,7 +1854,7 @@ def MySend1(request_path, payload=None,
def GetForm(url):
f = FormParser()
- f.feed(MySend(url).decode("utf-8")) # f.feed wants unicode
+ f.feed(ustr(MySend(url))) # f.feed wants unicode
f.close()
# convert back to utf-8 to restore sanity
m = {}
@@ -2280,10 +2427,18 @@ def GetRpcServer(options):
def GetUserCredentials():
"""Prompts the user for a username and password."""
+ # Disable status prints so they don't obscure the password prompt.
+ global global_status
+ st = global_status
+ global_status = None
+
email = options.email
if email is None:
email = GetEmail("Email (login for uploading to %s)" % options.server)
password = getpass.getpass("Password for %s: " % email)
+
+ # Put status back.
+ global_status = st
return (email, password)
# If this is the dev_appserver, use fake authentication.
@@ -2603,7 +2758,7 @@ class MercurialVCS(VersionControlSystem):
self.base_rev = self.options.revision
else:
mqparent, err = RunShellWithReturnCode(['hg', 'log', '--rev', 'qparent', '--template={node}'])
- if not err:
+ if not err and mqparent != "":
self.base_rev = mqparent
else:
self.base_rev = RunShell(["hg", "parents", "-q"]).split(':')[1].strip()