Revision: 0f74441d8848
Author: Noah Evans <
noah....@gmail.com>
Date: Mon May 30 00:46:36 2011
Log: codereview: add codereview
this is my first test of code review. Adding the codereview scripts to
inferno-npe.
R=noah.evans
CC=inferno-npe, noah.evans
http://codereview.appspot.com/4550089
http://code.google.com/p/inferno-npe/source/detail?r=0f74441d8848
Added:
/utils/lib/codereview/codereview.cfg
/utils/lib/codereview/codereview.py
/utils/lib/codereview/codereview.pyc
=======================================
--- /dev/null
+++ /utils/lib/codereview/codereview.cfg Mon May 30 00:46:36 2011
@@ -0,0 +1,1 @@
+defaultcc:
infer...@googlegroups.com
=======================================
--- /dev/null
+++ /utils/lib/codereview/codereview.py Mon May 30 00:46:36 2011
@@ -0,0 +1,3278 @@
+# coding=utf-8
+# (The line above is necessary so that I can use 世界 in the
+# *comment* below without Python getting all bent out of shape.)
+
+# Copyright 2007-2009 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#
http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+'''Mercurial interface to
codereview.appspot.com.
+
+To configure, set the following options in
+your repository's .hg/hgrc file.
+
+ [extensions]
+ codereview = path/to/codereview.py
+
+ [codereview]
+ server =
codereview.appspot.com
+
+The server should be running Rietveld; see
http://code.google.com/p/rietveld/.
+
+In addition to the new commands, this extension introduces
+the file pattern syntax @nnnnnn, where nnnnnn is a change list
+number, to mean the files included in that change list, which
+must be associated with the current client.
+
+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".
+'''
+
+from mercurial import cmdutil, commands, hg, util, error, match
+from mercurial.node import nullrev, hex, nullid, short
+import os, re, time
+import stat
+import subprocess
+import threading
+from HTMLParser import HTMLParser
+
+# The standard 'json' package is new in Python 2.6.
+# Before that it was an external package named simplejson.
+try:
+ # Standard location in 2.6 and beyond.
+ import json
+except Exception, e:
+ try:
+ # Conventional name for earlier package.
+ import simplejson as json
+ except:
+ try:
+ # Was also bundled with django, which is commonly installed.
+ from django.utils import simplejson as json
+ except:
+ # We give up.
+ raise e
+
+try:
+ hgversion = util.version()
+except:
+ from mercurial.version import version as v
+ hgversion = v.get_version()
+
+try:
+ from mercurial.discovery import findcommonincoming
+except:
+ def findcommonincoming(repo, remote):
+ return repo.findcommonincoming(remote)
+
+oldMessage = """
+The code review extension requires Mercurial 1.3 or newer.
+
+To install a new Mercurial,
+
+ sudo easy_install mercurial
+
+works on most systems.
+"""
+
+linuxMessage = """
+You may need to clear your current Mercurial installation by running:
+
+ sudo apt-get remove mercurial mercurial-common
+ sudo rm -rf /etc/mercurial
+"""
+
+if hgversion < '1.3':
+ 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"
+
+# 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
+#
+# Python distinguishes between the str (string of bytes)
+# and unicode (string of code points) types. Most operations
+# work on either one just fine, but some (like regexp matching)
+# require unicode, and others (like write) require str.
+#
+# As befits the language, Python hides the distinction between
+# unicode and str by converting between them silently, but
+# *only* if all the bytes/code points involved are 7-bit ASCII.
+# This means that if you're not careful, your program works
+# fine on "hello, world" and fails on "hello, 世界". And of course,
+# the obvious way to be careful - use static types - is unavailable.
+# So the only way is trial and error to find where to put explicit
+# conversions.
+#
+# Because more functions do implicit conversion to str (string of bytes)
+# than do implicit conversion to unicode (string of code points),
+# the convention in this module is to represent all text as str,
+# converting to unicode only when calling a unicode-only function
+# and then converting back to str as soon as possible.
+
+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.
+#
+# Change lists are stored in .hg/codereview/cl.nnnnnn
+# where nnnnnn is the number assigned by the code review server.
+# Most data about a change list is stored on the code review server
+# too: the description, reviewer, and cc list are all stored there.
+# The only thing in the cl.nnnnnn file is the list of relevant files.
+# Also, the existence of the cl.nnnnnn file marks this repository
+# as the one where the change list lives.
+
+emptydiff = """Index: ~rietveld~placeholder~
+===================================================================
+diff --git a/~rietveld~placeholder~ b/~rietveld~placeholder~
+new file mode 100644
+"""
+
+class CL(object):
+ def __init__(self, name):
+ typecheck(name, str)
+
self.name = name
+ self.desc = ''
+ self.files = []
+ self.reviewer = []
+ self.cc = []
+ self.url = ''
+ self.local = False
+ self.web = False
+ self.copied_from = None # None means current user
+ self.mailed = False
+ self.private = False
+
+ def DiskText(self):
+ cl = self
+ s = ""
+ if cl.copied_from:
+ s += "Author: " + cl.copied_from + "\n\n"
+ if cl.private:
+ s += "Private: " + str(self.private) + "\n"
+ s += "Mailed: " + str(self.mailed) + "\n"
+ s += "Description:\n"
+ s += Indent(cl.desc, "\t")
+ s += "Files:\n"
+ for f in cl.files:
+ s += "\t" + f + "\n"
+ typecheck(s, str)
+ return s
+
+ def EditorText(self):
+ cl = self
+ s = _change_prolog
+ s += "\n"
+ if cl.copied_from:
+ s += "Author: " + cl.copied_from + "\n"
+ if cl.url != '':
+ s += 'URL: ' + cl.url + ' # cannot edit\n\n'
+ if cl.private:
+ s += "Private: True\n"
+ s += "Reviewer: " + JoinComma(cl.reviewer) + "\n"
+ s += "CC: " + JoinComma(cl.cc) + "\n"
+ s += "\n"
+ s += "Description:\n"
+ if cl.desc == '':
+ s += "\t<enter description here>\n"
+ else:
+ s += Indent(cl.desc, "\t")
+ s += "\n"
+ if cl.local or
cl.name == "new":
+ s += "Files:\n"
+ for f in cl.files:
+ s += "\t" + f + "\n"
+ s += "\n"
+ typecheck(s, str)
+ return s
+
+ def PendingText(self):
+ cl = self
+ s =
cl.name + ":" + "\n"
+ s += Indent(cl.desc, "\t")
+ s += "\n"
+ if cl.copied_from:
+ s += "\tAuthor: " + cl.copied_from + "\n"
+ s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n"
+ s += "\tCC: " + JoinComma(cl.cc) + "\n"
+ s += "\tFiles:\n"
+ for f in cl.files:
+ s += "\t\t" + f + "\n"
+ typecheck(s, str)
+ return s
+
+ def Flush(self, ui, repo):
+ if
self.name == "new":
+ self.Upload(ui, repo, gofmt_just_warn=True, creating=True)
+ dir = CodeReviewDir(ui, repo)
+ path = dir + '/cl.' +
self.name
+ f = open(path+'!', "w")
+ f.write(self.DiskText())
+ f.close()
+ if sys.platform == "win32" and os.path.isfile(path):
+ os.remove(path)
+ os.rename(path+'!', path)
+ if self.web and not self.copied_from:
+ EditDesc(
self.name, desc=self.desc,
+ reviewers=JoinComma(self.reviewer), cc=JoinComma(self.cc),
+ private=self.private)
+
+ def Delete(self, ui, repo):
+ dir = CodeReviewDir(ui, repo)
+ os.unlink(dir + "/cl." +
self.name)
+
+ def Subject(self):
+ s = line1(self.desc)
+ if len(s) > 60:
+ s = s[0:55] + "..."
+ if
self.name != "new":
+ s = "code review %s: %s" % (
self.name, s)
+ typecheck(s, str)
+ return s
+
+ def Upload(self, ui, repo, send_mail=False, gofmt=True,
gofmt_just_warn=False, creating=False, quiet=False):
+ if not self.files and not creating:
+ ui.warn("no files in change list\n")
+ # if ui.configbool("codereview", "force_gofmt", True) and gofmt:
+ # CheckFormat(ui, repo, self.files, just_warn=gofmt_just_warn)
+ set_status("uploading CL metadata + diffs")
+ os.chdir(repo.root)
+ form_fields = [
+ ("content_upload", "1"),
+ ("reviewers", JoinComma(self.reviewer)),
+ ("cc", JoinComma(self.cc)),
+ ("description", self.desc),
+ ("base_hashes", ""),
+ ]
+
+ if
self.name != "new":
+ form_fields.append(("issue",
self.name))
+ vcs = None
+ # We do not include files when creating the issue,
+ # because we want the patch sets to record the repository
+ # and base revision they are diffs against. We use the patch
+ # set message for that purpose, but there is no message with
+ # the first patch set. Instead the message gets used as the
+ # new CL's overall subject. So omit the diffs when creating
+ # and then we'll run an immediate upload.
+ # This has the effect that every CL begins with an empty "Patch set 1".
+ if self.files and not creating:
+ vcs = MercurialVCS(upload_options, ui, repo)
+ data = vcs.GenerateDiff(self.files)
+ files = vcs.GetBaseFiles(data)
+ if len(data) > MAX_UPLOAD_SIZE:
+ uploaded_diff_file = []
+ form_fields.append(("separate_patches", "1"))
+ else:
+ uploaded_diff_file = [("data", "data.diff", data)]
+ else:
+ uploaded_diff_file = [("data", "data.diff", emptydiff)]
+
+ if vcs and
self.name != "new":
+ form_fields.append(("subject", "diff -r " + vcs.base_rev + " " +
getremote(ui, repo, {}).path))
+ else:
+ # First upload sets the subject for the CL itself.
+ form_fields.append(("subject", self.Subject()))
+ ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
+ response_body = MySend("/upload", body, content_type=ctype)
+ patchset = None
+ msg = response_body
+ lines = msg.splitlines()
+ if len(lines) >= 2:
+ msg = lines[0]
+ patchset = lines[1].strip()
+ patches = [x.split(" ", 1) for x in lines[2:]]
+ if response_body.startswith("Issue updated.") and quiet:
+ pass
+ else:
+ ui.status(msg + "\n")
+ set_status("uploaded CL metadata + diffs")
+ if not response_body.startswith("Issue created.") and not
response_body.startswith("Issue updated."):
+ raise util.Abort("failed to update issue: " + response_body)
+ issue = msg[msg.rfind("/")+1:]
+
self.name = issue
+ if not self.url:
+ self.url = server_url_base +
self.name
+ if not uploaded_diff_file:
+ set_status("uploading patches")
+ patches = UploadSeparatePatches(issue, rpc, patchset, data,
upload_options)
+ if vcs:
+ set_status("uploading base files")
+ vcs.UploadBaseFiles(issue, rpc, patches, patchset, upload_options,
files)
+ if send_mail:
+ set_status("sending mail")
+ MySend("/" + issue + "/mail", payload="")
+ self.web = True
+ set_status("flushing changes to disk")
+ self.Flush(ui, repo)
+ return
+
+ def Mail(self, ui, repo):
+ pmsg = "Hello " + JoinComma(self.reviewer)
+ if self.cc:
+ pmsg += " (cc: %s)" % (', '.join(self.cc),)
+ pmsg += ",\n"
+ pmsg += "\n"
+ repourl = getremote(ui, repo, {}).path
+ if not self.mailed:
+ pmsg += "I'd like you to review this change to\n" + repourl + "\n"
+ else:
+ pmsg += "Please take another look.\n"
+ typecheck(pmsg, str)
+ PostMessage(ui,
self.name, pmsg, subject=self.Subject())
+ self.mailed = True
+ self.Flush(ui, repo)
+
+def GoodCLName(name):
+ typecheck(name, str)
+ return re.match("^[0-9]+$", name)
+
+def ParseCL(text, name):
+ typecheck(text, str)
+ typecheck(name, str)
+ sname = None
+ lineno = 0
+ sections = {
+ 'Author': '',
+ 'Description': '',
+ 'Files': '',
+ 'URL': '',
+ 'Reviewer': '',
+ 'CC': '',
+ 'Mailed': '',
+ 'Private': '',
+ }
+ for line in text.split('\n'):
+ lineno += 1
+ line = line.rstrip()
+ if line != '' and line[0] == '#':
+ continue
+ if line == '' or line[0] == ' ' or line[0] == '\t':
+ if sname == None and line != '':
+ return None, lineno, 'text outside section'
+ if sname != None:
+ sections[sname] += line + '\n'
+ continue
+ p = line.find(':')
+ if p >= 0:
+ s, val = line[:p].strip(), line[p+1:].strip()
+ if s in sections:
+ sname = s
+ if val != '':
+ sections[sname] += val + '\n'
+ continue
+ return None, lineno, 'malformed section header'
+
+ for k in sections:
+ sections[k] = StripCommon(sections[k]).rstrip()
+
+ cl = CL(name)
+ if sections['Author']:
+ cl.copied_from = sections['Author']
+ cl.desc = sections['Description']
+ for line in sections['Files'].split('\n'):
+ i = line.find('#')
+ if i >= 0:
+ line = line[0:i].rstrip()
+ line = line.strip()
+ if line == '':
+ continue
+ cl.files.append(line)
+ cl.reviewer = SplitCommaSpace(sections['Reviewer'])
+ cl.cc = SplitCommaSpace(sections['CC'])
+ cl.url = sections['URL']
+ if sections['Mailed'] != 'False':
+ # Odd default, but avoids spurious mailings when
+ # reading old CLs that do not have a Mailed: line.
+ # CLs created with this update will always have
+ # Mailed: False on disk.
+ cl.mailed = True
+ if sections['Private'] in ('True', 'true', 'Yes', 'yes'):
+ cl.private = True
+ if cl.desc == '<enter description here>':
+ cl.desc = ''
+ return cl, 0, ''
+
+def SplitCommaSpace(s):
+ typecheck(s, str)
+ s = s.strip()
+ if s == "":
+ return []
+ return re.split(", *", s)
+
+def CutDomain(s):
+ typecheck(s, str)
+ i = s.find('@')
+ if i >= 0:
+ s = s[0:i]
+ return s
+
+def JoinComma(l):
+ for s in l:
+ typecheck(s, str)
+ return ", ".join(l)
+
+def ExceptionDetail():
+ s = str(sys.exc_info()[0])
+ if s.startswith("<type '") and s.endswith("'>"):
+ s = s[7:-2]
+ elif s.startswith("<class '") and s.endswith("'>"):
+ s = s[8:-2]
+ arg = str(sys.exc_info()[1])
+ if len(arg) > 0:
+ s += ": " + arg
+ return s
+
+def IsLocalCL(ui, repo, name):
+ return GoodCLName(name) and os.access(CodeReviewDir(ui, repo) + "/cl." +
name, 0)
+
+# Load CL from disk and/or the web.
+def LoadCL(ui, repo, name, web=True):
+ typecheck(name, str)
+ set_status("loading CL " + name)
+ if not GoodCLName(name):
+ return None, "invalid CL name"
+ dir = CodeReviewDir(ui, repo)
+ path = dir + "cl." + name
+ if os.access(path, 0):
+ ff = open(path)
+ text = ff.read()
+ ff.close()
+ cl, lineno, err = ParseCL(text, name)
+ if err != "":
+ return None, "malformed CL data: "+err
+ cl.local = True
+ else:
+ cl = CL(name)
+ if web:
+ set_status("getting issue metadata from web")
+ d = JSONGet(ui, "/api/" + name + "?messages=true")
+ set_status(None)
+ if d is None:
+ return None, "cannot load CL %s from server" % (name,)
+ if 'owner_email' not in d or 'issue' not in d or str(d['issue']) != name:
+ return None, "malformed response loading CL data from code review
server"
+ cl.dict = d
+ cl.reviewer = d.get('reviewers', [])
+ cl.cc = d.get('cc', [])
+ if cl.local and cl.copied_from and cl.desc:
+ # local copy of CL written by someone else
+ # and we saved a description. use that one,
+ # so that committers can edit the description
+ # before doing hg submit.
+ pass
+ else:
+ cl.desc = d.get('description', "")
+ cl.url = server_url_base + name
+ cl.web = True
+ cl.private = d.get('private', False) != False
+ set_status("loaded CL " + name)
+ 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):
+ def __init__(self, ui, repo, dir, f, web):
+ threading.Thread.__init__(self)
+ self.ui = ui
+ self.repo = repo
+ self.dir = dir
+ self.f = f
+ self.web = web
+
self.cl = None
+ def run(self):
+ cl, err = LoadCL(self.ui, self.repo, self.f[3:], web=self.web)
+ if err != '':
+ self.ui.warn("loading "+self.dir+self.f+": " + err + "\n")
+ return
+
self.cl = cl
+
+# Load all the CLs from this repository.
+def LoadAllCL(ui, repo, web=True):
+ dir = CodeReviewDir(ui, repo)
+ m = {}
+ files = [f for f in os.listdir(dir) if f.startswith('cl.')]
+ if not files:
+ return m
+ active = []
+ first = True
+ for f in files:
+ t = LoadCLThread(ui, repo, dir, f, web)
+ t.start()
+ if web and first:
+ # first request: wait in case it needs to authenticate
+ # otherwise we get lots of user/password prompts
+ # running in parallel.
+ t.join()
+ if
t.cl:
+ m[
t.cl.name] =
t.cl
+ first = False
+ else:
+ active.append(t)
+ for t in active:
+ t.join()
+ if
t.cl:
+ m[
t.cl.name] =
t.cl
+ return m
+
+# Find repository root. On error, ui.warn and return None
+def RepoDir(ui, repo):
+ url = repo.url();
+ if not url.startswith('file:'):
+ ui.warn("repository %s is not in local file system\n" % (url,))
+ return None
+ url = url[5:]
+ if url.endswith('/'):
+ url = url[:-1]
+ typecheck(url, str)
+ return url
+
+# Find (or make) code review directory. On error, ui.warn and return None
+def CodeReviewDir(ui, repo):
+ dir = RepoDir(ui, repo)
+ if dir == None:
+ return None
+ dir += '/.hg/codereview/'
+ if not os.path.isdir(dir):
+ try:
+ os.mkdir(dir, 0700)
+ except:
+ ui.warn('cannot mkdir %s: %s\n' % (dir, ExceptionDetail()))
+ return None
+ 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)
+ ws = None
+ for line in text.split('\n'):
+ line = line.rstrip()
+ if line == '':
+ continue
+ line = TabsToSpaces(line)
+ white = line[:len(line)-len(line.lstrip())]
+ if ws == None:
+ ws = white
+ else:
+ common = ''
+ for i in range(min(len(white), len(ws))+1):
+ if white[0:i] == ws[0:i]:
+ common = white[0:i]
+ ws = common
+ if ws == '':
+ break
+ if ws == None:
+ return 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 == '':
+ continue
+ t += line + '\n'
+ while len(t) >= 2 and t[-2:] == '\n\n':
+ t = t[:-1]
+ typecheck(t, str)
+ return t
+
+# Indent text with indent.
+def Indent(text, indent):
+ typecheck(text, str)
+ typecheck(indent, str)
+ t = ''
+ for line in text.split('\n'):
+ t += indent + line + '\n'
+ typecheck(t, str)
+ return t
+
+# Return the first line of l
+def line1(text):
+ typecheck(text, str)
+ return text.split('\n')[0]
+
+_change_prolog = """# Change list.
+# Lines beginning with # are ignored.
+# 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 cmdutil.revpair(repo, ['qparent'])
+ except:
+ return cmdutil.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 = cmdutil.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
+
+def Incoming(ui, repo, opts):
+ _, incoming, _ = findcommonincoming(repo, getremote(ui, repo, opts))
+ return incoming
+
+desc_re = '^(.+: |(tag )?(release|weekly)\.|fix build|undo CL)'
+
+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()
+ while True:
+ s = ui.edit(s, ui.username())
+ clx, line, err = ParseCL(s,
cl.name)
+ if err != '':
+ if not promptyesno(ui, "error parsing change list: line %d: %s\nre-edit
(y/n)?" % (line, err)):
+ return "change list not modified"
+ continue
+
+ # Check description.
+ if clx.desc == '':
+ if promptyesno(ui, "change list should have a description\nre-edit
(y/n)?"):
+ continue
+ elif re.search('<enter reason for undo>', clx.desc):
+ if promptyesno(ui, "change list description omits reason for
undo\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
+ cl.private = clx.private
+ break
+ return ""
+
+# For use by submit, etc. (NOT by change)
+# Get change list number or list of files from command line.
+# If files are given, make a new change list.
+def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
+ if len(pats) > 0 and GoodCLName(pats[0]):
+ if len(pats) != 1:
+ return None, "cannot specify change number and file names"
+ if opts.get('message'):
+ return None, "cannot use -m with existing CL"
+ cl, err = LoadCL(ui, repo, pats[0], web=True)
+ if err != "":
+ return None, err
+ else:
+ cl = CL("new")
+ cl.local = True
+ cl.files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
+ if not cl.files:
+ return None, "no files changed"
+ if opts.get('reviewer'):
+ cl.reviewer = Add(cl.reviewer, SplitCommaSpace(opts.get('reviewer')))
+ if opts.get('cc'):
+ cl.cc = Add(cl.cc, SplitCommaSpace(opts.get('cc')))
+ if defaultcc:
+ cl.cc = Add(cl.cc, defaultcc)
+ if
cl.name == "new":
+ if opts.get('message'):
+ cl.desc = opts.get('message')
+ else:
+ err = EditCL(ui, repo, cl)
+ if err != '':
+ return None, err
+ return cl, ""
+
+# reposetup replaces cmdutil.match with this wrapper,
+# which expands the syntax @clnumber to mean the files
+# in that CL.
+original_match = None
+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)
+ clname = p[1:]
+ if not GoodCLName(clname):
+ raise util.Abort("invalid CL name " + clname)
+ cl, err = LoadCL(repo.ui, repo, clname, web=False)
+ if err != '':
+ raise util.Abort("loading CL " + clname + ": " + err)
+ if not cl.files:
+ raise util.Abort("no files in CL " + clname)
+ files = Add(files, cl.files)
+ pats = Sub(pats, taken) + ['path:'+f for f in files]
+ return original_match(repo, pats=pats, opts=opts, globbed=globbed,
default=default)
+
+def RelativePath(path, cwd):
+ n = len(cwd)
+ if path.startswith(cwd) and path[n] == '/':
+ return path[n+1:]
+ return path
+
+def CheckFormat(ui, repo, files, just_warn=False):
+ # set_status("running gofmt")
+ # CheckGofmt(ui, repo, files, just_warn)
+ CheckTabfmt(ui, repo, files, just_warn)
+
+# Check that gofmt run on the list of files does not change them
+def CheckGofmt(ui, repo, files, just_warn):
+ files = [f for f in files if (f.startswith('src/') or
f.startswith('test/bench/')) and f.endswith('.go')]
+ if not files:
+ return
+ cwd = os.getcwd()
+ files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
+ files = [f for f in files if os.access(f, 0)]
+ if not files:
+ return
+ try:
+ cmd = subprocess.Popen(["gofmt", "-l"] + files, shell=False,
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
close_fds=sys.platform != "win32")
+ cmd.stdin.close()
+ except:
+ raise util.Abort("gofmt: " + ExceptionDetail())
+ data = cmd.stdout.read()
+ errors = cmd.stderr.read()
+ cmd.wait()
+ set_status("done with gofmt")
+ if len(errors) > 0:
+ ui.warn("gofmt errors:\n" + errors.rstrip() + "\n")
+ return
+ if len(data) > 0:
+ msg = "gofmt needs to format these files (run hg gofmt):\n" +
Indent(data, "\t").rstrip()
***The diff for this file has been truncated for email.***
=======================================
--- /dev/null
+++ /utils/lib/codereview/codereview.pyc Mon May 30 00:46:36 2011
Binary file, no diff available.