github.com/ccccaoqing/test@v0.0.0-20220510085219-3985d23445c0/lib/codereview/codereview.py (about)

     1  # coding=utf-8
     2  # (The line above is necessary so that I can use 世界 in the
     3  # *comment* below without Python getting all bent out of shape.)
     4  
     5  # Copyright 2007-2009 Google Inc.
     6  #
     7  # Licensed under the Apache License, Version 2.0 (the "License");
     8  # you may not use this file except in compliance with the License.
     9  # You may obtain a copy of the License at
    10  #
    11  #	http://www.apache.org/licenses/LICENSE-2.0
    12  #
    13  # Unless required by applicable law or agreed to in writing, software
    14  # distributed under the License is distributed on an "AS IS" BASIS,
    15  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  # See the License for the specific language governing permissions and
    17  # limitations under the License.
    18  
    19  '''Mercurial interface to codereview.appspot.com.
    20  
    21  To configure, set the following options in
    22  your repository's .hg/hgrc file.
    23  
    24  	[extensions]
    25  	codereview = /path/to/codereview.py
    26  
    27  	[codereview]
    28  	server = codereview.appspot.com
    29  
    30  The server should be running Rietveld; see http://code.google.com/p/rietveld/.
    31  
    32  In addition to the new commands, this extension introduces
    33  the file pattern syntax @nnnnnn, where nnnnnn is a change list
    34  number, to mean the files included in that change list, which
    35  must be associated with the current client.
    36  
    37  For example, if change 123456 contains the files x.go and y.go,
    38  "hg diff @123456" is equivalent to"hg diff x.go y.go".
    39  '''
    40  
    41  import sys
    42  
    43  if __name__ == "__main__":
    44  	print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly."
    45  	sys.exit(2)
    46  
    47  # We require Python 2.6 for the json package.
    48  if sys.version < '2.6':
    49  	print >>sys.stderr, "The codereview extension requires Python 2.6 or newer."
    50  	print >>sys.stderr, "You are running Python " + sys.version
    51  	sys.exit(2)
    52  
    53  import json
    54  import os
    55  import re
    56  import stat
    57  import subprocess
    58  import threading
    59  import time
    60  
    61  from mercurial import commands as hg_commands
    62  from mercurial import util as hg_util
    63  
    64  # bind Plan 9 preferred dotfile location
    65  if os.sys.platform == 'plan9':
    66  	try:
    67  		import plan9
    68  		n = plan9.bind(os.path.expanduser("~/lib"), os.path.expanduser("~"), plan9.MBEFORE|plan9.MCREATE)
    69  	except ImportError:
    70  		pass
    71  
    72  defaultcc = None
    73  codereview_disabled = None
    74  real_rollback = None
    75  releaseBranch = None
    76  server = "codereview.appspot.com"
    77  server_url_base = None
    78  testing = None
    79  
    80  #######################################################################
    81  # Normally I would split this into multiple files, but it simplifies
    82  # import path headaches to keep it all in one file.  Sorry.
    83  # The different parts of the file are separated by banners like this one.
    84  
    85  #######################################################################
    86  # Helpers
    87  
    88  def RelativePath(path, cwd):
    89  	n = len(cwd)
    90  	if path.startswith(cwd) and path[n] == '/':
    91  		return path[n+1:]
    92  	return path
    93  
    94  def Sub(l1, l2):
    95  	return [l for l in l1 if l not in l2]
    96  
    97  def Add(l1, l2):
    98  	l = l1 + Sub(l2, l1)
    99  	l.sort()
   100  	return l
   101  
   102  def Intersect(l1, l2):
   103  	return [l for l in l1 if l in l2]
   104  
   105  #######################################################################
   106  # RE: UNICODE STRING HANDLING
   107  #
   108  # Python distinguishes between the str (string of bytes)
   109  # and unicode (string of code points) types.  Most operations
   110  # work on either one just fine, but some (like regexp matching)
   111  # require unicode, and others (like write) require str.
   112  #
   113  # As befits the language, Python hides the distinction between
   114  # unicode and str by converting between them silently, but
   115  # *only* if all the bytes/code points involved are 7-bit ASCII.
   116  # This means that if you're not careful, your program works
   117  # fine on "hello, world" and fails on "hello, 世界".  And of course,
   118  # the obvious way to be careful - use static types - is unavailable.
   119  # So the only way is trial and error to find where to put explicit
   120  # conversions.
   121  #
   122  # Because more functions do implicit conversion to str (string of bytes)
   123  # than do implicit conversion to unicode (string of code points),
   124  # the convention in this module is to represent all text as str,
   125  # converting to unicode only when calling a unicode-only function
   126  # and then converting back to str as soon as possible.
   127  
   128  def typecheck(s, t):
   129  	if type(s) != t:
   130  		raise hg_util.Abort("type check failed: %s has type %s != %s" % (repr(s), type(s), t))
   131  
   132  # If we have to pass unicode instead of str, ustr does that conversion clearly.
   133  def ustr(s):
   134  	typecheck(s, str)
   135  	return s.decode("utf-8")
   136  
   137  # Even with those, Mercurial still sometimes turns unicode into str
   138  # and then tries to use it as ascii.  Change Mercurial's default.
   139  def set_mercurial_encoding_to_utf8():
   140  	from mercurial import encoding
   141  	encoding.encoding = 'utf-8'
   142  
   143  set_mercurial_encoding_to_utf8()
   144  
   145  # Even with those we still run into problems.
   146  # I tried to do things by the book but could not convince
   147  # Mercurial to let me check in a change with UTF-8 in the
   148  # CL description or author field, no matter how many conversions
   149  # between str and unicode I inserted and despite changing the
   150  # default encoding.  I'm tired of this game, so set the default
   151  # encoding for all of Python to 'utf-8', not 'ascii'.
   152  def default_to_utf8():
   153  	import sys
   154  	stdout, __stdout__ = sys.stdout, sys.__stdout__
   155  	reload(sys)  # site.py deleted setdefaultencoding; get it back
   156  	sys.stdout, sys.__stdout__ = stdout, __stdout__
   157  	sys.setdefaultencoding('utf-8')
   158  
   159  default_to_utf8()
   160  
   161  #######################################################################
   162  # Status printer for long-running commands
   163  
   164  global_status = None
   165  
   166  def set_status(s):
   167  	if verbosity > 0:
   168  		print >>sys.stderr, time.asctime(), s
   169  	global global_status
   170  	global_status = s
   171  
   172  class StatusThread(threading.Thread):
   173  	def __init__(self):
   174  		threading.Thread.__init__(self)
   175  	def run(self):
   176  		# pause a reasonable amount of time before
   177  		# starting to display status messages, so that
   178  		# most hg commands won't ever see them.
   179  		time.sleep(30)
   180  
   181  		# now show status every 15 seconds
   182  		while True:
   183  			time.sleep(15 - time.time() % 15)
   184  			s = global_status
   185  			if s is None:
   186  				continue
   187  			if s == "":
   188  				s = "(unknown status)"
   189  			print >>sys.stderr, time.asctime(), s
   190  
   191  def start_status_thread():
   192  	t = StatusThread()
   193  	t.setDaemon(True)  # allowed to exit if t is still running
   194  	t.start()
   195  
   196  #######################################################################
   197  # Change list parsing.
   198  #
   199  # Change lists are stored in .hg/codereview/cl.nnnnnn
   200  # where nnnnnn is the number assigned by the code review server.
   201  # Most data about a change list is stored on the code review server
   202  # too: the description, reviewer, and cc list are all stored there.
   203  # The only thing in the cl.nnnnnn file is the list of relevant files.
   204  # Also, the existence of the cl.nnnnnn file marks this repository
   205  # as the one where the change list lives.
   206  
   207  emptydiff = """Index: ~rietveld~placeholder~
   208  ===================================================================
   209  diff --git a/~rietveld~placeholder~ b/~rietveld~placeholder~
   210  new file mode 100644
   211  """
   212  
   213  class CL(object):
   214  	def __init__(self, name):
   215  		typecheck(name, str)
   216  		self.name = name
   217  		self.desc = ''
   218  		self.files = []
   219  		self.reviewer = []
   220  		self.cc = []
   221  		self.url = ''
   222  		self.local = False
   223  		self.web = False
   224  		self.copied_from = None	# None means current user
   225  		self.mailed = False
   226  		self.private = False
   227  		self.lgtm = []
   228  
   229  	def DiskText(self):
   230  		cl = self
   231  		s = ""
   232  		if cl.copied_from:
   233  			s += "Author: " + cl.copied_from + "\n\n"
   234  		if cl.private:
   235  			s += "Private: " + str(self.private) + "\n"
   236  		s += "Mailed: " + str(self.mailed) + "\n"
   237  		s += "Description:\n"
   238  		s += Indent(cl.desc, "\t")
   239  		s += "Files:\n"
   240  		for f in cl.files:
   241  			s += "\t" + f + "\n"
   242  		typecheck(s, str)
   243  		return s
   244  
   245  	def EditorText(self):
   246  		cl = self
   247  		s = _change_prolog
   248  		s += "\n"
   249  		if cl.copied_from:
   250  			s += "Author: " + cl.copied_from + "\n"
   251  		if cl.url != '':
   252  			s += 'URL: ' + cl.url + '	# cannot edit\n\n'
   253  		if cl.private:
   254  			s += "Private: True\n"
   255  		s += "Reviewer: " + JoinComma(cl.reviewer) + "\n"
   256  		s += "CC: " + JoinComma(cl.cc) + "\n"
   257  		s += "\n"
   258  		s += "Description:\n"
   259  		if cl.desc == '':
   260  			s += "\t<enter description here>\n"
   261  		else:
   262  			s += Indent(cl.desc, "\t")
   263  		s += "\n"
   264  		if cl.local or cl.name == "new":
   265  			s += "Files:\n"
   266  			for f in cl.files:
   267  				s += "\t" + f + "\n"
   268  			s += "\n"
   269  		typecheck(s, str)
   270  		return s
   271  
   272  	def PendingText(self, quick=False):
   273  		cl = self
   274  		s = cl.name + ":" + "\n"
   275  		s += Indent(cl.desc, "\t")
   276  		s += "\n"
   277  		if cl.copied_from:
   278  			s += "\tAuthor: " + cl.copied_from + "\n"
   279  		if not quick:
   280  			s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n"
   281  			for (who, line, _) in cl.lgtm:
   282  				s += "\t\t" + who + ": " + line + "\n"
   283  			s += "\tCC: " + JoinComma(cl.cc) + "\n"
   284  		s += "\tFiles:\n"
   285  		for f in cl.files:
   286  			s += "\t\t" + f + "\n"
   287  		typecheck(s, str)
   288  		return s
   289  
   290  	def Flush(self, ui, repo):
   291  		if self.name == "new":
   292  			self.Upload(ui, repo, gofmt_just_warn=True, creating=True)
   293  		dir = CodeReviewDir(ui, repo)
   294  		path = dir + '/cl.' + self.name
   295  		f = open(path+'!', "w")
   296  		f.write(self.DiskText())
   297  		f.close()
   298  		if sys.platform == "win32" and os.path.isfile(path):
   299  			os.remove(path)
   300  		os.rename(path+'!', path)
   301  		if self.web and not self.copied_from:
   302  			EditDesc(self.name, desc=self.desc,
   303  				reviewers=JoinComma(self.reviewer), cc=JoinComma(self.cc),
   304  				private=self.private)
   305  
   306  	def Delete(self, ui, repo):
   307  		dir = CodeReviewDir(ui, repo)
   308  		os.unlink(dir + "/cl." + self.name)
   309  
   310  	def Subject(self, ui, repo):
   311  		s = line1(self.desc)
   312  		if len(s) > 60:
   313  			s = s[0:55] + "..."
   314  		if self.name != "new":
   315  			s = "code review %s: %s" % (self.name, s)
   316  		typecheck(s, str)
   317  		s = branch_prefix(ui, repo) + s
   318  		# Rietveld does a hard reject on any subject > 100 chars. Be sure.
   319  		if len(s) >= 100:
   320  			s = s[0:95] + "..."
   321  		return s
   322  
   323  	def Upload(self, ui, repo, send_mail=False, gofmt=True, gofmt_just_warn=False, creating=False, quiet=False):
   324  		if not self.files and not creating:
   325  			ui.warn("no files in change list\n")
   326  		if ui.configbool("codereview", "force_gofmt", True) and gofmt:
   327  			CheckFormat(ui, repo, self.files, just_warn=gofmt_just_warn)
   328  		set_status("uploading CL metadata + diffs")
   329  		os.chdir(repo.root)
   330  
   331  		form_fields = [
   332  			("content_upload", "1"),
   333  			("reviewers", JoinComma(self.reviewer)),
   334  			("cc", JoinComma(self.cc)),
   335  			("description", self.desc),
   336  			("base_hashes", ""),
   337  		]
   338  
   339  		if self.name != "new":
   340  			form_fields.append(("issue", self.name))
   341  		vcs = None
   342  		# We do not include files when creating the issue,
   343  		# because we want the patch sets to record the repository
   344  		# and base revision they are diffs against.  We use the patch
   345  		# set message for that purpose, but there is no message with
   346  		# the first patch set.  Instead the message gets used as the
   347  		# new CL's overall subject.  So omit the diffs when creating
   348  		# and then we'll run an immediate upload.
   349  		# This has the effect that every CL begins with an empty "Patch set 1".
   350  		if self.files and not creating:
   351  			vcs = MercurialVCS(upload_options, ui, repo)
   352  			data = vcs.GenerateDiff(self.files)
   353  			files = vcs.GetBaseFiles(data)
   354  			if len(data) > MAX_UPLOAD_SIZE:
   355  				uploaded_diff_file = []
   356  				form_fields.append(("separate_patches", "1"))
   357  			else:
   358  				uploaded_diff_file = [("data", "data.diff", data)]
   359  		else:
   360  			uploaded_diff_file = [("data", "data.diff", emptydiff)]
   361  		
   362  		if vcs and self.name != "new":
   363  			form_fields.append(("subject", "diff -r " + vcs.base_rev + " " + ui.expandpath("default")))
   364  		else:
   365  			# First upload sets the subject for the CL itself.
   366  			form_fields.append(("subject", self.Subject(ui, repo)))
   367  		
   368  		ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
   369  		response_body = MySend("/upload", body, content_type=ctype)
   370  		patchset = None
   371  		msg = response_body
   372  		lines = msg.splitlines()
   373  		if len(lines) >= 2:
   374  			msg = lines[0]
   375  			patchset = lines[1].strip()
   376  			patches = [x.split(" ", 1) for x in lines[2:]]
   377  		else:
   378  			print >>sys.stderr, "Server says there is nothing to upload (probably wrong):\n" + msg
   379  		if response_body.startswith("Issue updated.") and quiet:
   380  			pass
   381  		else:
   382  			ui.status(msg + "\n")
   383  		set_status("uploaded CL metadata + diffs")
   384  		if not response_body.startswith("Issue created.") and not response_body.startswith("Issue updated."):
   385  			raise hg_util.Abort("failed to update issue: " + response_body)
   386  		issue = msg[msg.rfind("/")+1:]
   387  		self.name = issue
   388  		if not self.url:
   389  			self.url = server_url_base + self.name
   390  		if not uploaded_diff_file:
   391  			set_status("uploading patches")
   392  			patches = UploadSeparatePatches(issue, rpc, patchset, data, upload_options)
   393  		if vcs:
   394  			set_status("uploading base files")
   395  			vcs.UploadBaseFiles(issue, rpc, patches, patchset, upload_options, files)
   396  		if patchset != "1":
   397  			MySend("/" + issue + "/upload_complete/" + patchset, payload="")
   398  		if send_mail:
   399  			set_status("sending mail")
   400  			MySend("/" + issue + "/mail", payload="")
   401  		self.web = True
   402  		set_status("flushing changes to disk")
   403  		self.Flush(ui, repo)
   404  		return
   405  
   406  	def Mail(self, ui, repo):
   407  		pmsg = "Hello " + JoinComma(self.reviewer)
   408  		if self.cc:
   409  			pmsg += " (cc: %s)" % (', '.join(self.cc),)
   410  		pmsg += ",\n"
   411  		pmsg += "\n"
   412  		repourl = ui.expandpath("default")
   413  		if not self.mailed:
   414  			pmsg += "I'd like you to review this change to"
   415  			branch = repo[None].branch()
   416  			if workbranch(branch) and branch != "default":
   417  				pmsg += " the " + branch + " branch of"
   418  			pmsg += "\n" + repourl + "\n"
   419  		else:
   420  			pmsg += "Please take another look.\n"
   421  		typecheck(pmsg, str)
   422  		PostMessage(ui, self.name, pmsg, subject=self.Subject(ui, repo))
   423  		self.mailed = True
   424  		self.Flush(ui, repo)
   425  
   426  def GoodCLName(name):
   427  	typecheck(name, str)
   428  	return re.match("^[0-9]+$", name)
   429  
   430  def ParseCL(text, name):
   431  	typecheck(text, str)
   432  	typecheck(name, str)
   433  	sname = None
   434  	lineno = 0
   435  	sections = {
   436  		'Author': '',
   437  		'Description': '',
   438  		'Files': '',
   439  		'URL': '',
   440  		'Reviewer': '',
   441  		'CC': '',
   442  		'Mailed': '',
   443  		'Private': '',
   444  	}
   445  	for line in text.split('\n'):
   446  		lineno += 1
   447  		line = line.rstrip()
   448  		if line != '' and line[0] == '#':
   449  			continue
   450  		if line == '' or line[0] == ' ' or line[0] == '\t':
   451  			if sname == None and line != '':
   452  				return None, lineno, 'text outside section'
   453  			if sname != None:
   454  				sections[sname] += line + '\n'
   455  			continue
   456  		p = line.find(':')
   457  		if p >= 0:
   458  			s, val = line[:p].strip(), line[p+1:].strip()
   459  			if s in sections:
   460  				sname = s
   461  				if val != '':
   462  					sections[sname] += val + '\n'
   463  				continue
   464  		return None, lineno, 'malformed section header'
   465  
   466  	for k in sections:
   467  		sections[k] = StripCommon(sections[k]).rstrip()
   468  
   469  	cl = CL(name)
   470  	if sections['Author']:
   471  		cl.copied_from = sections['Author']
   472  	cl.desc = sections['Description']
   473  	for line in sections['Files'].split('\n'):
   474  		i = line.find('#')
   475  		if i >= 0:
   476  			line = line[0:i].rstrip()
   477  		line = line.strip()
   478  		if line == '':
   479  			continue
   480  		cl.files.append(line)
   481  	cl.reviewer = SplitCommaSpace(sections['Reviewer'])
   482  	cl.cc = SplitCommaSpace(sections['CC'])
   483  	cl.url = sections['URL']
   484  	if sections['Mailed'] != 'False':
   485  		# Odd default, but avoids spurious mailings when
   486  		# reading old CLs that do not have a Mailed: line.
   487  		# CLs created with this update will always have 
   488  		# Mailed: False on disk.
   489  		cl.mailed = True
   490  	if sections['Private'] in ('True', 'true', 'Yes', 'yes'):
   491  		cl.private = True
   492  	if cl.desc == '<enter description here>':
   493  		cl.desc = ''
   494  	return cl, 0, ''
   495  
   496  def SplitCommaSpace(s):
   497  	typecheck(s, str)
   498  	s = s.strip()
   499  	if s == "":
   500  		return []
   501  	return re.split(", *", s)
   502  
   503  def CutDomain(s):
   504  	typecheck(s, str)
   505  	i = s.find('@')
   506  	if i >= 0:
   507  		s = s[0:i]
   508  	return s
   509  
   510  def JoinComma(l):
   511  	seen = {}
   512  	uniq = []
   513  	for s in l:
   514  		typecheck(s, str)
   515  		if s not in seen:
   516  			seen[s] = True
   517  			uniq.append(s)
   518  			
   519  	return ", ".join(uniq)
   520  
   521  def ExceptionDetail():
   522  	s = str(sys.exc_info()[0])
   523  	if s.startswith("<type '") and s.endswith("'>"):
   524  		s = s[7:-2]
   525  	elif s.startswith("<class '") and s.endswith("'>"):
   526  		s = s[8:-2]
   527  	arg = str(sys.exc_info()[1])
   528  	if len(arg) > 0:
   529  		s += ": " + arg
   530  	return s
   531  
   532  def IsLocalCL(ui, repo, name):
   533  	return GoodCLName(name) and os.access(CodeReviewDir(ui, repo) + "/cl." + name, 0)
   534  
   535  # Load CL from disk and/or the web.
   536  def LoadCL(ui, repo, name, web=True):
   537  	typecheck(name, str)
   538  	set_status("loading CL " + name)
   539  	if not GoodCLName(name):
   540  		return None, "invalid CL name"
   541  	dir = CodeReviewDir(ui, repo)
   542  	path = dir + "cl." + name
   543  	if os.access(path, 0):
   544  		ff = open(path)
   545  		text = ff.read()
   546  		ff.close()
   547  		cl, lineno, err = ParseCL(text, name)
   548  		if err != "":
   549  			return None, "malformed CL data: "+err
   550  		cl.local = True
   551  	else:
   552  		cl = CL(name)
   553  	if web:
   554  		set_status("getting issue metadata from web")
   555  		d = JSONGet(ui, "/api/" + name + "?messages=true")
   556  		set_status(None)
   557  		if d is None:
   558  			return None, "cannot load CL %s from server" % (name,)
   559  		if 'owner_email' not in d or 'issue' not in d or str(d['issue']) != name:
   560  			return None, "malformed response loading CL data from code review server"
   561  		cl.dict = d
   562  		cl.reviewer = d.get('reviewers', [])
   563  		cl.cc = d.get('cc', [])
   564  		if cl.local and cl.copied_from and cl.desc:
   565  			# local copy of CL written by someone else
   566  			# and we saved a description.  use that one,
   567  			# so that committers can edit the description
   568  			# before doing hg submit.
   569  			pass
   570  		else:
   571  			cl.desc = d.get('description', "")
   572  		cl.url = server_url_base + name
   573  		cl.web = True
   574  		cl.private = d.get('private', False) != False
   575  		cl.lgtm = []
   576  		for m in d.get('messages', []):
   577  			if m.get('approval', False) == True or m.get('disapproval', False) == True:
   578  				who = re.sub('@.*', '', m.get('sender', ''))
   579  				text = re.sub("\n(.|\n)*", '', m.get('text', ''))
   580  				cl.lgtm.append((who, text, m.get('approval', False)))
   581  
   582  	set_status("loaded CL " + name)
   583  	return cl, ''
   584  
   585  class LoadCLThread(threading.Thread):
   586  	def __init__(self, ui, repo, dir, f, web):
   587  		threading.Thread.__init__(self)
   588  		self.ui = ui
   589  		self.repo = repo
   590  		self.dir = dir
   591  		self.f = f
   592  		self.web = web
   593  		self.cl = None
   594  	def run(self):
   595  		cl, err = LoadCL(self.ui, self.repo, self.f[3:], web=self.web)
   596  		if err != '':
   597  			self.ui.warn("loading "+self.dir+self.f+": " + err + "\n")
   598  			return
   599  		self.cl = cl
   600  
   601  # Load all the CLs from this repository.
   602  def LoadAllCL(ui, repo, web=True):
   603  	dir = CodeReviewDir(ui, repo)
   604  	m = {}
   605  	files = [f for f in os.listdir(dir) if f.startswith('cl.')]
   606  	if not files:
   607  		return m
   608  	active = []
   609  	first = True
   610  	for f in files:
   611  		t = LoadCLThread(ui, repo, dir, f, web)
   612  		t.start()
   613  		if web and first:
   614  			# first request: wait in case it needs to authenticate
   615  			# otherwise we get lots of user/password prompts
   616  			# running in parallel.
   617  			t.join()
   618  			if t.cl:
   619  				m[t.cl.name] = t.cl
   620  			first = False
   621  		else:
   622  			active.append(t)
   623  	for t in active:
   624  		t.join()
   625  		if t.cl:
   626  			m[t.cl.name] = t.cl
   627  	return m
   628  
   629  # Find repository root.  On error, ui.warn and return None
   630  def RepoDir(ui, repo):
   631  	url = repo.url();
   632  	if not url.startswith('file:'):
   633  		ui.warn("repository %s is not in local file system\n" % (url,))
   634  		return None
   635  	url = url[5:]
   636  	if url.endswith('/'):
   637  		url = url[:-1]
   638  	typecheck(url, str)
   639  	return url
   640  
   641  # Find (or make) code review directory.  On error, ui.warn and return None
   642  def CodeReviewDir(ui, repo):
   643  	dir = RepoDir(ui, repo)
   644  	if dir == None:
   645  		return None
   646  	dir += '/.hg/codereview/'
   647  	if not os.path.isdir(dir):
   648  		try:
   649  			os.mkdir(dir, 0700)
   650  		except:
   651  			ui.warn('cannot mkdir %s: %s\n' % (dir, ExceptionDetail()))
   652  			return None
   653  	typecheck(dir, str)
   654  	return dir
   655  
   656  # Turn leading tabs into spaces, so that the common white space
   657  # prefix doesn't get confused when people's editors write out 
   658  # some lines with spaces, some with tabs.  Only a heuristic
   659  # (some editors don't use 8 spaces either) but a useful one.
   660  def TabsToSpaces(line):
   661  	i = 0
   662  	while i < len(line) and line[i] == '\t':
   663  		i += 1
   664  	return ' '*(8*i) + line[i:]
   665  
   666  # Strip maximal common leading white space prefix from text
   667  def StripCommon(text):
   668  	typecheck(text, str)
   669  	ws = None
   670  	for line in text.split('\n'):
   671  		line = line.rstrip()
   672  		if line == '':
   673  			continue
   674  		line = TabsToSpaces(line)
   675  		white = line[:len(line)-len(line.lstrip())]
   676  		if ws == None:
   677  			ws = white
   678  		else:
   679  			common = ''
   680  			for i in range(min(len(white), len(ws))+1):
   681  				if white[0:i] == ws[0:i]:
   682  					common = white[0:i]
   683  			ws = common
   684  		if ws == '':
   685  			break
   686  	if ws == None:
   687  		return text
   688  	t = ''
   689  	for line in text.split('\n'):
   690  		line = line.rstrip()
   691  		line = TabsToSpaces(line)
   692  		if line.startswith(ws):
   693  			line = line[len(ws):]
   694  		if line == '' and t == '':
   695  			continue
   696  		t += line + '\n'
   697  	while len(t) >= 2 and t[-2:] == '\n\n':
   698  		t = t[:-1]
   699  	typecheck(t, str)
   700  	return t
   701  
   702  # Indent text with indent.
   703  def Indent(text, indent):
   704  	typecheck(text, str)
   705  	typecheck(indent, str)
   706  	t = ''
   707  	for line in text.split('\n'):
   708  		t += indent + line + '\n'
   709  	typecheck(t, str)
   710  	return t
   711  
   712  # Return the first line of l
   713  def line1(text):
   714  	typecheck(text, str)
   715  	return text.split('\n')[0]
   716  
   717  _change_prolog = """# Change list.
   718  # Lines beginning with # are ignored.
   719  # Multi-line values should be indented.
   720  """
   721  
   722  desc_re = '^(.+: |(tag )?(release|weekly)\.|fix build|undo CL)'
   723  
   724  desc_msg = '''Your CL description appears not to use the standard form.
   725  
   726  The first line of your change description is conventionally a
   727  one-line summary of the change, prefixed by the primary affected package,
   728  and is used as the subject for code review mail; the rest of the description
   729  elaborates.
   730  
   731  Examples:
   732  
   733  	encoding/rot13: new package
   734  
   735  	math: add IsInf, IsNaN
   736  	
   737  	net: fix cname in LookupHost
   738  
   739  	unicode: update to Unicode 5.0.2
   740  
   741  '''
   742  
   743  def promptyesno(ui, msg):
   744  	if hgversion >= "2.7":
   745  		return ui.promptchoice(msg + " $$ &yes $$ &no", 0) == 0
   746  	else:
   747  		return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0
   748  
   749  def promptremove(ui, repo, f):
   750  	if promptyesno(ui, "hg remove %s (y/n)?" % (f,)):
   751  		if hg_commands.remove(ui, repo, 'path:'+f) != 0:
   752  			ui.warn("error removing %s" % (f,))
   753  
   754  def promptadd(ui, repo, f):
   755  	if promptyesno(ui, "hg add %s (y/n)?" % (f,)):
   756  		if hg_commands.add(ui, repo, 'path:'+f) != 0:
   757  			ui.warn("error adding %s" % (f,))
   758  
   759  def EditCL(ui, repo, cl):
   760  	set_status(None)	# do not show status
   761  	s = cl.EditorText()
   762  	while True:
   763  		s = ui.edit(s, ui.username())
   764  		
   765  		# We can't trust Mercurial + Python not to die before making the change,
   766  		# so, by popular demand, just scribble the most recent CL edit into
   767  		# $(hg root)/last-change so that if Mercurial does die, people
   768  		# can look there for their work.
   769  		try:
   770  			f = open(repo.root+"/last-change", "w")
   771  			f.write(s)
   772  			f.close()
   773  		except:
   774  			pass
   775  
   776  		clx, line, err = ParseCL(s, cl.name)
   777  		if err != '':
   778  			if not promptyesno(ui, "error parsing change list: line %d: %s\nre-edit (y/n)?" % (line, err)):
   779  				return "change list not modified"
   780  			continue
   781  		
   782  		# Check description.
   783  		if clx.desc == '':
   784  			if promptyesno(ui, "change list should have a description\nre-edit (y/n)?"):
   785  				continue
   786  		elif re.search('<enter reason for undo>', clx.desc):
   787  			if promptyesno(ui, "change list description omits reason for undo\nre-edit (y/n)?"):
   788  				continue
   789  		elif not re.match(desc_re, clx.desc.split('\n')[0]):
   790  			if promptyesno(ui, desc_msg + "re-edit (y/n)?"):
   791  				continue
   792  
   793  		# Check file list for files that need to be hg added or hg removed
   794  		# or simply aren't understood.
   795  		pats = ['path:'+f for f in clx.files]
   796  		changed = hg_matchPattern(ui, repo, *pats, modified=True, added=True, removed=True)
   797  		deleted = hg_matchPattern(ui, repo, *pats, deleted=True)
   798  		unknown = hg_matchPattern(ui, repo, *pats, unknown=True)
   799  		ignored = hg_matchPattern(ui, repo, *pats, ignored=True)
   800  		clean = hg_matchPattern(ui, repo, *pats, clean=True)
   801  		files = []
   802  		for f in clx.files:
   803  			if f in changed:
   804  				files.append(f)
   805  				continue
   806  			if f in deleted:
   807  				promptremove(ui, repo, f)
   808  				files.append(f)
   809  				continue
   810  			if f in unknown:
   811  				promptadd(ui, repo, f)
   812  				files.append(f)
   813  				continue
   814  			if f in ignored:
   815  				ui.warn("error: %s is excluded by .hgignore; omitting\n" % (f,))
   816  				continue
   817  			if f in clean:
   818  				ui.warn("warning: %s is listed in the CL but unchanged\n" % (f,))
   819  				files.append(f)
   820  				continue
   821  			p = repo.root + '/' + f
   822  			if os.path.isfile(p):
   823  				ui.warn("warning: %s is a file but not known to hg\n" % (f,))
   824  				files.append(f)
   825  				continue
   826  			if os.path.isdir(p):
   827  				ui.warn("error: %s is a directory, not a file; omitting\n" % (f,))
   828  				continue
   829  			ui.warn("error: %s does not exist; omitting\n" % (f,))
   830  		clx.files = files
   831  
   832  		cl.desc = clx.desc
   833  		cl.reviewer = clx.reviewer
   834  		cl.cc = clx.cc
   835  		cl.files = clx.files
   836  		cl.private = clx.private
   837  		break
   838  	return ""
   839  
   840  # For use by submit, etc. (NOT by change)
   841  # Get change list number or list of files from command line.
   842  # If files are given, make a new change list.
   843  def CommandLineCL(ui, repo, pats, opts, op="verb", defaultcc=None):
   844  	if len(pats) > 0 and GoodCLName(pats[0]):
   845  		if len(pats) != 1:
   846  			return None, "cannot specify change number and file names"
   847  		if opts.get('message'):
   848  			return None, "cannot use -m with existing CL"
   849  		cl, err = LoadCL(ui, repo, pats[0], web=True)
   850  		if err != "":
   851  			return None, err
   852  	else:
   853  		cl = CL("new")
   854  		cl.local = True
   855  		cl.files = ChangedFiles(ui, repo, pats, taken=Taken(ui, repo))
   856  		if not cl.files:
   857  			return None, "no files changed (use hg %s <number> to use existing CL)" % op
   858  	if opts.get('reviewer'):
   859  		cl.reviewer = Add(cl.reviewer, SplitCommaSpace(opts.get('reviewer')))
   860  	if opts.get('cc'):
   861  		cl.cc = Add(cl.cc, SplitCommaSpace(opts.get('cc')))
   862  	if defaultcc and not cl.private:
   863  		cl.cc = Add(cl.cc, defaultcc)
   864  	if cl.name == "new":
   865  		if opts.get('message'):
   866  			cl.desc = opts.get('message')
   867  		else:
   868  			err = EditCL(ui, repo, cl)
   869  			if err != '':
   870  				return None, err
   871  	return cl, ""
   872  
   873  #######################################################################
   874  # Change list file management
   875  
   876  # Return list of changed files in repository that match pats.
   877  # The patterns came from the command line, so we warn
   878  # if they have no effect or cannot be understood.
   879  def ChangedFiles(ui, repo, pats, taken=None):
   880  	taken = taken or {}
   881  	# Run each pattern separately so that we can warn about
   882  	# patterns that didn't do anything useful.
   883  	for p in pats:
   884  		for f in hg_matchPattern(ui, repo, p, unknown=True):
   885  			promptadd(ui, repo, f)
   886  		for f in hg_matchPattern(ui, repo, p, removed=True):
   887  			promptremove(ui, repo, f)
   888  		files = hg_matchPattern(ui, repo, p, modified=True, added=True, removed=True)
   889  		for f in files:
   890  			if f in taken:
   891  				ui.warn("warning: %s already in CL %s\n" % (f, taken[f].name))
   892  		if not files:
   893  			ui.warn("warning: %s did not match any modified files\n" % (p,))
   894  
   895  	# Again, all at once (eliminates duplicates)
   896  	l = hg_matchPattern(ui, repo, *pats, modified=True, added=True, removed=True)
   897  	l.sort()
   898  	if taken:
   899  		l = Sub(l, taken.keys())
   900  	return l
   901  
   902  # Return list of changed files in repository that match pats and still exist.
   903  def ChangedExistingFiles(ui, repo, pats, opts):
   904  	l = hg_matchPattern(ui, repo, *pats, modified=True, added=True)
   905  	l.sort()
   906  	return l
   907  
   908  # Return list of files claimed by existing CLs
   909  def Taken(ui, repo):
   910  	all = LoadAllCL(ui, repo, web=False)
   911  	taken = {}
   912  	for _, cl in all.items():
   913  		for f in cl.files:
   914  			taken[f] = cl
   915  	return taken
   916  
   917  # Return list of changed files that are not claimed by other CLs
   918  def DefaultFiles(ui, repo, pats):
   919  	return ChangedFiles(ui, repo, pats, taken=Taken(ui, repo))
   920  
   921  #######################################################################
   922  # File format checking.
   923  
   924  def CheckFormat(ui, repo, files, just_warn=False):
   925  	set_status("running gofmt")
   926  	CheckGofmt(ui, repo, files, just_warn)
   927  	CheckTabfmt(ui, repo, files, just_warn)
   928  
   929  # Check that gofmt run on the list of files does not change them
   930  def CheckGofmt(ui, repo, files, just_warn):
   931  	files = gofmt_required(files)
   932  	if not files:
   933  		return
   934  	cwd = os.getcwd()
   935  	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
   936  	files = [f for f in files if os.access(f, 0)]
   937  	if not files:
   938  		return
   939  	try:
   940  		cmd = subprocess.Popen(["gofmt", "-l"] + files, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=sys.platform != "win32")
   941  		cmd.stdin.close()
   942  	except:
   943  		raise hg_util.Abort("gofmt: " + ExceptionDetail())
   944  	data = cmd.stdout.read()
   945  	errors = cmd.stderr.read()
   946  	cmd.wait()
   947  	set_status("done with gofmt")
   948  	if len(errors) > 0:
   949  		ui.warn("gofmt errors:\n" + errors.rstrip() + "\n")
   950  		return
   951  	if len(data) > 0:
   952  		msg = "gofmt needs to format these files (run hg gofmt):\n" + Indent(data, "\t").rstrip()
   953  		if just_warn:
   954  			ui.warn("warning: " + msg + "\n")
   955  		else:
   956  			raise hg_util.Abort(msg)
   957  	return
   958  
   959  # Check that *.[chys] files indent using tabs.
   960  def CheckTabfmt(ui, repo, files, just_warn):
   961  	files = [f for f in files if f.startswith('src/') and re.search(r"\.[chys]$", f) and not re.search(r"\.tab\.[ch]$", f)]
   962  	if not files:
   963  		return
   964  	cwd = os.getcwd()
   965  	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
   966  	files = [f for f in files if os.access(f, 0)]
   967  	badfiles = []
   968  	for f in files:
   969  		try:
   970  			for line in open(f, 'r'):
   971  				# Four leading spaces is enough to complain about,
   972  				# except that some Plan 9 code uses four spaces as the label indent,
   973  				# so allow that.
   974  				if line.startswith('    ') and not re.match('    [A-Za-z0-9_]+:', line):
   975  					badfiles.append(f)
   976  					break
   977  		except:
   978  			# ignore cannot open file, etc.
   979  			pass
   980  	if len(badfiles) > 0:
   981  		msg = "these files use spaces for indentation (use tabs instead):\n\t" + "\n\t".join(badfiles)
   982  		if just_warn:
   983  			ui.warn("warning: " + msg + "\n")
   984  		else:
   985  			raise hg_util.Abort(msg)
   986  	return
   987  
   988  #######################################################################
   989  # CONTRIBUTORS file parsing
   990  
   991  contributorsCache = None
   992  contributorsURL = None
   993  
   994  def ReadContributors(ui, repo):
   995  	global contributorsCache
   996  	if contributorsCache is not None:
   997  		return contributorsCache
   998  
   999  	try:
  1000  		if contributorsURL is not None:
  1001  			opening = contributorsURL
  1002  			f = urllib2.urlopen(contributorsURL)
  1003  		else:
  1004  			opening = repo.root + '/CONTRIBUTORS'
  1005  			f = open(repo.root + '/CONTRIBUTORS', 'r')
  1006  	except:
  1007  		ui.write("warning: cannot open %s: %s\n" % (opening, ExceptionDetail()))
  1008  		return {}
  1009  
  1010  	contributors = {}
  1011  	for line in f:
  1012  		# CONTRIBUTORS is a list of lines like:
  1013  		#	Person <email>
  1014  		#	Person <email> <alt-email>
  1015  		# The first email address is the one used in commit logs.
  1016  		if line.startswith('#'):
  1017  			continue
  1018  		m = re.match(r"([^<>]+\S)\s+(<[^<>\s]+>)((\s+<[^<>\s]+>)*)\s*$", line)
  1019  		if m:
  1020  			name = m.group(1)
  1021  			email = m.group(2)[1:-1]
  1022  			contributors[email.lower()] = (name, email)
  1023  			for extra in m.group(3).split():
  1024  				contributors[extra[1:-1].lower()] = (name, email)
  1025  
  1026  	contributorsCache = contributors
  1027  	return contributors
  1028  
  1029  def CheckContributor(ui, repo, user=None):
  1030  	set_status("checking CONTRIBUTORS file")
  1031  	user, userline = FindContributor(ui, repo, user, warn=False)
  1032  	if not userline:
  1033  		raise hg_util.Abort("cannot find %s in CONTRIBUTORS" % (user,))
  1034  	return userline
  1035  
  1036  def FindContributor(ui, repo, user=None, warn=True):
  1037  	if not user:
  1038  		user = ui.config("ui", "username")
  1039  		if not user:
  1040  			raise hg_util.Abort("[ui] username is not configured in .hgrc")
  1041  	user = user.lower()
  1042  	m = re.match(r".*<(.*)>", user)
  1043  	if m:
  1044  		user = m.group(1)
  1045  
  1046  	contributors = ReadContributors(ui, repo)
  1047  	if user not in contributors:
  1048  		if warn:
  1049  			ui.warn("warning: cannot find %s in CONTRIBUTORS\n" % (user,))
  1050  		return user, None
  1051  	
  1052  	user, email = contributors[user]
  1053  	return email, "%s <%s>" % (user, email)
  1054  
  1055  #######################################################################
  1056  # Mercurial helper functions.
  1057  # Read http://mercurial.selenic.com/wiki/MercurialApi before writing any of these.
  1058  # We use the ui.pushbuffer/ui.popbuffer + hg_commands.xxx tricks for all interaction
  1059  # with Mercurial.  It has proved the most stable as they make changes.
  1060  
  1061  hgversion = hg_util.version()
  1062  
  1063  # We require Mercurial 1.9 and suggest Mercurial 2.1.
  1064  # The details of the scmutil package changed then,
  1065  # so allowing earlier versions would require extra band-aids below.
  1066  # Ubuntu 11.10 ships with Mercurial 1.9.1 as the default version.
  1067  hg_required = "1.9"
  1068  hg_suggested = "2.1"
  1069  
  1070  old_message = """
  1071  
  1072  The code review extension requires Mercurial """+hg_required+""" or newer.
  1073  You are using Mercurial """+hgversion+""".
  1074  
  1075  To install a new Mercurial, visit http://mercurial.selenic.com/downloads/.
  1076  """
  1077  
  1078  linux_message = """
  1079  You may need to clear your current Mercurial installation by running:
  1080  
  1081  	sudo apt-get remove mercurial mercurial-common
  1082  	sudo rm -rf /etc/mercurial
  1083  """
  1084  
  1085  if hgversion < hg_required:
  1086  	msg = old_message
  1087  	if os.access("/etc/mercurial", 0):
  1088  		msg += linux_message
  1089  	raise hg_util.Abort(msg)
  1090  
  1091  from mercurial.hg import clean as hg_clean
  1092  from mercurial import cmdutil as hg_cmdutil
  1093  from mercurial import error as hg_error
  1094  from mercurial import match as hg_match
  1095  from mercurial import node as hg_node
  1096  
  1097  class uiwrap(object):
  1098  	def __init__(self, ui):
  1099  		self.ui = ui
  1100  		ui.pushbuffer()
  1101  		self.oldQuiet = ui.quiet
  1102  		ui.quiet = True
  1103  		self.oldVerbose = ui.verbose
  1104  		ui.verbose = False
  1105  	def output(self):
  1106  		ui = self.ui
  1107  		ui.quiet = self.oldQuiet
  1108  		ui.verbose = self.oldVerbose
  1109  		return ui.popbuffer()
  1110  
  1111  def to_slash(path):
  1112  	if sys.platform == "win32":
  1113  		return path.replace('\\', '/')
  1114  	return path
  1115  
  1116  def hg_matchPattern(ui, repo, *pats, **opts):
  1117  	w = uiwrap(ui)
  1118  	hg_commands.status(ui, repo, *pats, **opts)
  1119  	text = w.output()
  1120  	ret = []
  1121  	prefix = to_slash(os.path.realpath(repo.root))+'/'
  1122  	for line in text.split('\n'):
  1123  		f = line.split()
  1124  		if len(f) > 1:
  1125  			if len(pats) > 0:
  1126  				# Given patterns, Mercurial shows relative to cwd
  1127  				p = to_slash(os.path.realpath(f[1]))
  1128  				if not p.startswith(prefix):
  1129  					print >>sys.stderr, "File %s not in repo root %s.\n" % (p, prefix)
  1130  				else:
  1131  					ret.append(p[len(prefix):])
  1132  			else:
  1133  				# Without patterns, Mercurial shows relative to root (what we want)
  1134  				ret.append(to_slash(f[1]))
  1135  	return ret
  1136  
  1137  def hg_heads(ui, repo):
  1138  	w = uiwrap(ui)
  1139  	hg_commands.heads(ui, repo)
  1140  	return w.output()
  1141  
  1142  noise = [
  1143  	"",
  1144  	"resolving manifests",
  1145  	"searching for changes",
  1146  	"couldn't find merge tool hgmerge",
  1147  	"adding changesets",
  1148  	"adding manifests",
  1149  	"adding file changes",
  1150  	"all local heads known remotely",
  1151  ]
  1152  
  1153  def isNoise(line):
  1154  	line = str(line)
  1155  	for x in noise:
  1156  		if line == x:
  1157  			return True
  1158  	return False
  1159  
  1160  def hg_incoming(ui, repo):
  1161  	w = uiwrap(ui)
  1162  	ret = hg_commands.incoming(ui, repo, force=False, bundle="")
  1163  	if ret and ret != 1:
  1164  		raise hg_util.Abort(ret)
  1165  	return w.output()
  1166  
  1167  def hg_log(ui, repo, **opts):
  1168  	for k in ['date', 'keyword', 'rev', 'user']:
  1169  		if not opts.has_key(k):
  1170  			opts[k] = ""
  1171  	w = uiwrap(ui)
  1172  	ret = hg_commands.log(ui, repo, **opts)
  1173  	if ret:
  1174  		raise hg_util.Abort(ret)
  1175  	return w.output()
  1176  
  1177  def hg_outgoing(ui, repo, **opts):
  1178  	w = uiwrap(ui)
  1179  	ret = hg_commands.outgoing(ui, repo, **opts)
  1180  	if ret and ret != 1:
  1181  		raise hg_util.Abort(ret)
  1182  	return w.output()
  1183  
  1184  def hg_pull(ui, repo, **opts):
  1185  	w = uiwrap(ui)
  1186  	ui.quiet = False
  1187  	ui.verbose = True  # for file list
  1188  	err = hg_commands.pull(ui, repo, **opts)
  1189  	for line in w.output().split('\n'):
  1190  		if isNoise(line):
  1191  			continue
  1192  		if line.startswith('moving '):
  1193  			line = 'mv ' + line[len('moving '):]
  1194  		if line.startswith('getting ') and line.find(' to ') >= 0:
  1195  			line = 'mv ' + line[len('getting '):]
  1196  		if line.startswith('getting '):
  1197  			line = '+ ' + line[len('getting '):]
  1198  		if line.startswith('removing '):
  1199  			line = '- ' + line[len('removing '):]
  1200  		ui.write(line + '\n')
  1201  	return err
  1202  
  1203  def hg_update(ui, repo, **opts):
  1204  	w = uiwrap(ui)
  1205  	ui.quiet = False
  1206  	ui.verbose = True  # for file list
  1207  	err = hg_commands.update(ui, repo, **opts)
  1208  	for line in w.output().split('\n'):
  1209  		if isNoise(line):
  1210  			continue
  1211  		if line.startswith('moving '):
  1212  			line = 'mv ' + line[len('moving '):]
  1213  		if line.startswith('getting ') and line.find(' to ') >= 0:
  1214  			line = 'mv ' + line[len('getting '):]
  1215  		if line.startswith('getting '):
  1216  			line = '+ ' + line[len('getting '):]
  1217  		if line.startswith('removing '):
  1218  			line = '- ' + line[len('removing '):]
  1219  		ui.write(line + '\n')
  1220  	return err
  1221  
  1222  def hg_push(ui, repo, **opts):
  1223  	w = uiwrap(ui)
  1224  	ui.quiet = False
  1225  	ui.verbose = True
  1226  	err = hg_commands.push(ui, repo, **opts)
  1227  	for line in w.output().split('\n'):
  1228  		if not isNoise(line):
  1229  			ui.write(line + '\n')
  1230  	return err
  1231  
  1232  def hg_commit(ui, repo, *pats, **opts):
  1233  	return hg_commands.commit(ui, repo, *pats, **opts)
  1234  
  1235  #######################################################################
  1236  # Mercurial precommit hook to disable commit except through this interface.
  1237  
  1238  commit_okay = False
  1239  
  1240  def precommithook(ui, repo, **opts):
  1241  	if hgversion >= "2.1":
  1242  		from mercurial import phases
  1243  		if repo.ui.config('phases', 'new-commit') >= phases.secret:
  1244  			return False
  1245  	if commit_okay:
  1246  		return False  # False means okay.
  1247  	ui.write("\ncodereview extension enabled; use mail, upload, or submit instead of commit\n\n")
  1248  	return True
  1249  
  1250  #######################################################################
  1251  # @clnumber file pattern support
  1252  
  1253  # We replace scmutil.match with the MatchAt wrapper to add the @clnumber pattern.
  1254  
  1255  match_repo = None
  1256  match_ui = None
  1257  match_orig = None
  1258  
  1259  def InstallMatch(ui, repo):
  1260  	global match_repo
  1261  	global match_ui
  1262  	global match_orig
  1263  
  1264  	match_ui = ui
  1265  	match_repo = repo
  1266  
  1267  	from mercurial import scmutil
  1268  	match_orig = scmutil.match
  1269  	scmutil.match = MatchAt
  1270  
  1271  def MatchAt(ctx, pats=None, opts=None, globbed=False, default='relpath'):
  1272  	taken = []
  1273  	files = []
  1274  	pats = pats or []
  1275  	opts = opts or {}
  1276  	
  1277  	for p in pats:
  1278  		if p.startswith('@'):
  1279  			taken.append(p)
  1280  			clname = p[1:]
  1281  			if clname == "default":
  1282  				files = DefaultFiles(match_ui, match_repo, [])
  1283  			else:
  1284  				if not GoodCLName(clname):
  1285  					raise hg_util.Abort("invalid CL name " + clname)
  1286  				cl, err = LoadCL(match_repo.ui, match_repo, clname, web=False)
  1287  				if err != '':
  1288  					raise hg_util.Abort("loading CL " + clname + ": " + err)
  1289  				if not cl.files:
  1290  					raise hg_util.Abort("no files in CL " + clname)
  1291  				files = Add(files, cl.files)
  1292  	pats = Sub(pats, taken) + ['path:'+f for f in files]
  1293  
  1294  	# work-around for http://selenic.com/hg/rev/785bbc8634f8
  1295  	if not hasattr(ctx, 'match'):
  1296  		ctx = ctx[None]
  1297  	return match_orig(ctx, pats=pats, opts=opts, globbed=globbed, default=default)
  1298  
  1299  #######################################################################
  1300  # Commands added by code review extension.
  1301  
  1302  def hgcommand(f):
  1303  	return f
  1304  
  1305  #######################################################################
  1306  # hg change
  1307  
  1308  @hgcommand
  1309  def change(ui, repo, *pats, **opts):
  1310  	"""create, edit or delete a change list
  1311  
  1312  	Create, edit or delete a change list.
  1313  	A change list is a group of files to be reviewed and submitted together,
  1314  	plus a textual description of the change.
  1315  	Change lists are referred to by simple alphanumeric names.
  1316  
  1317  	Changes must be reviewed before they can be submitted.
  1318  
  1319  	In the absence of options, the change command opens the
  1320  	change list for editing in the default editor.
  1321  
  1322  	Deleting a change with the -d or -D flag does not affect
  1323  	the contents of the files listed in that change.  To revert
  1324  	the files listed in a change, use
  1325  
  1326  		hg revert @123456
  1327  
  1328  	before running hg change -d 123456.
  1329  	"""
  1330  
  1331  	if codereview_disabled:
  1332  		raise hg_util.Abort(codereview_disabled)
  1333  	
  1334  	dirty = {}
  1335  	if len(pats) > 0 and GoodCLName(pats[0]):
  1336  		name = pats[0]
  1337  		if len(pats) != 1:
  1338  			raise hg_util.Abort("cannot specify CL name and file patterns")
  1339  		pats = pats[1:]
  1340  		cl, err = LoadCL(ui, repo, name, web=True)
  1341  		if err != '':
  1342  			raise hg_util.Abort(err)
  1343  		if not cl.local and (opts["stdin"] or not opts["stdout"]):
  1344  			raise hg_util.Abort("cannot change non-local CL " + name)
  1345  	else:
  1346  		name = "new"
  1347  		cl = CL("new")
  1348  		if not workbranch(repo[None].branch()):
  1349  			raise hg_util.Abort("cannot create CL outside default branch; switch with 'hg update default'")
  1350  		dirty[cl] = True
  1351  		files = ChangedFiles(ui, repo, pats, taken=Taken(ui, repo))
  1352  
  1353  	if opts["delete"] or opts["deletelocal"]:
  1354  		if opts["delete"] and opts["deletelocal"]:
  1355  			raise hg_util.Abort("cannot use -d and -D together")
  1356  		flag = "-d"
  1357  		if opts["deletelocal"]:
  1358  			flag = "-D"
  1359  		if name == "new":
  1360  			raise hg_util.Abort("cannot use "+flag+" with file patterns")
  1361  		if opts["stdin"] or opts["stdout"]:
  1362  			raise hg_util.Abort("cannot use "+flag+" with -i or -o")
  1363  		if not cl.local:
  1364  			raise hg_util.Abort("cannot change non-local CL " + name)
  1365  		if opts["delete"]:
  1366  			if cl.copied_from:
  1367  				raise hg_util.Abort("original author must delete CL; hg change -D will remove locally")
  1368  			PostMessage(ui, cl.name, "*** Abandoned ***", send_mail=cl.mailed)
  1369  			EditDesc(cl.name, closed=True, private=cl.private)
  1370  		cl.Delete(ui, repo)
  1371  		return
  1372  
  1373  	if opts["stdin"]:
  1374  		s = sys.stdin.read()
  1375  		clx, line, err = ParseCL(s, name)
  1376  		if err != '':
  1377  			raise hg_util.Abort("error parsing change list: line %d: %s" % (line, err))
  1378  		if clx.desc is not None:
  1379  			cl.desc = clx.desc;
  1380  			dirty[cl] = True
  1381  		if clx.reviewer is not None:
  1382  			cl.reviewer = clx.reviewer
  1383  			dirty[cl] = True
  1384  		if clx.cc is not None:
  1385  			cl.cc = clx.cc
  1386  			dirty[cl] = True
  1387  		if clx.files is not None:
  1388  			cl.files = clx.files
  1389  			dirty[cl] = True
  1390  		if clx.private != cl.private:
  1391  			cl.private = clx.private
  1392  			dirty[cl] = True
  1393  
  1394  	if not opts["stdin"] and not opts["stdout"]:
  1395  		if name == "new":
  1396  			cl.files = files
  1397  		err = EditCL(ui, repo, cl)
  1398  		if err != "":
  1399  			raise hg_util.Abort(err)
  1400  		dirty[cl] = True
  1401  
  1402  	for d, _ in dirty.items():
  1403  		name = d.name
  1404  		d.Flush(ui, repo)
  1405  		if name == "new":
  1406  			d.Upload(ui, repo, quiet=True)
  1407  
  1408  	if opts["stdout"]:
  1409  		ui.write(cl.EditorText())
  1410  	elif opts["pending"]:
  1411  		ui.write(cl.PendingText())
  1412  	elif name == "new":
  1413  		if ui.quiet:
  1414  			ui.write(cl.name)
  1415  		else:
  1416  			ui.write("CL created: " + cl.url + "\n")
  1417  	return
  1418  
  1419  #######################################################################
  1420  # hg code-login (broken?)
  1421  
  1422  @hgcommand
  1423  def code_login(ui, repo, **opts):
  1424  	"""log in to code review server
  1425  
  1426  	Logs in to the code review server, saving a cookie in
  1427  	a file in your home directory.
  1428  	"""
  1429  	if codereview_disabled:
  1430  		raise hg_util.Abort(codereview_disabled)
  1431  
  1432  	MySend(None)
  1433  
  1434  #######################################################################
  1435  # hg clpatch / undo / release-apply / download
  1436  # All concerned with applying or unapplying patches to the repository.
  1437  
  1438  @hgcommand
  1439  def clpatch(ui, repo, clname, **opts):
  1440  	"""import a patch from the code review server
  1441  
  1442  	Imports a patch from the code review server into the local client.
  1443  	If the local client has already modified any of the files that the
  1444  	patch modifies, this command will refuse to apply the patch.
  1445  
  1446  	Submitting an imported patch will keep the original author's
  1447  	name as the Author: line but add your own name to a Committer: line.
  1448  	"""
  1449  	if not workbranch(repo[None].branch()):
  1450  		raise hg_util.Abort("cannot run hg clpatch outside default branch")
  1451  	err = clpatch_or_undo(ui, repo, clname, opts, mode="clpatch")
  1452  	if err:
  1453  		raise hg_util.Abort(err)
  1454  
  1455  @hgcommand
  1456  def undo(ui, repo, clname, **opts):
  1457  	"""undo the effect of a CL
  1458  	
  1459  	Creates a new CL that undoes an earlier CL.
  1460  	After creating the CL, opens the CL text for editing so that
  1461  	you can add the reason for the undo to the description.
  1462  	"""
  1463  	if not workbranch(repo[None].branch()):
  1464  		raise hg_util.Abort("cannot run hg undo outside default branch")
  1465  	err = clpatch_or_undo(ui, repo, clname, opts, mode="undo")
  1466  	if err:
  1467  		raise hg_util.Abort(err)
  1468  
  1469  @hgcommand
  1470  def release_apply(ui, repo, clname, **opts):
  1471  	"""apply a CL to the release branch
  1472  
  1473  	Creates a new CL copying a previously committed change
  1474  	from the main branch to the release branch.
  1475  	The current client must either be clean or already be in
  1476  	the release branch.
  1477  	
  1478  	The release branch must be created by starting with a
  1479  	clean client, disabling the code review plugin, and running:
  1480  	
  1481  		hg update weekly.YYYY-MM-DD
  1482  		hg branch release-branch.rNN
  1483  		hg commit -m 'create release-branch.rNN'
  1484  		hg push --new-branch
  1485  	
  1486  	Then re-enable the code review plugin.
  1487  	
  1488  	People can test the release branch by running
  1489  	
  1490  		hg update release-branch.rNN
  1491  	
  1492  	in a clean client.  To return to the normal tree,
  1493  	
  1494  		hg update default
  1495  	
  1496  	Move changes since the weekly into the release branch 
  1497  	using hg release-apply followed by the usual code review
  1498  	process and hg submit.
  1499  
  1500  	When it comes time to tag the release, record the
  1501  	final long-form tag of the release-branch.rNN
  1502  	in the *default* branch's .hgtags file.  That is, run
  1503  	
  1504  		hg update default
  1505  	
  1506  	and then edit .hgtags as you would for a weekly.
  1507  		
  1508  	"""
  1509  	c = repo[None]
  1510  	if not releaseBranch:
  1511  		raise hg_util.Abort("no active release branches")
  1512  	if c.branch() != releaseBranch:
  1513  		if c.modified() or c.added() or c.removed():
  1514  			raise hg_util.Abort("uncommitted local changes - cannot switch branches")
  1515  		err = hg_clean(repo, releaseBranch)
  1516  		if err:
  1517  			raise hg_util.Abort(err)
  1518  	try:
  1519  		err = clpatch_or_undo(ui, repo, clname, opts, mode="backport")
  1520  		if err:
  1521  			raise hg_util.Abort(err)
  1522  	except Exception, e:
  1523  		hg_clean(repo, "default")
  1524  		raise e
  1525  
  1526  def rev2clname(rev):
  1527  	# Extract CL name from revision description.
  1528  	# The last line in the description that is a codereview URL is the real one.
  1529  	# Earlier lines might be part of the user-written description.
  1530  	all = re.findall('(?m)^https?://codereview.appspot.com/([0-9]+)$', rev.description())
  1531  	if len(all) > 0:
  1532  		return all[-1]
  1533  	return ""
  1534  
  1535  undoHeader = """undo CL %s / %s
  1536  
  1537  <enter reason for undo>
  1538  
  1539  ««« original CL description
  1540  """
  1541  
  1542  undoFooter = """
  1543  »»»
  1544  """
  1545  
  1546  backportHeader = """[%s] %s
  1547  
  1548  ««« CL %s / %s
  1549  """
  1550  
  1551  backportFooter = """
  1552  »»»
  1553  """
  1554  
  1555  # Implementation of clpatch/undo.
  1556  def clpatch_or_undo(ui, repo, clname, opts, mode):
  1557  	if codereview_disabled:
  1558  		return codereview_disabled
  1559  
  1560  	if mode == "undo" or mode == "backport":
  1561  		# Find revision in Mercurial repository.
  1562  		# Assume CL number is 7+ decimal digits.
  1563  		# Otherwise is either change log sequence number (fewer decimal digits),
  1564  		# hexadecimal hash, or tag name.
  1565  		# Mercurial will fall over long before the change log
  1566  		# sequence numbers get to be 7 digits long.
  1567  		if re.match('^[0-9]{7,}$', clname):
  1568  			found = False
  1569  			for r in hg_log(ui, repo, keyword="codereview.appspot.com/"+clname, limit=100, template="{node}\n").split():
  1570  				rev = repo[r]
  1571  				# Last line with a code review URL is the actual review URL.
  1572  				# Earlier ones might be part of the CL description.
  1573  				n = rev2clname(rev)
  1574  				if n == clname:
  1575  					found = True
  1576  					break
  1577  			if not found:
  1578  				return "cannot find CL %s in local repository" % clname
  1579  		else:
  1580  			rev = repo[clname]
  1581  			if not rev:
  1582  				return "unknown revision %s" % clname
  1583  			clname = rev2clname(rev)
  1584  			if clname == "":
  1585  				return "cannot find CL name in revision description"
  1586  		
  1587  		# Create fresh CL and start with patch that would reverse the change.
  1588  		vers = hg_node.short(rev.node())
  1589  		cl = CL("new")
  1590  		desc = str(rev.description())
  1591  		if mode == "undo":
  1592  			cl.desc = (undoHeader % (clname, vers)) + desc + undoFooter
  1593  		else:
  1594  			cl.desc = (backportHeader % (releaseBranch, line1(desc), clname, vers)) + desc + undoFooter
  1595  		v1 = vers
  1596  		v0 = hg_node.short(rev.parents()[0].node())
  1597  		if mode == "undo":
  1598  			arg = v1 + ":" + v0
  1599  		else:
  1600  			vers = v0
  1601  			arg = v0 + ":" + v1
  1602  		patch = RunShell(["hg", "diff", "--git", "-r", arg])
  1603  
  1604  	else:  # clpatch
  1605  		cl, vers, patch, err = DownloadCL(ui, repo, clname)
  1606  		if err != "":
  1607  			return err
  1608  		if patch == emptydiff:
  1609  			return "codereview issue %s has no diff" % clname
  1610  
  1611  	# find current hg version (hg identify)
  1612  	ctx = repo[None]
  1613  	parents = ctx.parents()
  1614  	id = '+'.join([hg_node.short(p.node()) for p in parents])
  1615  
  1616  	# if version does not match the patch version,
  1617  	# try to update the patch line numbers.
  1618  	if vers != "" and id != vers:
  1619  		# "vers in repo" gives the wrong answer
  1620  		# on some versions of Mercurial.  Instead, do the actual
  1621  		# lookup and catch the exception.
  1622  		try:
  1623  			repo[vers].description()
  1624  		except:
  1625  			return "local repository is out of date; sync to get %s" % (vers)
  1626  		patch1, err = portPatch(repo, patch, vers, id)
  1627  		if err != "":
  1628  			if not opts["ignore_hgapplydiff_failure"]:
  1629  				return "codereview issue %s is out of date: %s (%s->%s)" % (clname, err, vers, id)
  1630  		else:
  1631  			patch = patch1
  1632  	argv = ["hgapplydiff"]
  1633  	if opts["no_incoming"] or mode == "backport":
  1634  		argv += ["--checksync=false"]
  1635  	try:
  1636  		cmd = subprocess.Popen(argv, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, close_fds=sys.platform != "win32")
  1637  	except:
  1638  		return "hgapplydiff: " + ExceptionDetail() + "\nInstall hgapplydiff with:\n$ go get golang.org/x/codereview/cmd/hgapplydiff\n"
  1639  
  1640  	out, err = cmd.communicate(patch)
  1641  	if cmd.returncode != 0 and not opts["ignore_hgapplydiff_failure"]:
  1642  		return "hgapplydiff failed"
  1643  	cl.local = True
  1644  	cl.files = out.strip().split()
  1645  	if not cl.files and not opts["ignore_hgapplydiff_failure"]:
  1646  		return "codereview issue %s has no changed files" % clname
  1647  	files = ChangedFiles(ui, repo, [])
  1648  	extra = Sub(cl.files, files)
  1649  	if extra:
  1650  		ui.warn("warning: these files were listed in the patch but not changed:\n\t" + "\n\t".join(extra) + "\n")
  1651  	cl.Flush(ui, repo)
  1652  	if mode == "undo":
  1653  		err = EditCL(ui, repo, cl)
  1654  		if err != "":
  1655  			return "CL created, but error editing: " + err
  1656  		cl.Flush(ui, repo)
  1657  	else:
  1658  		ui.write(cl.PendingText() + "\n")
  1659  
  1660  # portPatch rewrites patch from being a patch against
  1661  # oldver to being a patch against newver.
  1662  def portPatch(repo, patch, oldver, newver):
  1663  	lines = patch.splitlines(True) # True = keep \n
  1664  	delta = None
  1665  	for i in range(len(lines)):
  1666  		line = lines[i]
  1667  		if line.startswith('--- a/'):
  1668  			file = line[6:-1]
  1669  			delta = fileDeltas(repo, file, oldver, newver)
  1670  		if not delta or not line.startswith('@@ '):
  1671  			continue
  1672  		# @@ -x,y +z,w @@ means the patch chunk replaces
  1673  		# the original file's line numbers x up to x+y with the
  1674  		# line numbers z up to z+w in the new file.
  1675  		# Find the delta from x in the original to the same
  1676  		# line in the current version and add that delta to both
  1677  		# x and z.
  1678  		m = re.match('@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@', line)
  1679  		if not m:
  1680  			return None, "error parsing patch line numbers"
  1681  		n1, len1, n2, len2 = int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
  1682  		d, err = lineDelta(delta, n1, len1)
  1683  		if err != "":
  1684  			return "", err
  1685  		n1 += d
  1686  		n2 += d
  1687  		lines[i] = "@@ -%d,%d +%d,%d @@\n" % (n1, len1, n2, len2)
  1688  		
  1689  	newpatch = ''.join(lines)
  1690  	return newpatch, ""
  1691  
  1692  # fileDelta returns the line number deltas for the given file's
  1693  # changes from oldver to newver.
  1694  # The deltas are a list of (n, len, newdelta) triples that say
  1695  # lines [n, n+len) were modified, and after that range the
  1696  # line numbers are +newdelta from what they were before.
  1697  def fileDeltas(repo, file, oldver, newver):
  1698  	cmd = ["hg", "diff", "--git", "-r", oldver + ":" + newver, "path:" + file]
  1699  	data = RunShell(cmd, silent_ok=True)
  1700  	deltas = []
  1701  	for line in data.splitlines():
  1702  		m = re.match('@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@', line)
  1703  		if not m:
  1704  			continue
  1705  		n1, len1, n2, len2 = int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
  1706  		deltas.append((n1, len1, n2+len2-(n1+len1)))
  1707  	return deltas
  1708  
  1709  # lineDelta finds the appropriate line number delta to apply to the lines [n, n+len).
  1710  # It returns an error if those lines were rewritten by the patch.
  1711  def lineDelta(deltas, n, len):
  1712  	d = 0
  1713  	for (old, oldlen, newdelta) in deltas:
  1714  		if old >= n+len:
  1715  			break
  1716  		if old+len > n:
  1717  			return 0, "patch and recent changes conflict"
  1718  		d = newdelta
  1719  	return d, ""
  1720  
  1721  @hgcommand
  1722  def download(ui, repo, clname, **opts):
  1723  	"""download a change from the code review server
  1724  
  1725  	Download prints a description of the given change list
  1726  	followed by its diff, downloaded from the code review server.
  1727  	"""
  1728  	if codereview_disabled:
  1729  		raise hg_util.Abort(codereview_disabled)
  1730  
  1731  	cl, vers, patch, err = DownloadCL(ui, repo, clname)
  1732  	if err != "":
  1733  		return err
  1734  	ui.write(cl.EditorText() + "\n")
  1735  	ui.write(patch + "\n")
  1736  	return
  1737  
  1738  #######################################################################
  1739  # hg file
  1740  
  1741  @hgcommand
  1742  def file(ui, repo, clname, pat, *pats, **opts):
  1743  	"""assign files to or remove files from a change list
  1744  
  1745  	Assign files to or (with -d) remove files from a change list.
  1746  
  1747  	The -d option only removes files from the change list.
  1748  	It does not edit them or remove them from the repository.
  1749  	"""
  1750  	if codereview_disabled:
  1751  		raise hg_util.Abort(codereview_disabled)
  1752  
  1753  	pats = tuple([pat] + list(pats))
  1754  	if not GoodCLName(clname):
  1755  		return "invalid CL name " + clname
  1756  
  1757  	dirty = {}
  1758  	cl, err = LoadCL(ui, repo, clname, web=False)
  1759  	if err != '':
  1760  		return err
  1761  	if not cl.local:
  1762  		return "cannot change non-local CL " + clname
  1763  
  1764  	files = ChangedFiles(ui, repo, pats)
  1765  
  1766  	if opts["delete"]:
  1767  		oldfiles = Intersect(files, cl.files)
  1768  		if oldfiles:
  1769  			if not ui.quiet:
  1770  				ui.status("# Removing files from CL.  To undo:\n")
  1771  				ui.status("#	cd %s\n" % (repo.root))
  1772  				for f in oldfiles:
  1773  					ui.status("#	hg file %s %s\n" % (cl.name, f))
  1774  			cl.files = Sub(cl.files, oldfiles)
  1775  			cl.Flush(ui, repo)
  1776  		else:
  1777  			ui.status("no such files in CL")
  1778  		return
  1779  
  1780  	if not files:
  1781  		return "no such modified files"
  1782  
  1783  	files = Sub(files, cl.files)
  1784  	taken = Taken(ui, repo)
  1785  	warned = False
  1786  	for f in files:
  1787  		if f in taken:
  1788  			if not warned and not ui.quiet:
  1789  				ui.status("# Taking files from other CLs.  To undo:\n")
  1790  				ui.status("#	cd %s\n" % (repo.root))
  1791  				warned = True
  1792  			ocl = taken[f]
  1793  			if not ui.quiet:
  1794  				ui.status("#	hg file %s %s\n" % (ocl.name, f))
  1795  			if ocl not in dirty:
  1796  				ocl.files = Sub(ocl.files, files)
  1797  				dirty[ocl] = True
  1798  	cl.files = Add(cl.files, files)
  1799  	dirty[cl] = True
  1800  	for d, _ in dirty.items():
  1801  		d.Flush(ui, repo)
  1802  	return
  1803  
  1804  #######################################################################
  1805  # hg gofmt
  1806  
  1807  @hgcommand
  1808  def gofmt(ui, repo, *pats, **opts):
  1809  	"""apply gofmt to modified files
  1810  
  1811  	Applies gofmt to the modified files in the repository that match
  1812  	the given patterns.
  1813  	"""
  1814  	if codereview_disabled:
  1815  		raise hg_util.Abort(codereview_disabled)
  1816  
  1817  	files = ChangedExistingFiles(ui, repo, pats, opts)
  1818  	files = gofmt_required(files)
  1819  	if not files:
  1820  		ui.status("no modified go files\n")
  1821  		return
  1822  	cwd = os.getcwd()
  1823  	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
  1824  	try:
  1825  		cmd = ["gofmt", "-l"]
  1826  		if not opts["list"]:
  1827  			cmd += ["-w"]
  1828  		if subprocess.call(cmd + files) != 0:
  1829  			raise hg_util.Abort("gofmt did not exit cleanly")
  1830  	except hg_error.Abort, e:
  1831  		raise
  1832  	except:
  1833  		raise hg_util.Abort("gofmt: " + ExceptionDetail())
  1834  	return
  1835  
  1836  def gofmt_required(files):
  1837  	return [f for f in files if (not f.startswith('test/') or f.startswith('test/bench/')) and f.endswith('.go')]
  1838  
  1839  #######################################################################
  1840  # hg mail
  1841  
  1842  @hgcommand
  1843  def mail(ui, repo, *pats, **opts):
  1844  	"""mail a change for review
  1845  
  1846  	Uploads a patch to the code review server and then sends mail
  1847  	to the reviewer and CC list asking for a review.
  1848  	"""
  1849  	if codereview_disabled:
  1850  		raise hg_util.Abort(codereview_disabled)
  1851  
  1852  	cl, err = CommandLineCL(ui, repo, pats, opts, op="mail", defaultcc=defaultcc)
  1853  	if err != "":
  1854  		raise hg_util.Abort(err)
  1855  	cl.Upload(ui, repo, gofmt_just_warn=True)
  1856  	if not cl.reviewer:
  1857  		# If no reviewer is listed, assign the review to defaultcc.
  1858  		# This makes sure that it appears in the 
  1859  		# codereview.appspot.com/user/defaultcc
  1860  		# page, so that it doesn't get dropped on the floor.
  1861  		if not defaultcc or cl.private:
  1862  			raise hg_util.Abort("no reviewers listed in CL")
  1863  		cl.cc = Sub(cl.cc, defaultcc)
  1864  		cl.reviewer = defaultcc
  1865  		cl.Flush(ui, repo)
  1866  
  1867  	if cl.files == []:
  1868  			raise hg_util.Abort("no changed files, not sending mail")
  1869  
  1870  	cl.Mail(ui, repo)
  1871  
  1872  #######################################################################
  1873  # hg p / hg pq / hg ps / hg pending
  1874  
  1875  @hgcommand
  1876  def ps(ui, repo, *pats, **opts):
  1877  	"""alias for hg p --short
  1878  	"""
  1879  	opts['short'] = True
  1880  	return pending(ui, repo, *pats, **opts)
  1881  
  1882  @hgcommand
  1883  def pq(ui, repo, *pats, **opts):
  1884  	"""alias for hg p --quick
  1885  	"""
  1886  	opts['quick'] = True
  1887  	return pending(ui, repo, *pats, **opts)
  1888  
  1889  @hgcommand
  1890  def pending(ui, repo, *pats, **opts):
  1891  	"""show pending changes
  1892  
  1893  	Lists pending changes followed by a list of unassigned but modified files.
  1894  	"""
  1895  	if codereview_disabled:
  1896  		raise hg_util.Abort(codereview_disabled)
  1897  
  1898  	quick = opts.get('quick', False)
  1899  	short = opts.get('short', False)
  1900  	m = LoadAllCL(ui, repo, web=not quick and not short)
  1901  	names = m.keys()
  1902  	names.sort()
  1903  	for name in names:
  1904  		cl = m[name]
  1905  		if short:
  1906  			ui.write(name + "\t" + line1(cl.desc) + "\n")
  1907  		else:
  1908  			ui.write(cl.PendingText(quick=quick) + "\n")
  1909  
  1910  	if short:
  1911  		return 0
  1912  	files = DefaultFiles(ui, repo, [])
  1913  	if len(files) > 0:
  1914  		s = "Changed files not in any CL:\n"
  1915  		for f in files:
  1916  			s += "\t" + f + "\n"
  1917  		ui.write(s)
  1918  
  1919  #######################################################################
  1920  # hg submit
  1921  
  1922  def need_sync():
  1923  	raise hg_util.Abort("local repository out of date; must sync before submit")
  1924  
  1925  def branch_prefix(ui, repo):
  1926  	prefix = ""
  1927  	branch = repo[None].branch()
  1928  	if workbranch(branch) and branch != "default":
  1929  		prefix = "[" + branch + "] "
  1930  	return prefix
  1931  
  1932  @hgcommand
  1933  def submit(ui, repo, *pats, **opts):
  1934  	"""submit change to remote repository
  1935  
  1936  	Submits change to remote repository.
  1937  	Bails out if the local repository is not in sync with the remote one.
  1938  	"""
  1939  	if codereview_disabled:
  1940  		raise hg_util.Abort(codereview_disabled)
  1941  
  1942  	# We already called this on startup but sometimes Mercurial forgets.
  1943  	set_mercurial_encoding_to_utf8()
  1944  
  1945  	if not opts["no_incoming"] and hg_incoming(ui, repo):
  1946  		need_sync()
  1947  
  1948  	cl, err = CommandLineCL(ui, repo, pats, opts, op="submit", defaultcc=defaultcc)
  1949  	if err != "":
  1950  		raise hg_util.Abort(err)
  1951  
  1952  	user = None
  1953  	if cl.copied_from:
  1954  		user = cl.copied_from
  1955  	userline = CheckContributor(ui, repo, user)
  1956  	typecheck(userline, str)
  1957  
  1958  	about = ""
  1959  
  1960  	if not cl.lgtm and not opts.get('tbr') and needLGTM(cl):
  1961  		raise hg_util.Abort("this CL has not been LGTM'ed")
  1962  	if cl.lgtm:
  1963  		about += "LGTM=" + JoinComma([CutDomain(who) for (who, line, approval) in cl.lgtm if approval]) + "\n"
  1964  	reviewer = cl.reviewer
  1965  	if opts.get('tbr'):
  1966  		tbr = SplitCommaSpace(opts.get('tbr'))
  1967  		for name in tbr:
  1968  			if name.startswith('golang-'):
  1969  				raise hg_util.Abort("--tbr requires a person, not a mailing list")
  1970  		cl.reviewer = Add(cl.reviewer, tbr)
  1971  		about += "TBR=" + JoinComma([CutDomain(s) for s in tbr]) + "\n"
  1972  	if reviewer:
  1973  		about += "R=" + JoinComma([CutDomain(s) for s in reviewer]) + "\n"
  1974  	if cl.cc:
  1975  		about += "CC=" + JoinComma([CutDomain(s) for s in cl.cc]) + "\n"
  1976  
  1977  	if not cl.reviewer and needLGTM(cl):
  1978  		raise hg_util.Abort("no reviewers listed in CL")
  1979  
  1980  	if not cl.local:
  1981  		raise hg_util.Abort("cannot submit non-local CL")
  1982  
  1983  	# upload, to sync current patch and also get change number if CL is new.
  1984  	if not cl.copied_from:
  1985  		cl.Upload(ui, repo, gofmt_just_warn=True)
  1986  
  1987  	# check gofmt for real; allowed upload to warn in order to save CL.
  1988  	cl.Flush(ui, repo)
  1989  	CheckFormat(ui, repo, cl.files)
  1990  
  1991  	about += "%s%s\n" % (server_url_base, cl.name)
  1992  
  1993  	if cl.copied_from:
  1994  		about += "\nCommitter: " + CheckContributor(ui, repo, None) + "\n"
  1995  	typecheck(about, str)
  1996  
  1997  	if not cl.mailed and not cl.copied_from:		# in case this is TBR
  1998  		cl.Mail(ui, repo)
  1999  
  2000  	# submit changes locally
  2001  	message = branch_prefix(ui, repo) + cl.desc.rstrip() + "\n\n" + about
  2002  	typecheck(message, str)
  2003  
  2004  	set_status("pushing " + cl.name + " to remote server")
  2005  
  2006  	if hg_outgoing(ui, repo):
  2007  		raise hg_util.Abort("local repository corrupt or out-of-phase with remote: found outgoing changes")
  2008  	
  2009  	old_heads = len(hg_heads(ui, repo).split())
  2010  
  2011  	# Normally we commit listing the specific files in the CL.
  2012  	# If there are no changed files other than those in the CL, however,
  2013  	# let hg build the list, because then committing a merge works.
  2014  	# (You cannot name files for a merge commit, even if you name
  2015  	# all the files that would be committed by not naming any.)
  2016  	files = ['path:'+f for f in cl.files]
  2017  	if ChangedFiles(ui, repo, []) == cl.files:
  2018  		files = []
  2019  
  2020  	global commit_okay
  2021  	commit_okay = True
  2022  	ret = hg_commit(ui, repo, *files, message=message, user=userline)
  2023  	commit_okay = False
  2024  	if ret:
  2025  		raise hg_util.Abort("nothing changed")
  2026  
  2027  	node = repo["-1"].node()
  2028  	# push to remote; if it fails for any reason, roll back
  2029  	try:
  2030  		new_heads = len(hg_heads(ui, repo).split())
  2031  		if old_heads != new_heads and not (old_heads == 0 and new_heads == 1):
  2032  			# Created new head, so we weren't up to date.
  2033  			need_sync()
  2034  
  2035  		# Push changes to remote.  If it works, we're committed.  If not, roll back.
  2036  		try:
  2037  			if hg_push(ui, repo):
  2038  				raise hg_util.Abort("push error")
  2039  		except hg_error.Abort, e:
  2040  			if e.message.find("push creates new heads") >= 0:
  2041  				# Remote repository had changes we missed.
  2042  				need_sync()
  2043  			raise
  2044  		except urllib2.HTTPError, e:
  2045  			print >>sys.stderr, "pushing to remote server failed; do you have commit permissions?"
  2046  			raise
  2047  	except:
  2048  		real_rollback()
  2049  		raise
  2050  
  2051  	# We're committed. Upload final patch, close review, add commit message.
  2052  	changeURL = hg_node.short(node)
  2053  	url = ui.expandpath("default")
  2054  	m = re.match("(^https?://([^@/]+@)?([^.]+)\.googlecode\.com/hg/?)" + "|" +
  2055  		"(^https?://([^@/]+@)?code\.google\.com/p/([^/.]+)(\.[^./]+)?/?)", url)
  2056  	if m:
  2057  		if m.group(1): # prj.googlecode.com/hg/ case
  2058  			changeURL = "https://code.google.com/p/%s/source/detail?r=%s" % (m.group(3), changeURL)
  2059  		elif m.group(4) and m.group(7): # code.google.com/p/prj.subrepo/ case
  2060  			changeURL = "https://code.google.com/p/%s/source/detail?r=%s&repo=%s" % (m.group(6), changeURL, m.group(7)[1:])
  2061  		elif m.group(4): # code.google.com/p/prj/ case
  2062  			changeURL = "https://code.google.com/p/%s/source/detail?r=%s" % (m.group(6), changeURL)
  2063  		else:
  2064  			print >>sys.stderr, "URL: ", url
  2065  	else:
  2066  		print >>sys.stderr, "URL: ", url
  2067  	pmsg = "*** Submitted as " + changeURL + " ***\n\n" + message
  2068  
  2069  	# When posting, move reviewers to CC line,
  2070  	# so that the issue stops showing up in their "My Issues" page.
  2071  	PostMessage(ui, cl.name, pmsg, reviewers="", cc=JoinComma(cl.reviewer+cl.cc))
  2072  
  2073  	if not cl.copied_from:
  2074  		EditDesc(cl.name, closed=True, private=cl.private)
  2075  	cl.Delete(ui, repo)
  2076  
  2077  	c = repo[None]
  2078  	if c.branch() == releaseBranch and not c.modified() and not c.added() and not c.removed():
  2079  		ui.write("switching from %s to default branch.\n" % releaseBranch)
  2080  		err = hg_clean(repo, "default")
  2081  		if err:
  2082  			return err
  2083  	return 0
  2084  
  2085  def needLGTM(cl):
  2086  	rev = cl.reviewer
  2087  	isGobot = 'gobot' in rev or 'gobot@swtch.com' in rev or 'gobot@golang.org' in rev
  2088  	
  2089  	# A+C CLs generated by addca do not need LGTM
  2090  	if cl.desc.startswith('A+C:') and 'Generated by a+c.' in cl.desc and isGobot:
  2091  		return False
  2092  	
  2093  	# CLs modifying only go1.x.txt do not need LGTM
  2094  	if len(cl.files) == 1 and cl.files[0].startswith('doc/go1.') and cl.files[0].endswith('.txt'):
  2095  		return False
  2096  	
  2097  	# Other CLs need LGTM
  2098  	return True
  2099  
  2100  #######################################################################
  2101  # hg sync
  2102  
  2103  @hgcommand
  2104  def sync(ui, repo, **opts):
  2105  	"""synchronize with remote repository
  2106  
  2107  	Incorporates recent changes from the remote repository
  2108  	into the local repository.
  2109  	"""
  2110  	if codereview_disabled:
  2111  		raise hg_util.Abort(codereview_disabled)
  2112  
  2113  	if not opts["local"]:
  2114  		# If there are incoming CLs, pull -u will do the update.
  2115  		# If there are no incoming CLs, do hg update to make sure
  2116  		# that an update always happens regardless. This is less
  2117  		# surprising than update depending on incoming CLs.
  2118  		# It is important not to do both hg pull -u and hg update
  2119  		# in the same command, because the hg update will end
  2120  		# up marking resolve conflicts from the hg pull -u as resolved,
  2121  		# causing files with <<< >>> markers to not show up in 
  2122  		# hg resolve -l. Yay Mercurial.
  2123  		if hg_incoming(ui, repo):
  2124  			err = hg_pull(ui, repo, update=True)
  2125  		else:
  2126  			err = hg_update(ui, repo)
  2127  		if err:
  2128  			return err
  2129  	sync_changes(ui, repo)
  2130  
  2131  def sync_changes(ui, repo):
  2132  	# Look through recent change log descriptions to find
  2133  	# potential references to http://.*/our-CL-number.
  2134  	# Double-check them by looking at the Rietveld log.
  2135  	for rev in hg_log(ui, repo, limit=100, template="{node}\n").split():
  2136  		desc = repo[rev].description().strip()
  2137  		for clname in re.findall('(?m)^https?://(?:[^\n]+)/([0-9]+)$', desc):
  2138  			if IsLocalCL(ui, repo, clname) and IsRietveldSubmitted(ui, clname, repo[rev].hex()):
  2139  				ui.warn("CL %s submitted as %s; closing\n" % (clname, repo[rev]))
  2140  				cl, err = LoadCL(ui, repo, clname, web=False)
  2141  				if err != "":
  2142  					ui.warn("loading CL %s: %s\n" % (clname, err))
  2143  					continue
  2144  				if not cl.copied_from:
  2145  					EditDesc(cl.name, closed=True, private=cl.private)
  2146  				cl.Delete(ui, repo)
  2147  
  2148  	# Remove files that are not modified from the CLs in which they appear.
  2149  	all = LoadAllCL(ui, repo, web=False)
  2150  	changed = ChangedFiles(ui, repo, [])
  2151  	for cl in all.values():
  2152  		extra = Sub(cl.files, changed)
  2153  		if extra:
  2154  			ui.warn("Removing unmodified files from CL %s:\n" % (cl.name,))
  2155  			for f in extra:
  2156  				ui.warn("\t%s\n" % (f,))
  2157  			cl.files = Sub(cl.files, extra)
  2158  			cl.Flush(ui, repo)
  2159  		if not cl.files:
  2160  			if not cl.copied_from:
  2161  				ui.warn("CL %s has no files; delete (abandon) with hg change -d %s\n" % (cl.name, cl.name))
  2162  			else:
  2163  				ui.warn("CL %s has no files; delete locally with hg change -D %s\n" % (cl.name, cl.name))
  2164  	return 0
  2165  
  2166  #######################################################################
  2167  # hg upload
  2168  
  2169  @hgcommand
  2170  def upload(ui, repo, name, **opts):
  2171  	"""upload diffs to the code review server
  2172  
  2173  	Uploads the current modifications for a given change to the server.
  2174  	"""
  2175  	if codereview_disabled:
  2176  		raise hg_util.Abort(codereview_disabled)
  2177  
  2178  	repo.ui.quiet = True
  2179  	cl, err = LoadCL(ui, repo, name, web=True)
  2180  	if err != "":
  2181  		raise hg_util.Abort(err)
  2182  	if not cl.local:
  2183  		raise hg_util.Abort("cannot upload non-local change")
  2184  	cl.Upload(ui, repo)
  2185  	print "%s%s\n" % (server_url_base, cl.name)
  2186  	return 0
  2187  
  2188  #######################################################################
  2189  # Table of commands, supplied to Mercurial for installation.
  2190  
  2191  review_opts = [
  2192  	('r', 'reviewer', '', 'add reviewer'),
  2193  	('', 'cc', '', 'add cc'),
  2194  	('', 'tbr', '', 'add future reviewer'),
  2195  	('m', 'message', '', 'change description (for new change)'),
  2196  ]
  2197  
  2198  cmdtable = {
  2199  	# The ^ means to show this command in the help text that
  2200  	# is printed when running hg with no arguments.
  2201  	"^change": (
  2202  		change,
  2203  		[
  2204  			('d', 'delete', None, 'delete existing change list'),
  2205  			('D', 'deletelocal', None, 'delete locally, but do not change CL on server'),
  2206  			('i', 'stdin', None, 'read change list from standard input'),
  2207  			('o', 'stdout', None, 'print change list to standard output'),
  2208  			('p', 'pending', None, 'print pending summary to standard output'),
  2209  		],
  2210  		"[-d | -D] [-i] [-o] change# or FILE ..."
  2211  	),
  2212  	"^clpatch": (
  2213  		clpatch,
  2214  		[
  2215  			('', 'ignore_hgapplydiff_failure', None, 'create CL metadata even if hgapplydiff fails'),
  2216  			('', 'no_incoming', None, 'disable check for incoming changes'),
  2217  		],
  2218  		"change#"
  2219  	),
  2220  	# Would prefer to call this codereview-login, but then
  2221  	# hg help codereview prints the help for this command
  2222  	# instead of the help for the extension.
  2223  	"code-login": (
  2224  		code_login,
  2225  		[],
  2226  		"",
  2227  	),
  2228  	"^download": (
  2229  		download,
  2230  		[],
  2231  		"change#"
  2232  	),
  2233  	"^file": (
  2234  		file,
  2235  		[
  2236  			('d', 'delete', None, 'delete files from change list (but not repository)'),
  2237  		],
  2238  		"[-d] change# FILE ..."
  2239  	),
  2240  	"^gofmt": (
  2241  		gofmt,
  2242  		[
  2243  			('l', 'list', None, 'list files that would change, but do not edit them'),
  2244  		],
  2245  		"FILE ..."
  2246  	),
  2247  	"^pending|p": (
  2248  		pending,
  2249  		[
  2250  			('s', 'short', False, 'show short result form'),
  2251  			('', 'quick', False, 'do not consult codereview server'),
  2252  		],
  2253  		"[FILE ...]"
  2254  	),
  2255  	"^ps": (
  2256  		ps,
  2257  		[],
  2258  		"[FILE ...]"
  2259  	),
  2260  	"^pq": (
  2261  		pq,
  2262  		[],
  2263  		"[FILE ...]"
  2264  	),
  2265  	"^mail": (
  2266  		mail,
  2267  		review_opts + [
  2268  		] + hg_commands.walkopts,
  2269  		"[-r reviewer] [--cc cc] [change# | file ...]"
  2270  	),
  2271  	"^release-apply": (
  2272  		release_apply,
  2273  		[
  2274  			('', 'ignore_hgapplydiff_failure', None, 'create CL metadata even if hgapplydiff fails'),
  2275  			('', 'no_incoming', None, 'disable check for incoming changes'),
  2276  		],
  2277  		"change#"
  2278  	),
  2279  	# TODO: release-start, release-tag, weekly-tag
  2280  	"^submit": (
  2281  		submit,
  2282  		review_opts + [
  2283  			('', 'no_incoming', None, 'disable initial incoming check (for testing)'),
  2284  		] + hg_commands.walkopts + hg_commands.commitopts + hg_commands.commitopts2,
  2285  		"[-r reviewer] [--cc cc] [change# | file ...]"
  2286  	),
  2287  	"^sync": (
  2288  		sync,
  2289  		[
  2290  			('', 'local', None, 'do not pull changes from remote repository')
  2291  		],
  2292  		"[--local]",
  2293  	),
  2294  	"^undo": (
  2295  		undo,
  2296  		[
  2297  			('', 'ignore_hgapplydiff_failure', None, 'create CL metadata even if hgapplydiff fails'),
  2298  			('', 'no_incoming', None, 'disable check for incoming changes'),
  2299  		],
  2300  		"change#"
  2301  	),
  2302  	"^upload": (
  2303  		upload,
  2304  		[],
  2305  		"change#"
  2306  	),
  2307  }
  2308  
  2309  #######################################################################
  2310  # Mercurial extension initialization
  2311  
  2312  def norollback(*pats, **opts):
  2313  	"""(disabled when using this extension)"""
  2314  	raise hg_util.Abort("codereview extension enabled; use undo instead of rollback")
  2315  
  2316  codereview_init = False
  2317  
  2318  def uisetup(ui):
  2319  	global testing
  2320  	testing = ui.config("codereview", "testing")
  2321  	# Disable the Mercurial commands that might change the repository.
  2322  	# Only commands in this extension are supposed to do that.
  2323  	ui.setconfig("hooks", "pre-commit.codereview", precommithook) # runs before 'hg commit'
  2324  	ui.setconfig("hooks", "precommit.codereview", precommithook) # catches all cases
  2325  
  2326  def reposetup(ui, repo):
  2327  	global codereview_disabled
  2328  	global defaultcc
  2329  	
  2330  	# reposetup gets called both for the local repository
  2331  	# and also for any repository we are pulling or pushing to.
  2332  	# Only initialize the first time.
  2333  	global codereview_init
  2334  	if codereview_init:
  2335  		return
  2336  	codereview_init = True
  2337  	start_status_thread()
  2338  
  2339  	# Read repository-specific options from lib/codereview/codereview.cfg or codereview.cfg.
  2340  	root = ''
  2341  	try:
  2342  		root = repo.root
  2343  	except:
  2344  		# Yes, repo might not have root; see issue 959.
  2345  		codereview_disabled = 'codereview disabled: repository has no root'
  2346  		return
  2347  	
  2348  	repo_config_path = ''
  2349  	p1 = root + '/lib/codereview/codereview.cfg'
  2350  	p2 = root + '/codereview.cfg'
  2351  	if os.access(p1, os.F_OK):
  2352  		repo_config_path = p1
  2353  	else:
  2354  		repo_config_path = p2
  2355  	try:
  2356  		f = open(repo_config_path)
  2357  		for line in f:
  2358  			if line.startswith('defaultcc:'):
  2359  				defaultcc = SplitCommaSpace(line[len('defaultcc:'):])
  2360  			if line.startswith('contributors:'):
  2361  				global contributorsURL
  2362  				contributorsURL = line[len('contributors:'):].strip()
  2363  	except:
  2364  		codereview_disabled = 'codereview disabled: cannot open ' + repo_config_path
  2365  		return
  2366  
  2367  	remote = ui.config("paths", "default", "")
  2368  	if remote.find("://") < 0 and not testing:
  2369  		raise hg_util.Abort("codereview: default path '%s' is not a URL" % (remote,))
  2370  
  2371  	InstallMatch(ui, repo)
  2372  	RietveldSetup(ui, repo)
  2373  
  2374  	# Rollback removes an existing commit.  Don't do that either.
  2375  	global real_rollback
  2376  	real_rollback = repo.rollback
  2377  	repo.rollback = norollback
  2378  	
  2379  
  2380  #######################################################################
  2381  # Wrappers around upload.py for interacting with Rietveld
  2382  
  2383  from HTMLParser import HTMLParser
  2384  
  2385  # HTML form parser
  2386  class FormParser(HTMLParser):
  2387  	def __init__(self):
  2388  		self.map = {}
  2389  		self.curtag = None
  2390  		self.curdata = None
  2391  		HTMLParser.__init__(self)
  2392  	def handle_starttag(self, tag, attrs):
  2393  		if tag == "input":
  2394  			key = None
  2395  			value = ''
  2396  			for a in attrs:
  2397  				if a[0] == 'name':
  2398  					key = a[1]
  2399  				if a[0] == 'value':
  2400  					value = a[1]
  2401  			if key is not None:
  2402  				self.map[key] = value
  2403  		if tag == "textarea":
  2404  			key = None
  2405  			for a in attrs:
  2406  				if a[0] == 'name':
  2407  					key = a[1]
  2408  			if key is not None:
  2409  				self.curtag = key
  2410  				self.curdata = ''
  2411  	def handle_endtag(self, tag):
  2412  		if tag == "textarea" and self.curtag is not None:
  2413  			self.map[self.curtag] = self.curdata
  2414  			self.curtag = None
  2415  			self.curdata = None
  2416  	def handle_charref(self, name):
  2417  		self.handle_data(unichr(int(name)))
  2418  	def handle_entityref(self, name):
  2419  		import htmlentitydefs
  2420  		if name in htmlentitydefs.entitydefs:
  2421  			self.handle_data(htmlentitydefs.entitydefs[name])
  2422  		else:
  2423  			self.handle_data("&" + name + ";")
  2424  	def handle_data(self, data):
  2425  		if self.curdata is not None:
  2426  			self.curdata += data
  2427  
  2428  def JSONGet(ui, path):
  2429  	try:
  2430  		data = MySend(path, force_auth=False)
  2431  		typecheck(data, str)
  2432  		d = fix_json(json.loads(data))
  2433  	except:
  2434  		ui.warn("JSONGet %s: %s\n" % (path, ExceptionDetail()))
  2435  		return None
  2436  	return d
  2437  
  2438  # Clean up json parser output to match our expectations:
  2439  #   * all strings are UTF-8-encoded str, not unicode.
  2440  #   * missing fields are missing, not None,
  2441  #     so that d.get("foo", defaultvalue) works.
  2442  def fix_json(x):
  2443  	if type(x) in [str, int, float, bool, type(None)]:
  2444  		pass
  2445  	elif type(x) is unicode:
  2446  		x = x.encode("utf-8")
  2447  	elif type(x) is list:
  2448  		for i in range(len(x)):
  2449  			x[i] = fix_json(x[i])
  2450  	elif type(x) is dict:
  2451  		todel = []
  2452  		for k in x:
  2453  			if x[k] is None:
  2454  				todel.append(k)
  2455  			else:
  2456  				x[k] = fix_json(x[k])
  2457  		for k in todel:
  2458  			del x[k]
  2459  	else:
  2460  		raise hg_util.Abort("unknown type " + str(type(x)) + " in fix_json")
  2461  	if type(x) is str:
  2462  		x = x.replace('\r\n', '\n')
  2463  	return x
  2464  
  2465  def IsRietveldSubmitted(ui, clname, hex):
  2466  	dict = JSONGet(ui, "/api/" + clname + "?messages=true")
  2467  	if dict is None:
  2468  		return False
  2469  	for msg in dict.get("messages", []):
  2470  		text = msg.get("text", "")
  2471  		regex = '\*\*\* Submitted as [^*]*?r=([0-9a-f]+)[^ ]* \*\*\*'
  2472  		if testing:
  2473  			regex = '\*\*\* Submitted as ([0-9a-f]+) \*\*\*'
  2474  		m = re.match(regex, text)
  2475  		if m is not None and len(m.group(1)) >= 8 and hex.startswith(m.group(1)):
  2476  			return True
  2477  	return False
  2478  
  2479  def IsRietveldMailed(cl):
  2480  	for msg in cl.dict.get("messages", []):
  2481  		if msg.get("text", "").find("I'd like you to review this change") >= 0:
  2482  			return True
  2483  	return False
  2484  
  2485  def DownloadCL(ui, repo, clname):
  2486  	set_status("downloading CL " + clname)
  2487  	cl, err = LoadCL(ui, repo, clname, web=True)
  2488  	if err != "":
  2489  		return None, None, None, "error loading CL %s: %s" % (clname, err)
  2490  
  2491  	# Find most recent diff
  2492  	diffs = cl.dict.get("patchsets", [])
  2493  	if not diffs:
  2494  		return None, None, None, "CL has no patch sets"
  2495  	patchid = diffs[-1]
  2496  
  2497  	patchset = JSONGet(ui, "/api/" + clname + "/" + str(patchid))
  2498  	if patchset is None:
  2499  		return None, None, None, "error loading CL patchset %s/%d" % (clname, patchid)
  2500  	if patchset.get("patchset", 0) != patchid:
  2501  		return None, None, None, "malformed patchset information"
  2502  	
  2503  	vers = ""
  2504  	msg = patchset.get("message", "").split()
  2505  	if len(msg) >= 3 and msg[0] == "diff" and msg[1] == "-r":
  2506  		vers = msg[2]
  2507  	diff = "/download/issue" + clname + "_" + str(patchid) + ".diff"
  2508  
  2509  	diffdata = MySend(diff, force_auth=False)
  2510  	
  2511  	# Print warning if email is not in CONTRIBUTORS file.
  2512  	email = cl.dict.get("owner_email", "")
  2513  	if not email:
  2514  		return None, None, None, "cannot find owner for %s" % (clname)
  2515  	him = FindContributor(ui, repo, email)
  2516  	me = FindContributor(ui, repo, None)
  2517  	if him == me:
  2518  		cl.mailed = IsRietveldMailed(cl)
  2519  	else:
  2520  		cl.copied_from = email
  2521  
  2522  	return cl, vers, diffdata, ""
  2523  
  2524  def MySend(request_path, payload=None,
  2525  		content_type="application/octet-stream",
  2526  		timeout=None, force_auth=True,
  2527  		**kwargs):
  2528  	"""Run MySend1 maybe twice, because Rietveld is unreliable."""
  2529  	try:
  2530  		return MySend1(request_path, payload, content_type, timeout, force_auth, **kwargs)
  2531  	except Exception, e:
  2532  		if type(e) != urllib2.HTTPError or e.code != 500:	# only retry on HTTP 500 error
  2533  			raise
  2534  		print >>sys.stderr, "Loading "+request_path+": "+ExceptionDetail()+"; trying again in 2 seconds."
  2535  		time.sleep(2)
  2536  		return MySend1(request_path, payload, content_type, timeout, force_auth, **kwargs)
  2537  
  2538  # Like upload.py Send but only authenticates when the
  2539  # redirect is to www.google.com/accounts.  This keeps
  2540  # unnecessary redirects from happening during testing.
  2541  def MySend1(request_path, payload=None,
  2542  				content_type="application/octet-stream",
  2543  				timeout=None, force_auth=True,
  2544  				**kwargs):
  2545  	"""Sends an RPC and returns the response.
  2546  
  2547  	Args:
  2548  		request_path: The path to send the request to, eg /api/appversion/create.
  2549  		payload: The body of the request, or None to send an empty request.
  2550  		content_type: The Content-Type header to use.
  2551  		timeout: timeout in seconds; default None i.e. no timeout.
  2552  			(Note: for large requests on OS X, the timeout doesn't work right.)
  2553  		kwargs: Any keyword arguments are converted into query string parameters.
  2554  
  2555  	Returns:
  2556  		The response body, as a string.
  2557  	"""
  2558  	# TODO: Don't require authentication.  Let the server say
  2559  	# whether it is necessary.
  2560  	global rpc
  2561  	if rpc == None:
  2562  		rpc = GetRpcServer(upload_options)
  2563  	self = rpc
  2564  	if not self.authenticated and force_auth:
  2565  		self._Authenticate()
  2566  	if request_path is None:
  2567  		return
  2568  	if timeout is None:
  2569  		timeout = 30 # seconds
  2570  
  2571  	old_timeout = socket.getdefaulttimeout()
  2572  	socket.setdefaulttimeout(timeout)
  2573  	try:
  2574  		tries = 0
  2575  		while True:
  2576  			tries += 1
  2577  			args = dict(kwargs)
  2578  			url = "https://%s%s" % (self.host, request_path)
  2579  			if testing:
  2580  				url = url.replace("https://", "http://")
  2581  			if args:
  2582  				url += "?" + urllib.urlencode(args)
  2583  			req = self._CreateRequest(url=url, data=payload)
  2584  			req.add_header("Content-Type", content_type)
  2585  			try:
  2586  				f = self.opener.open(req)
  2587  				response = f.read()
  2588  				f.close()
  2589  				# Translate \r\n into \n, because Rietveld doesn't.
  2590  				response = response.replace('\r\n', '\n')
  2591  				# who knows what urllib will give us
  2592  				if type(response) == unicode:
  2593  					response = response.encode("utf-8")
  2594  				typecheck(response, str)
  2595  				return response
  2596  			except urllib2.HTTPError, e:
  2597  				if tries > 3:
  2598  					raise
  2599  				elif e.code == 401:
  2600  					self._Authenticate()
  2601  				elif e.code == 302:
  2602  					loc = e.info()["location"]
  2603  					if not loc.startswith('https://www.google.com/a') or loc.find('/ServiceLogin') < 0:
  2604  						return ''
  2605  					self._Authenticate()
  2606  				else:
  2607  					raise
  2608  	finally:
  2609  		socket.setdefaulttimeout(old_timeout)
  2610  
  2611  def GetForm(url):
  2612  	f = FormParser()
  2613  	f.feed(ustr(MySend(url)))	# f.feed wants unicode
  2614  	f.close()
  2615  	# convert back to utf-8 to restore sanity
  2616  	m = {}
  2617  	for k,v in f.map.items():
  2618  		m[k.encode("utf-8")] = v.replace("\r\n", "\n").encode("utf-8")
  2619  	return m
  2620  
  2621  def EditDesc(issue, subject=None, desc=None, reviewers=None, cc=None, closed=False, private=False):
  2622  	set_status("uploading change to description")
  2623  	form_fields = GetForm("/" + issue + "/edit")
  2624  	if subject is not None:
  2625  		form_fields['subject'] = subject
  2626  	if desc is not None:
  2627  		form_fields['description'] = desc
  2628  	if reviewers is not None:
  2629  		form_fields['reviewers'] = reviewers
  2630  	if cc is not None:
  2631  		form_fields['cc'] = cc
  2632  	if closed:
  2633  		form_fields['closed'] = "checked"
  2634  	if private:
  2635  		form_fields['private'] = "checked"
  2636  	ctype, body = EncodeMultipartFormData(form_fields.items(), [])
  2637  	response = MySend("/" + issue + "/edit", body, content_type=ctype)
  2638  	if response != "":
  2639  		print >>sys.stderr, "Error editing description:\n" + "Sent form: \n", form_fields, "\n", response
  2640  		sys.exit(2)
  2641  
  2642  def PostMessage(ui, issue, message, reviewers=None, cc=None, send_mail=True, subject=None):
  2643  	set_status("uploading message")
  2644  	form_fields = GetForm("/" + issue + "/publish")
  2645  	if reviewers is not None:
  2646  		form_fields['reviewers'] = reviewers
  2647  	if cc is not None:
  2648  		form_fields['cc'] = cc
  2649  	if send_mail:
  2650  		form_fields['send_mail'] = "checked"
  2651  	else:
  2652  		del form_fields['send_mail']
  2653  	if subject is not None:
  2654  		form_fields['subject'] = subject
  2655  	form_fields['message'] = message
  2656  	
  2657  	form_fields['message_only'] = '1'	# Don't include draft comments
  2658  	if reviewers is not None or cc is not None:
  2659  		form_fields['message_only'] = ''	# Must set '' in order to override cc/reviewer
  2660  	ctype = "applications/x-www-form-urlencoded"
  2661  	body = urllib.urlencode(form_fields)
  2662  	response = MySend("/" + issue + "/publish", body, content_type=ctype)
  2663  	if response != "":
  2664  		print response
  2665  		sys.exit(2)
  2666  
  2667  class opt(object):
  2668  	pass
  2669  
  2670  def RietveldSetup(ui, repo):
  2671  	global force_google_account
  2672  	global rpc
  2673  	global server
  2674  	global server_url_base
  2675  	global upload_options
  2676  	global verbosity
  2677  
  2678  	if not ui.verbose:
  2679  		verbosity = 0
  2680  
  2681  	# Config options.
  2682  	x = ui.config("codereview", "server")
  2683  	if x is not None:
  2684  		server = x
  2685  
  2686  	# TODO(rsc): Take from ui.username?
  2687  	email = None
  2688  	x = ui.config("codereview", "email")
  2689  	if x is not None:
  2690  		email = x
  2691  
  2692  	server_url_base = "https://" + server + "/"
  2693  	if testing:
  2694  		server_url_base = server_url_base.replace("https://", "http://")
  2695  
  2696  	force_google_account = ui.configbool("codereview", "force_google_account", False)
  2697  
  2698  	upload_options = opt()
  2699  	upload_options.email = email
  2700  	upload_options.host = None
  2701  	upload_options.verbose = 0
  2702  	upload_options.description = None
  2703  	upload_options.description_file = None
  2704  	upload_options.reviewers = None
  2705  	upload_options.cc = None
  2706  	upload_options.message = None
  2707  	upload_options.issue = None
  2708  	upload_options.download_base = False
  2709  	upload_options.send_mail = False
  2710  	upload_options.vcs = None
  2711  	upload_options.server = server
  2712  	upload_options.save_cookies = True
  2713  
  2714  	if testing:
  2715  		upload_options.save_cookies = False
  2716  		upload_options.email = "test@example.com"
  2717  
  2718  	rpc = None
  2719  	
  2720  	global releaseBranch
  2721  	tags = repo.branchmap().keys()
  2722  	if 'release-branch.go10' in tags:
  2723  		# NOTE(rsc): This tags.sort is going to get the wrong
  2724  		# answer when comparing release-branch.go9 with
  2725  		# release-branch.go10.  It will be a while before we care.
  2726  		raise hg_util.Abort('tags.sort needs to be fixed for release-branch.go10')
  2727  	tags.sort()
  2728  	for t in tags:
  2729  		if t.startswith('release-branch.go'):
  2730  			releaseBranch = t			
  2731  
  2732  def workbranch(name):
  2733  	return name == "default" or name.startswith('dev.') or name == 'release-branch.go1.4'
  2734  
  2735  #######################################################################
  2736  # http://codereview.appspot.com/static/upload.py, heavily edited.
  2737  
  2738  #!/usr/bin/env python
  2739  #
  2740  # Copyright 2007 Google Inc.
  2741  #
  2742  # Licensed under the Apache License, Version 2.0 (the "License");
  2743  # you may not use this file except in compliance with the License.
  2744  # You may obtain a copy of the License at
  2745  #
  2746  #	http://www.apache.org/licenses/LICENSE-2.0
  2747  #
  2748  # Unless required by applicable law or agreed to in writing, software
  2749  # distributed under the License is distributed on an "AS IS" BASIS,
  2750  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  2751  # See the License for the specific language governing permissions and
  2752  # limitations under the License.
  2753  
  2754  """Tool for uploading diffs from a version control system to the codereview app.
  2755  
  2756  Usage summary: upload.py [options] [-- diff_options]
  2757  
  2758  Diff options are passed to the diff command of the underlying system.
  2759  
  2760  Supported version control systems:
  2761  	Git
  2762  	Mercurial
  2763  	Subversion
  2764  
  2765  It is important for Git/Mercurial users to specify a tree/node/branch to diff
  2766  against by using the '--rev' option.
  2767  """
  2768  # This code is derived from appcfg.py in the App Engine SDK (open source),
  2769  # and from ASPN recipe #146306.
  2770  
  2771  import cookielib
  2772  import getpass
  2773  import logging
  2774  import mimetypes
  2775  import optparse
  2776  import os
  2777  import re
  2778  import socket
  2779  import subprocess
  2780  import sys
  2781  import urllib
  2782  import urllib2
  2783  import urlparse
  2784  
  2785  # The md5 module was deprecated in Python 2.5.
  2786  try:
  2787  	from hashlib import md5
  2788  except ImportError:
  2789  	from md5 import md5
  2790  
  2791  try:
  2792  	import readline
  2793  except ImportError:
  2794  	pass
  2795  
  2796  # The logging verbosity:
  2797  #  0: Errors only.
  2798  #  1: Status messages.
  2799  #  2: Info logs.
  2800  #  3: Debug logs.
  2801  verbosity = 1
  2802  
  2803  # Max size of patch or base file.
  2804  MAX_UPLOAD_SIZE = 900 * 1024
  2805  
  2806  # whitelist for non-binary filetypes which do not start with "text/"
  2807  # .mm (Objective-C) shows up as application/x-freemind on my Linux box.
  2808  TEXT_MIMETYPES = [
  2809  	'application/javascript',
  2810  	'application/x-javascript',
  2811  	'application/x-freemind'
  2812  ]
  2813  
  2814  def GetEmail(prompt):
  2815  	"""Prompts the user for their email address and returns it.
  2816  
  2817  	The last used email address is saved to a file and offered up as a suggestion
  2818  	to the user. If the user presses enter without typing in anything the last
  2819  	used email address is used. If the user enters a new address, it is saved
  2820  	for next time we prompt.
  2821  
  2822  	"""
  2823  	last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
  2824  	last_email = ""
  2825  	if os.path.exists(last_email_file_name):
  2826  		try:
  2827  			last_email_file = open(last_email_file_name, "r")
  2828  			last_email = last_email_file.readline().strip("\n")
  2829  			last_email_file.close()
  2830  			prompt += " [%s]" % last_email
  2831  		except IOError, e:
  2832  			pass
  2833  	email = raw_input(prompt + ": ").strip()
  2834  	if email:
  2835  		try:
  2836  			last_email_file = open(last_email_file_name, "w")
  2837  			last_email_file.write(email)
  2838  			last_email_file.close()
  2839  		except IOError, e:
  2840  			pass
  2841  	else:
  2842  		email = last_email
  2843  	return email
  2844  
  2845  
  2846  def StatusUpdate(msg):
  2847  	"""Print a status message to stdout.
  2848  
  2849  	If 'verbosity' is greater than 0, print the message.
  2850  
  2851  	Args:
  2852  		msg: The string to print.
  2853  	"""
  2854  	if verbosity > 0:
  2855  		print msg
  2856  
  2857  
  2858  def ErrorExit(msg):
  2859  	"""Print an error message to stderr and exit."""
  2860  	print >>sys.stderr, msg
  2861  	sys.exit(1)
  2862  
  2863  
  2864  class ClientLoginError(urllib2.HTTPError):
  2865  	"""Raised to indicate there was an error authenticating with ClientLogin."""
  2866  
  2867  	def __init__(self, url, code, msg, headers, args):
  2868  		urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
  2869  		self.args = args
  2870  		# .reason is now a read-only property based on .msg
  2871  		# this means we ignore 'msg', but that seems to work fine.
  2872  		self.msg = args["Error"] 
  2873  
  2874  
  2875  class AbstractRpcServer(object):
  2876  	"""Provides a common interface for a simple RPC server."""
  2877  
  2878  	def __init__(self, host, auth_function, host_override=None, extra_headers={}, save_cookies=False):
  2879  		"""Creates a new HttpRpcServer.
  2880  
  2881  		Args:
  2882  			host: The host to send requests to.
  2883  			auth_function: A function that takes no arguments and returns an
  2884  				(email, password) tuple when called. Will be called if authentication
  2885  				is required.
  2886  			host_override: The host header to send to the server (defaults to host).
  2887  			extra_headers: A dict of extra headers to append to every request.
  2888  			save_cookies: If True, save the authentication cookies to local disk.
  2889  				If False, use an in-memory cookiejar instead.  Subclasses must
  2890  				implement this functionality.  Defaults to False.
  2891  		"""
  2892  		self.host = host
  2893  		self.host_override = host_override
  2894  		self.auth_function = auth_function
  2895  		self.authenticated = False
  2896  		self.extra_headers = extra_headers
  2897  		self.save_cookies = save_cookies
  2898  		self.opener = self._GetOpener()
  2899  		if self.host_override:
  2900  			logging.info("Server: %s; Host: %s", self.host, self.host_override)
  2901  		else:
  2902  			logging.info("Server: %s", self.host)
  2903  
  2904  	def _GetOpener(self):
  2905  		"""Returns an OpenerDirector for making HTTP requests.
  2906  
  2907  		Returns:
  2908  			A urllib2.OpenerDirector object.
  2909  		"""
  2910  		raise NotImplementedError()
  2911  
  2912  	def _CreateRequest(self, url, data=None):
  2913  		"""Creates a new urllib request."""
  2914  		logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
  2915  		req = urllib2.Request(url, data=data)
  2916  		if self.host_override:
  2917  			req.add_header("Host", self.host_override)
  2918  		for key, value in self.extra_headers.iteritems():
  2919  			req.add_header(key, value)
  2920  		return req
  2921  
  2922  	def _GetAuthToken(self, email, password):
  2923  		"""Uses ClientLogin to authenticate the user, returning an auth token.
  2924  
  2925  		Args:
  2926  			email:    The user's email address
  2927  			password: The user's password
  2928  
  2929  		Raises:
  2930  			ClientLoginError: If there was an error authenticating with ClientLogin.
  2931  			HTTPError: If there was some other form of HTTP error.
  2932  
  2933  		Returns:
  2934  			The authentication token returned by ClientLogin.
  2935  		"""
  2936  		account_type = "GOOGLE"
  2937  		if self.host.endswith(".google.com") and not force_google_account:
  2938  			# Needed for use inside Google.
  2939  			account_type = "HOSTED"
  2940  		req = self._CreateRequest(
  2941  				url="https://www.google.com/accounts/ClientLogin",
  2942  				data=urllib.urlencode({
  2943  						"Email": email,
  2944  						"Passwd": password,
  2945  						"service": "ah",
  2946  						"source": "rietveld-codereview-upload",
  2947  						"accountType": account_type,
  2948  				}),
  2949  		)
  2950  		try:
  2951  			response = self.opener.open(req)
  2952  			response_body = response.read()
  2953  			response_dict = dict(x.split("=") for x in response_body.split("\n") if x)
  2954  			return response_dict["Auth"]
  2955  		except urllib2.HTTPError, e:
  2956  			if e.code == 403:
  2957  				body = e.read()
  2958  				response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
  2959  				raise ClientLoginError(req.get_full_url(), e.code, e.msg, e.headers, response_dict)
  2960  			else:
  2961  				raise
  2962  
  2963  	def _GetAuthCookie(self, auth_token):
  2964  		"""Fetches authentication cookies for an authentication token.
  2965  
  2966  		Args:
  2967  			auth_token: The authentication token returned by ClientLogin.
  2968  
  2969  		Raises:
  2970  			HTTPError: If there was an error fetching the authentication cookies.
  2971  		"""
  2972  		# This is a dummy value to allow us to identify when we're successful.
  2973  		continue_location = "http://localhost/"
  2974  		args = {"continue": continue_location, "auth": auth_token}
  2975  		reqUrl = "https://%s/_ah/login?%s" % (self.host, urllib.urlencode(args))
  2976  		if testing:
  2977  			reqUrl = reqUrl.replace("https://", "http://")
  2978  		req = self._CreateRequest(reqUrl)
  2979  		try:
  2980  			response = self.opener.open(req)
  2981  		except urllib2.HTTPError, e:
  2982  			response = e
  2983  		if (response.code != 302 or
  2984  				response.info()["location"] != continue_location):
  2985  			raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg, response.headers, response.fp)
  2986  		self.authenticated = True
  2987  
  2988  	def _Authenticate(self):
  2989  		"""Authenticates the user.
  2990  
  2991  		The authentication process works as follows:
  2992  		1) We get a username and password from the user
  2993  		2) We use ClientLogin to obtain an AUTH token for the user
  2994  				(see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
  2995  		3) We pass the auth token to /_ah/login on the server to obtain an
  2996  				authentication cookie. If login was successful, it tries to redirect
  2997  				us to the URL we provided.
  2998  
  2999  		If we attempt to access the upload API without first obtaining an
  3000  		authentication cookie, it returns a 401 response (or a 302) and
  3001  		directs us to authenticate ourselves with ClientLogin.
  3002  		"""
  3003  		for i in range(3):
  3004  			credentials = self.auth_function()
  3005  			try:
  3006  				auth_token = self._GetAuthToken(credentials[0], credentials[1])
  3007  			except ClientLoginError, e:
  3008  				if e.msg == "BadAuthentication":
  3009  					print >>sys.stderr, "Invalid username or password."
  3010  					continue
  3011  				if e.msg == "CaptchaRequired":
  3012  					print >>sys.stderr, (
  3013  						"Please go to\n"
  3014  						"https://www.google.com/accounts/DisplayUnlockCaptcha\n"
  3015  						"and verify you are a human.  Then try again.")
  3016  					break
  3017  				if e.msg == "NotVerified":
  3018  					print >>sys.stderr, "Account not verified."
  3019  					break
  3020  				if e.msg == "TermsNotAgreed":
  3021  					print >>sys.stderr, "User has not agreed to TOS."
  3022  					break
  3023  				if e.msg == "AccountDeleted":
  3024  					print >>sys.stderr, "The user account has been deleted."
  3025  					break
  3026  				if e.msg == "AccountDisabled":
  3027  					print >>sys.stderr, "The user account has been disabled."
  3028  					break
  3029  				if e.msg == "ServiceDisabled":
  3030  					print >>sys.stderr, "The user's access to the service has been disabled."
  3031  					break
  3032  				if e.msg == "ServiceUnavailable":
  3033  					print >>sys.stderr, "The service is not available; try again later."
  3034  					break
  3035  				raise
  3036  			self._GetAuthCookie(auth_token)
  3037  			return
  3038  
  3039  	def Send(self, request_path, payload=None,
  3040  					content_type="application/octet-stream",
  3041  					timeout=None,
  3042  					**kwargs):
  3043  		"""Sends an RPC and returns the response.
  3044  
  3045  		Args:
  3046  			request_path: The path to send the request to, eg /api/appversion/create.
  3047  			payload: The body of the request, or None to send an empty request.
  3048  			content_type: The Content-Type header to use.
  3049  			timeout: timeout in seconds; default None i.e. no timeout.
  3050  				(Note: for large requests on OS X, the timeout doesn't work right.)
  3051  			kwargs: Any keyword arguments are converted into query string parameters.
  3052  
  3053  		Returns:
  3054  			The response body, as a string.
  3055  		"""
  3056  		# TODO: Don't require authentication.  Let the server say
  3057  		# whether it is necessary.
  3058  		if not self.authenticated:
  3059  			self._Authenticate()
  3060  
  3061  		old_timeout = socket.getdefaulttimeout()
  3062  		socket.setdefaulttimeout(timeout)
  3063  		try:
  3064  			tries = 0
  3065  			while True:
  3066  				tries += 1
  3067  				args = dict(kwargs)
  3068  				url = "https://%s%s" % (self.host, request_path)
  3069  				if testing:
  3070  					url = url.replace("https://", "http://")
  3071  				if args:
  3072  					url += "?" + urllib.urlencode(args)
  3073  				req = self._CreateRequest(url=url, data=payload)
  3074  				req.add_header("Content-Type", content_type)
  3075  				try:
  3076  					f = self.opener.open(req)
  3077  					response = f.read()
  3078  					f.close()
  3079  					return response
  3080  				except urllib2.HTTPError, e:
  3081  					if tries > 3:
  3082  						raise
  3083  					elif e.code == 401 or e.code == 302:
  3084  						self._Authenticate()
  3085  					else:
  3086  						raise
  3087  		finally:
  3088  			socket.setdefaulttimeout(old_timeout)
  3089  
  3090  
  3091  class HttpRpcServer(AbstractRpcServer):
  3092  	"""Provides a simplified RPC-style interface for HTTP requests."""
  3093  
  3094  	def _Authenticate(self):
  3095  		"""Save the cookie jar after authentication."""
  3096  		super(HttpRpcServer, self)._Authenticate()
  3097  		if self.save_cookies:
  3098  			StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
  3099  			self.cookie_jar.save()
  3100  
  3101  	def _GetOpener(self):
  3102  		"""Returns an OpenerDirector that supports cookies and ignores redirects.
  3103  
  3104  		Returns:
  3105  			A urllib2.OpenerDirector object.
  3106  		"""
  3107  		opener = urllib2.OpenerDirector()
  3108  		opener.add_handler(urllib2.ProxyHandler())
  3109  		opener.add_handler(urllib2.UnknownHandler())
  3110  		opener.add_handler(urllib2.HTTPHandler())
  3111  		opener.add_handler(urllib2.HTTPDefaultErrorHandler())
  3112  		opener.add_handler(urllib2.HTTPSHandler())
  3113  		opener.add_handler(urllib2.HTTPErrorProcessor())
  3114  		if self.save_cookies:
  3115  			self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies_" + server)
  3116  			self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
  3117  			if os.path.exists(self.cookie_file):
  3118  				try:
  3119  					self.cookie_jar.load()
  3120  					self.authenticated = True
  3121  					StatusUpdate("Loaded authentication cookies from %s" % self.cookie_file)
  3122  				except (cookielib.LoadError, IOError):
  3123  					# Failed to load cookies - just ignore them.
  3124  					pass
  3125  			else:
  3126  				# Create an empty cookie file with mode 600
  3127  				fd = os.open(self.cookie_file, os.O_CREAT, 0600)
  3128  				os.close(fd)
  3129  			# Always chmod the cookie file
  3130  			os.chmod(self.cookie_file, 0600)
  3131  		else:
  3132  			# Don't save cookies across runs of update.py.
  3133  			self.cookie_jar = cookielib.CookieJar()
  3134  		opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
  3135  		return opener
  3136  
  3137  
  3138  def GetRpcServer(options):
  3139  	"""Returns an instance of an AbstractRpcServer.
  3140  
  3141  	Returns:
  3142  		A new AbstractRpcServer, on which RPC calls can be made.
  3143  	"""
  3144  
  3145  	rpc_server_class = HttpRpcServer
  3146  
  3147  	def GetUserCredentials():
  3148  		"""Prompts the user for a username and password."""
  3149  		# Disable status prints so they don't obscure the password prompt.
  3150  		global global_status
  3151  		st = global_status
  3152  		global_status = None
  3153  
  3154  		email = options.email
  3155  		if email is None:
  3156  			email = GetEmail("Email (login for uploading to %s)" % options.server)
  3157  		password = getpass.getpass("Password for %s: " % email)
  3158  
  3159  		# Put status back.
  3160  		global_status = st
  3161  		return (email, password)
  3162  
  3163  	# If this is the dev_appserver, use fake authentication.
  3164  	host = (options.host or options.server).lower()
  3165  	if host == "localhost" or host.startswith("localhost:"):
  3166  		email = options.email
  3167  		if email is None:
  3168  			email = "test@example.com"
  3169  			logging.info("Using debug user %s.  Override with --email" % email)
  3170  		server = rpc_server_class(
  3171  				options.server,
  3172  				lambda: (email, "password"),
  3173  				host_override=options.host,
  3174  				extra_headers={"Cookie": 'dev_appserver_login="%s:False"' % email},
  3175  				save_cookies=options.save_cookies)
  3176  		# Don't try to talk to ClientLogin.
  3177  		server.authenticated = True
  3178  		return server
  3179  
  3180  	return rpc_server_class(options.server, GetUserCredentials,
  3181  		host_override=options.host, save_cookies=options.save_cookies)
  3182  
  3183  
  3184  def EncodeMultipartFormData(fields, files):
  3185  	"""Encode form fields for multipart/form-data.
  3186  
  3187  	Args:
  3188  		fields: A sequence of (name, value) elements for regular form fields.
  3189  		files: A sequence of (name, filename, value) elements for data to be
  3190  					uploaded as files.
  3191  	Returns:
  3192  		(content_type, body) ready for httplib.HTTP instance.
  3193  
  3194  	Source:
  3195  		http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
  3196  	"""
  3197  	BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
  3198  	CRLF = '\r\n'
  3199  	lines = []
  3200  	for (key, value) in fields:
  3201  		typecheck(key, str)
  3202  		typecheck(value, str)
  3203  		lines.append('--' + BOUNDARY)
  3204  		lines.append('Content-Disposition: form-data; name="%s"' % key)
  3205  		lines.append('')
  3206  		lines.append(value)
  3207  	for (key, filename, value) in files:
  3208  		typecheck(key, str)
  3209  		typecheck(filename, str)
  3210  		typecheck(value, str)
  3211  		lines.append('--' + BOUNDARY)
  3212  		lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
  3213  		lines.append('Content-Type: %s' % GetContentType(filename))
  3214  		lines.append('')
  3215  		lines.append(value)
  3216  	lines.append('--' + BOUNDARY + '--')
  3217  	lines.append('')
  3218  	body = CRLF.join(lines)
  3219  	content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
  3220  	return content_type, body
  3221  
  3222  
  3223  def GetContentType(filename):
  3224  	"""Helper to guess the content-type from the filename."""
  3225  	return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
  3226  
  3227  
  3228  # Use a shell for subcommands on Windows to get a PATH search.
  3229  use_shell = sys.platform.startswith("win")
  3230  
  3231  def RunShellWithReturnCode(command, print_output=False,
  3232  		universal_newlines=True, env=os.environ):
  3233  	"""Executes a command and returns the output from stdout and the return code.
  3234  
  3235  	Args:
  3236  		command: Command to execute.
  3237  		print_output: If True, the output is printed to stdout.
  3238  			If False, both stdout and stderr are ignored.
  3239  		universal_newlines: Use universal_newlines flag (default: True).
  3240  
  3241  	Returns:
  3242  		Tuple (output, return code)
  3243  	"""
  3244  	logging.info("Running %s", command)
  3245  	p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  3246  		shell=use_shell, universal_newlines=universal_newlines, env=env)
  3247  	if print_output:
  3248  		output_array = []
  3249  		while True:
  3250  			line = p.stdout.readline()
  3251  			if not line:
  3252  				break
  3253  			print line.strip("\n")
  3254  			output_array.append(line)
  3255  		output = "".join(output_array)
  3256  	else:
  3257  		output = p.stdout.read()
  3258  	p.wait()
  3259  	errout = p.stderr.read()
  3260  	if print_output and errout:
  3261  		print >>sys.stderr, errout
  3262  	p.stdout.close()
  3263  	p.stderr.close()
  3264  	return output, p.returncode
  3265  
  3266  
  3267  def RunShell(command, silent_ok=False, universal_newlines=True,
  3268  		print_output=False, env=os.environ):
  3269  	data, retcode = RunShellWithReturnCode(command, print_output, universal_newlines, env)
  3270  	if retcode:
  3271  		ErrorExit("Got error status from %s:\n%s" % (command, data))
  3272  	if not silent_ok and not data:
  3273  		ErrorExit("No output from %s" % command)
  3274  	return data
  3275  
  3276  
  3277  class VersionControlSystem(object):
  3278  	"""Abstract base class providing an interface to the VCS."""
  3279  
  3280  	def __init__(self, options):
  3281  		"""Constructor.
  3282  
  3283  		Args:
  3284  			options: Command line options.
  3285  		"""
  3286  		self.options = options
  3287  
  3288  	def GenerateDiff(self, args):
  3289  		"""Return the current diff as a string.
  3290  
  3291  		Args:
  3292  			args: Extra arguments to pass to the diff command.
  3293  		"""
  3294  		raise NotImplementedError(
  3295  				"abstract method -- subclass %s must override" % self.__class__)
  3296  
  3297  	def GetUnknownFiles(self):
  3298  		"""Return a list of files unknown to the VCS."""
  3299  		raise NotImplementedError(
  3300  				"abstract method -- subclass %s must override" % self.__class__)
  3301  
  3302  	def CheckForUnknownFiles(self):
  3303  		"""Show an "are you sure?" prompt if there are unknown files."""
  3304  		unknown_files = self.GetUnknownFiles()
  3305  		if unknown_files:
  3306  			print "The following files are not added to version control:"
  3307  			for line in unknown_files:
  3308  				print line
  3309  			prompt = "Are you sure to continue?(y/N) "
  3310  			answer = raw_input(prompt).strip()
  3311  			if answer != "y":
  3312  				ErrorExit("User aborted")
  3313  
  3314  	def GetBaseFile(self, filename):
  3315  		"""Get the content of the upstream version of a file.
  3316  
  3317  		Returns:
  3318  			A tuple (base_content, new_content, is_binary, status)
  3319  				base_content: The contents of the base file.
  3320  				new_content: For text files, this is empty.  For binary files, this is
  3321  					the contents of the new file, since the diff output won't contain
  3322  					information to reconstruct the current file.
  3323  				is_binary: True iff the file is binary.
  3324  				status: The status of the file.
  3325  		"""
  3326  
  3327  		raise NotImplementedError(
  3328  				"abstract method -- subclass %s must override" % self.__class__)
  3329  
  3330  
  3331  	def GetBaseFiles(self, diff):
  3332  		"""Helper that calls GetBase file for each file in the patch.
  3333  
  3334  		Returns:
  3335  			A dictionary that maps from filename to GetBaseFile's tuple.  Filenames
  3336  			are retrieved based on lines that start with "Index:" or
  3337  			"Property changes on:".
  3338  		"""
  3339  		files = {}
  3340  		for line in diff.splitlines(True):
  3341  			if line.startswith('Index:') or line.startswith('Property changes on:'):
  3342  				unused, filename = line.split(':', 1)
  3343  				# On Windows if a file has property changes its filename uses '\'
  3344  				# instead of '/'.
  3345  				filename = to_slash(filename.strip())
  3346  				files[filename] = self.GetBaseFile(filename)
  3347  		return files
  3348  
  3349  
  3350  	def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options,
  3351  											files):
  3352  		"""Uploads the base files (and if necessary, the current ones as well)."""
  3353  
  3354  		def UploadFile(filename, file_id, content, is_binary, status, is_base):
  3355  			"""Uploads a file to the server."""
  3356  			set_status("uploading " + filename)
  3357  			file_too_large = False
  3358  			if is_base:
  3359  				type = "base"
  3360  			else:
  3361  				type = "current"
  3362  			if len(content) > MAX_UPLOAD_SIZE:
  3363  				print ("Not uploading the %s file for %s because it's too large." %
  3364  							(type, filename))
  3365  				file_too_large = True
  3366  				content = ""
  3367  			checksum = md5(content).hexdigest()
  3368  			if options.verbose > 0 and not file_too_large:
  3369  				print "Uploading %s file for %s" % (type, filename)
  3370  			url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
  3371  			form_fields = [
  3372  				("filename", filename),
  3373  				("status", status),
  3374  				("checksum", checksum),
  3375  				("is_binary", str(is_binary)),
  3376  				("is_current", str(not is_base)),
  3377  			]
  3378  			if file_too_large:
  3379  				form_fields.append(("file_too_large", "1"))
  3380  			if options.email:
  3381  				form_fields.append(("user", options.email))
  3382  			ctype, body = EncodeMultipartFormData(form_fields, [("data", filename, content)])
  3383  			response_body = rpc_server.Send(url, body, content_type=ctype)
  3384  			if not response_body.startswith("OK"):
  3385  				StatusUpdate("  --> %s" % response_body)
  3386  				sys.exit(1)
  3387  
  3388  		# Don't want to spawn too many threads, nor do we want to
  3389  		# hit Rietveld too hard, or it will start serving 500 errors.
  3390  		# When 8 works, it's no better than 4, and sometimes 8 is
  3391  		# too many for Rietveld to handle.
  3392  		MAX_PARALLEL_UPLOADS = 4
  3393  
  3394  		sema = threading.BoundedSemaphore(MAX_PARALLEL_UPLOADS)
  3395  		upload_threads = []
  3396  		finished_upload_threads = []
  3397  		
  3398  		class UploadFileThread(threading.Thread):
  3399  			def __init__(self, args):
  3400  				threading.Thread.__init__(self)
  3401  				self.args = args
  3402  			def run(self):
  3403  				UploadFile(*self.args)
  3404  				finished_upload_threads.append(self)
  3405  				sema.release()
  3406  
  3407  		def StartUploadFile(*args):
  3408  			sema.acquire()
  3409  			while len(finished_upload_threads) > 0:
  3410  				t = finished_upload_threads.pop()
  3411  				upload_threads.remove(t)
  3412  				t.join()
  3413  			t = UploadFileThread(args)
  3414  			upload_threads.append(t)
  3415  			t.start()
  3416  
  3417  		def WaitForUploads():			
  3418  			for t in upload_threads:
  3419  				t.join()
  3420  
  3421  		patches = dict()
  3422  		[patches.setdefault(v, k) for k, v in patch_list]
  3423  		for filename in patches.keys():
  3424  			base_content, new_content, is_binary, status = files[filename]
  3425  			file_id_str = patches.get(filename)
  3426  			if file_id_str.find("nobase") != -1:
  3427  				base_content = None
  3428  				file_id_str = file_id_str[file_id_str.rfind("_") + 1:]
  3429  			file_id = int(file_id_str)
  3430  			if base_content != None:
  3431  				StartUploadFile(filename, file_id, base_content, is_binary, status, True)
  3432  			if new_content != None:
  3433  				StartUploadFile(filename, file_id, new_content, is_binary, status, False)
  3434  		WaitForUploads()
  3435  
  3436  	def IsImage(self, filename):
  3437  		"""Returns true if the filename has an image extension."""
  3438  		mimetype =  mimetypes.guess_type(filename)[0]
  3439  		if not mimetype:
  3440  			return False
  3441  		return mimetype.startswith("image/")
  3442  
  3443  	def IsBinary(self, filename):
  3444  		"""Returns true if the guessed mimetyped isnt't in text group."""
  3445  		mimetype = mimetypes.guess_type(filename)[0]
  3446  		if not mimetype:
  3447  			return False  # e.g. README, "real" binaries usually have an extension
  3448  		# special case for text files which don't start with text/
  3449  		if mimetype in TEXT_MIMETYPES:
  3450  			return False
  3451  		return not mimetype.startswith("text/")
  3452  
  3453  
  3454  class FakeMercurialUI(object):
  3455  	def __init__(self):
  3456  		self.quiet = True
  3457  		self.output = ''
  3458  		self.debugflag = False
  3459  	
  3460  	def write(self, *args, **opts):
  3461  		self.output += ' '.join(args)
  3462  	def copy(self):
  3463  		return self
  3464  	def status(self, *args, **opts):
  3465  		pass
  3466  
  3467  	def formatter(self, topic, opts):
  3468  		from mercurial.formatter import plainformatter
  3469  		return plainformatter(self, topic, opts)
  3470  	
  3471  	def readconfig(self, *args, **opts):
  3472  		pass
  3473  	def expandpath(self, *args, **opts):
  3474  		return global_ui.expandpath(*args, **opts)
  3475  	def configitems(self, *args, **opts):
  3476  		return global_ui.configitems(*args, **opts)
  3477  	def config(self, *args, **opts):
  3478  		return global_ui.config(*args, **opts)
  3479  
  3480  use_hg_shell = False	# set to True to shell out to hg always; slower
  3481  
  3482  class MercurialVCS(VersionControlSystem):
  3483  	"""Implementation of the VersionControlSystem interface for Mercurial."""
  3484  
  3485  	def __init__(self, options, ui, repo):
  3486  		super(MercurialVCS, self).__init__(options)
  3487  		self.ui = ui
  3488  		self.repo = repo
  3489  		self.status = None
  3490  		# Absolute path to repository (we can be in a subdir)
  3491  		self.repo_dir = os.path.normpath(repo.root)
  3492  		# Compute the subdir
  3493  		cwd = os.path.normpath(os.getcwd())
  3494  		assert cwd.startswith(self.repo_dir)
  3495  		self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/")
  3496  		mqparent, err = RunShellWithReturnCode(['hg', 'log', '--rev', 'qparent', '--template={node}'])
  3497  		if not err and mqparent != "":
  3498  			self.base_rev = mqparent
  3499  		else:
  3500  			out = RunShell(["hg", "parents", "-q", "--template={node} {branch}"], silent_ok=True).strip()
  3501  			if not out:
  3502  				# No revisions; use 0 to mean a repository with nothing.
  3503  				out = "0:0 default"
  3504  			
  3505  			# Find parent along current branch.
  3506  			branch = repo[None].branch()
  3507  			base = ""
  3508  			for line in out.splitlines():
  3509  				fields = line.strip().split(' ')
  3510  				if fields[1] == branch:
  3511  					base = fields[0]
  3512  					break
  3513  			if base == "":
  3514  				# Use the first parent
  3515  				base = out.strip().split(' ')[0]
  3516  			self.base_rev = base
  3517  
  3518  	def _GetRelPath(self, filename):
  3519  		"""Get relative path of a file according to the current directory,
  3520  		given its logical path in the repo."""
  3521  		assert filename.startswith(self.subdir), (filename, self.subdir)
  3522  		return filename[len(self.subdir):].lstrip(r"\/")
  3523  
  3524  	def GenerateDiff(self, extra_args):
  3525  		# If no file specified, restrict to the current subdir
  3526  		extra_args = extra_args or ["."]
  3527  		cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
  3528  		data = RunShell(cmd, silent_ok=True)
  3529  		svndiff = []
  3530  		filecount = 0
  3531  		for line in data.splitlines():
  3532  			m = re.match("diff --git a/(\S+) b/(\S+)", line)
  3533  			if m:
  3534  				# Modify line to make it look like as it comes from svn diff.
  3535  				# With this modification no changes on the server side are required
  3536  				# to make upload.py work with Mercurial repos.
  3537  				# NOTE: for proper handling of moved/copied files, we have to use
  3538  				# the second filename.
  3539  				filename = m.group(2)
  3540  				svndiff.append("Index: %s" % filename)
  3541  				svndiff.append("=" * 67)
  3542  				filecount += 1
  3543  				logging.info(line)
  3544  			else:
  3545  				svndiff.append(line)
  3546  		if not filecount:
  3547  			ErrorExit("No valid patches found in output from hg diff")
  3548  		return "\n".join(svndiff) + "\n"
  3549  
  3550  	def GetUnknownFiles(self):
  3551  		"""Return a list of files unknown to the VCS."""
  3552  		args = []
  3553  		status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."],
  3554  				silent_ok=True)
  3555  		unknown_files = []
  3556  		for line in status.splitlines():
  3557  			st, fn = line.split(" ", 1)
  3558  			if st == "?":
  3559  				unknown_files.append(fn)
  3560  		return unknown_files
  3561  
  3562  	def get_hg_status(self, rev, path):
  3563  		# We'd like to use 'hg status -C path', but that is buggy
  3564  		# (see http://mercurial.selenic.com/bts/issue3023).
  3565  		# Instead, run 'hg status -C' without a path
  3566  		# and skim the output for the path we want.
  3567  		if self.status is None:
  3568  			if use_hg_shell:
  3569  				out = RunShell(["hg", "status", "-C", "--rev", rev])
  3570  			else:
  3571  				fui = FakeMercurialUI()
  3572  				ret = hg_commands.status(fui, self.repo, *[], **{'rev': [rev], 'copies': True})
  3573  				if ret:
  3574  					raise hg_util.Abort(ret)
  3575  				out = fui.output
  3576  			self.status = out.splitlines()
  3577  		for i in range(len(self.status)):
  3578  			# line is
  3579  			#	A path
  3580  			#	M path
  3581  			# etc
  3582  			line = to_slash(self.status[i])
  3583  			if line[2:] == path:
  3584  				if i+1 < len(self.status) and self.status[i+1][:2] == '  ':
  3585  					return self.status[i:i+2]
  3586  				return self.status[i:i+1]
  3587  		raise hg_util.Abort("no status for " + path)
  3588  	
  3589  	def GetBaseFile(self, filename):
  3590  		set_status("inspecting " + filename)
  3591  		# "hg status" and "hg cat" both take a path relative to the current subdir
  3592  		# rather than to the repo root, but "hg diff" has given us the full path
  3593  		# to the repo root.
  3594  		base_content = ""
  3595  		new_content = None
  3596  		is_binary = False
  3597  		oldrelpath = relpath = self._GetRelPath(filename)
  3598  		out = self.get_hg_status(self.base_rev, relpath)
  3599  		status, what = out[0].split(' ', 1)
  3600  		if len(out) > 1 and status == "A" and what == relpath:
  3601  			oldrelpath = out[1].strip()
  3602  			status = "M"
  3603  		if ":" in self.base_rev:
  3604  			base_rev = self.base_rev.split(":", 1)[0]
  3605  		else:
  3606  			base_rev = self.base_rev
  3607  		if status != "A":
  3608  			if use_hg_shell:
  3609  				base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath], silent_ok=True)
  3610  			else:
  3611  				base_content = str(self.repo[base_rev][oldrelpath].data())
  3612  			is_binary = "\0" in base_content  # Mercurial's heuristic
  3613  		if status != "R":
  3614  			new_content = open(relpath, "rb").read()
  3615  			is_binary = is_binary or "\0" in new_content
  3616  		if is_binary and base_content and use_hg_shell:
  3617  			# Fetch again without converting newlines
  3618  			base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
  3619  				silent_ok=True, universal_newlines=False)
  3620  		if not is_binary or not self.IsImage(relpath):
  3621  			new_content = None
  3622  		return base_content, new_content, is_binary, status
  3623  
  3624  
  3625  # NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync.
  3626  def SplitPatch(data):
  3627  	"""Splits a patch into separate pieces for each file.
  3628  
  3629  	Args:
  3630  		data: A string containing the output of svn diff.
  3631  
  3632  	Returns:
  3633  		A list of 2-tuple (filename, text) where text is the svn diff output
  3634  			pertaining to filename.
  3635  	"""
  3636  	patches = []
  3637  	filename = None
  3638  	diff = []
  3639  	for line in data.splitlines(True):
  3640  		new_filename = None
  3641  		if line.startswith('Index:'):
  3642  			unused, new_filename = line.split(':', 1)
  3643  			new_filename = new_filename.strip()
  3644  		elif line.startswith('Property changes on:'):
  3645  			unused, temp_filename = line.split(':', 1)
  3646  			# When a file is modified, paths use '/' between directories, however
  3647  			# when a property is modified '\' is used on Windows.  Make them the same
  3648  			# otherwise the file shows up twice.
  3649  			temp_filename = to_slash(temp_filename.strip())
  3650  			if temp_filename != filename:
  3651  				# File has property changes but no modifications, create a new diff.
  3652  				new_filename = temp_filename
  3653  		if new_filename:
  3654  			if filename and diff:
  3655  				patches.append((filename, ''.join(diff)))
  3656  			filename = new_filename
  3657  			diff = [line]
  3658  			continue
  3659  		if diff is not None:
  3660  			diff.append(line)
  3661  	if filename and diff:
  3662  		patches.append((filename, ''.join(diff)))
  3663  	return patches
  3664  
  3665  
  3666  def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
  3667  	"""Uploads a separate patch for each file in the diff output.
  3668  
  3669  	Returns a list of [patch_key, filename] for each file.
  3670  	"""
  3671  	patches = SplitPatch(data)
  3672  	rv = []
  3673  	for patch in patches:
  3674  		set_status("uploading patch for " + patch[0])
  3675  		if len(patch[1]) > MAX_UPLOAD_SIZE:
  3676  			print ("Not uploading the patch for " + patch[0] +
  3677  				" because the file is too large.")
  3678  			continue
  3679  		form_fields = [("filename", patch[0])]
  3680  		if not options.download_base:
  3681  			form_fields.append(("content_upload", "1"))
  3682  		files = [("data", "data.diff", patch[1])]
  3683  		ctype, body = EncodeMultipartFormData(form_fields, files)
  3684  		url = "/%d/upload_patch/%d" % (int(issue), int(patchset))
  3685  		print "Uploading patch for " + patch[0]
  3686  		response_body = rpc_server.Send(url, body, content_type=ctype)
  3687  		lines = response_body.splitlines()
  3688  		if not lines or lines[0] != "OK":
  3689  			StatusUpdate("  --> %s" % response_body)
  3690  			sys.exit(1)
  3691  		rv.append([lines[1], patch[0]])
  3692  	return rv