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