Echo Lineinitial commit9fd65008 years agoHistory
3562 lines · 104.0 KB · Python
RawDownload
# 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 inyour 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 introducesthe file pattern syntax @nnnnnn, where nnnnnn is a change listnumber, to mean the files included in that change list, whichmust 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".''' import sys if __name__ == "__main__":	print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly."	sys.exit(2) # We require Python 2.6 for the json package.if sys.version < '2.6':	print >>sys.stderr, "The codereview extension requires Python 2.6 or newer."	print >>sys.stderr, "You are running Python " + sys.version	sys.exit(2) import jsonimport osimport reimport statimport subprocessimport threadingimport time from mercurial import commands as hg_commandsfrom mercurial import util as hg_util defaultcc = Nonecodereview_disabled = Nonereal_rollback = NonereleaseBranch = Noneserver = "codereview.appspot.com"server_url_base = None ######################################################################## Normally I would split this into multiple files, but it simplifies# import path headaches to keep it all in one file.  Sorry.# The different parts of the file are separated by banners like this one. ######################################################################## Helpers def RelativePath(path, cwd):	n = len(cwd)	if path.startswith(cwd) and path[n] == '/':		return path[n+1:]	return path 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] ######################################################################## 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 hg_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	stdout, __stdout__ = sys.stdout, sys.__stdout__	reload(sys)  # site.py deleted setdefaultencoding; get it back	sys.stdout, sys.__stdout__ = stdout, __stdout__	sys.setdefaultencoding('utf-8') default_to_utf8() ######################################################################## Status printer for long-running commands global_status = None def set_status(s):	# print >>sys.stderr, "\t", time.asctime(), s	global global_status	global_status = s class StatusThread(threading.Thread):	def __init__(self):		threading.Thread.__init__(self)	def run(self):		# pause a reasonable amount of time before		# starting to display status messages, so that		# most hg commands won't ever see them.		time.sleep(30) 		# now show status every 15 seconds		while True:			time.sleep(15 - time.time() % 15)			s = global_status			if s is None:				continue			if s == "":				s = "(unknown status)"			print >>sys.stderr, time.asctime(), s def start_status_thread():	t = StatusThread()	t.setDaemon(True)  # allowed to exit if t is still running	t.start() ######################################################################## Change list parsing.## Change 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		self.lgtm = [] 	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, quick=False):		cl = self		s = cl.name + ":" + "\n"		s += Indent(cl.desc, "\t")		s += "\n"		if cl.copied_from:			s += "\tAuthor: " + cl.copied_from + "\n"		if not quick:			s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n"			for (who, line) in cl.lgtm:				s += "\t\t" + who + ": " + line + "\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 + " " + ui.expandpath("default")))		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 hg_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 = ui.expandpath("default")		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

Lines 1–500 of 3562.