diff options
author | Ondřej Surý <ondrej@sury.org> | 2011-02-14 13:23:51 +0100 |
---|---|---|
committer | Ondřej Surý <ondrej@sury.org> | 2011-02-14 13:23:51 +0100 |
commit | 758ff64c69e34965f8af5b2d6ffd65e8d7ab2150 (patch) | |
tree | 6d6b34f8c678862fe9b56c945a7b63f68502c245 /lib/codereview/codereview.py | |
parent | 3e45412327a2654a77944249962b3652e6142299 (diff) | |
download | golang-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.py | 205 |
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() |