PageRenderTime 198ms CodeModel.GetById 85ms app.highlight 95ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/codereview/codereview.py

https://code.google.com/p/go-uuid/
Python | 3291 lines | 3134 code | 45 blank | 112 comment | 128 complexity | cb8a7871ced764bda93ec1e72f7c9f48 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   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
  21To configure, set the following options in
  22your repository's .hg/hgrc file.
  23
  24	[extensions]
  25	codereview = path/to/codereview.py
  26
  27	[codereview]
  28	server = codereview.appspot.com
  29
  30The server should be running Rietveld; see http://code.google.com/p/rietveld/.
  31
  32In addition to the new commands, this extension introduces
  33the file pattern syntax @nnnnnn, where nnnnnn is a change list
  34number, to mean the files included in that change list, which
  35must be associated with the current client.
  36
  37For 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
  41from mercurial import cmdutil, commands, hg, util, error, match
  42from mercurial.node import nullrev, hex, nullid, short
  43import os, re, time
  44import stat
  45import subprocess
  46import threading
  47from HTMLParser import HTMLParser
  48
  49# The standard 'json' package is new in Python 2.6.
  50# Before that it was an external package named simplejson.
  51try:
  52	# Standard location in 2.6 and beyond.
  53	import json
  54except Exception, e:
  55	try:
  56		# Conventional name for earlier package.
  57		import simplejson as json
  58	except:
  59		try:
  60			# Was also bundled with django, which is commonly installed.
  61			from django.utils import simplejson as json
  62		except:
  63			# We give up.
  64			raise e
  65
  66try:
  67	hgversion = util.version()
  68except:
  69	from mercurial.version import version as v
  70	hgversion = v.get_version()
  71
  72try:
  73	from mercurial.discovery import findcommonincoming
  74except:
  75	def findcommonincoming(repo, remote):
  76		return repo.findcommonincoming(remote)
  77
  78# in Mercurial 1.9 the cmdutil.match and cmdutil.revpair moved to scmutil
  79if hgversion >= '1.9':
  80    from mercurial import scmutil
  81else:
  82    scmutil = cmdutil
  83
  84oldMessage = """
  85The code review extension requires Mercurial 1.3 or newer.
  86
  87To install a new Mercurial,
  88
  89	sudo easy_install mercurial
  90
  91works on most systems.
  92"""
  93
  94linuxMessage = """
  95You may need to clear your current Mercurial installation by running:
  96
  97	sudo apt-get remove mercurial mercurial-common
  98	sudo rm -rf /etc/mercurial
  99"""
 100
 101if hgversion < '1.3':
 102	msg = oldMessage
 103	if os.access("/etc/mercurial", 0):
 104		msg += linuxMessage
 105	raise util.Abort(msg)
 106
 107def promptyesno(ui, msg):
 108	# Arguments to ui.prompt changed between 1.3 and 1.3.1.
 109	# Even so, some 1.3.1 distributions seem to have the old prompt!?!?
 110	# What a terrible way to maintain software.
 111	try:
 112		return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0
 113	except AttributeError:
 114		return ui.prompt(msg, ["&yes", "&no"], "y") != "n"
 115
 116# To experiment with Mercurial in the python interpreter:
 117#    >>> repo = hg.repository(ui.ui(), path = ".")
 118
 119#######################################################################
 120# Normally I would split this into multiple files, but it simplifies
 121# import path headaches to keep it all in one file.  Sorry.
 122
 123import sys
 124if __name__ == "__main__":
 125	print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly."
 126	sys.exit(2)
 127
 128server = "codereview.appspot.com"
 129server_url_base = None
 130defaultcc = None
 131contributors = {}
 132missing_codereview = None
 133real_rollback = None
 134releaseBranch = None
 135
 136#######################################################################
 137# RE: UNICODE STRING HANDLING
 138#
 139# Python distinguishes between the str (string of bytes)
 140# and unicode (string of code points) types.  Most operations
 141# work on either one just fine, but some (like regexp matching)
 142# require unicode, and others (like write) require str.
 143#
 144# As befits the language, Python hides the distinction between
 145# unicode and str by converting between them silently, but
 146# *only* if all the bytes/code points involved are 7-bit ASCII.
 147# This means that if you're not careful, your program works
 148# fine on "hello, world" and fails on "hello, 世界".  And of course,
 149# the obvious way to be careful - use static types - is unavailable.
 150# So the only way is trial and error to find where to put explicit
 151# conversions.
 152#
 153# Because more functions do implicit conversion to str (string of bytes)
 154# than do implicit conversion to unicode (string of code points),
 155# the convention in this module is to represent all text as str,
 156# converting to unicode only when calling a unicode-only function
 157# and then converting back to str as soon as possible.
 158
 159def typecheck(s, t):
 160	if type(s) != t:
 161		raise util.Abort("type check failed: %s has type %s != %s" % (repr(s), type(s), t))
 162
 163# If we have to pass unicode instead of str, ustr does that conversion clearly.
 164def ustr(s):
 165	typecheck(s, str)
 166	return s.decode("utf-8")
 167
 168# Even with those, Mercurial still sometimes turns unicode into str
 169# and then tries to use it as ascii.  Change Mercurial's default.
 170def set_mercurial_encoding_to_utf8():
 171	from mercurial import encoding
 172	encoding.encoding = 'utf-8'
 173
 174set_mercurial_encoding_to_utf8()
 175
 176# Even with those we still run into problems.
 177# I tried to do things by the book but could not convince
 178# Mercurial to let me check in a change with UTF-8 in the
 179# CL description or author field, no matter how many conversions
 180# between str and unicode I inserted and despite changing the
 181# default encoding.  I'm tired of this game, so set the default
 182# encoding for all of Python to 'utf-8', not 'ascii'.
 183def default_to_utf8():
 184	import sys
 185	reload(sys)  # site.py deleted setdefaultencoding; get it back
 186	sys.setdefaultencoding('utf-8')
 187
 188default_to_utf8()
 189
 190#######################################################################
 191# Change list parsing.
 192#
 193# Change lists are stored in .hg/codereview/cl.nnnnnn
 194# where nnnnnn is the number assigned by the code review server.
 195# Most data about a change list is stored on the code review server
 196# too: the description, reviewer, and cc list are all stored there.
 197# The only thing in the cl.nnnnnn file is the list of relevant files.
 198# Also, the existence of the cl.nnnnnn file marks this repository
 199# as the one where the change list lives.
 200
 201emptydiff = """Index: ~rietveld~placeholder~
 202===================================================================
 203diff --git a/~rietveld~placeholder~ b/~rietveld~placeholder~
 204new file mode 100644
 205"""
 206
 207class CL(object):
 208	def __init__(self, name):
 209		typecheck(name, str)
 210		self.name = name
 211		self.desc = ''
 212		self.files = []
 213		self.reviewer = []
 214		self.cc = []
 215		self.url = ''
 216		self.local = False
 217		self.web = False
 218		self.copied_from = None	# None means current user
 219		self.mailed = False
 220		self.private = False
 221
 222	def DiskText(self):
 223		cl = self
 224		s = ""
 225		if cl.copied_from:
 226			s += "Author: " + cl.copied_from + "\n\n"
 227		if cl.private:
 228			s += "Private: " + str(self.private) + "\n"
 229		s += "Mailed: " + str(self.mailed) + "\n"
 230		s += "Description:\n"
 231		s += Indent(cl.desc, "\t")
 232		s += "Files:\n"
 233		for f in cl.files:
 234			s += "\t" + f + "\n"
 235		typecheck(s, str)
 236		return s
 237
 238	def EditorText(self):
 239		cl = self
 240		s = _change_prolog
 241		s += "\n"
 242		if cl.copied_from:
 243			s += "Author: " + cl.copied_from + "\n"
 244		if cl.url != '':
 245			s += 'URL: ' + cl.url + '	# cannot edit\n\n'
 246		if cl.private:
 247			s += "Private: True\n"
 248		s += "Reviewer: " + JoinComma(cl.reviewer) + "\n"
 249		s += "CC: " + JoinComma(cl.cc) + "\n"
 250		s += "\n"
 251		s += "Description:\n"
 252		if cl.desc == '':
 253			s += "\t<enter description here>\n"
 254		else:
 255			s += Indent(cl.desc, "\t")
 256		s += "\n"
 257		if cl.local or cl.name == "new":
 258			s += "Files:\n"
 259			for f in cl.files:
 260				s += "\t" + f + "\n"
 261			s += "\n"
 262		typecheck(s, str)
 263		return s
 264
 265	def PendingText(self):
 266		cl = self
 267		s = cl.name + ":" + "\n"
 268		s += Indent(cl.desc, "\t")
 269		s += "\n"
 270		if cl.copied_from:
 271			s += "\tAuthor: " + cl.copied_from + "\n"
 272		s += "\tReviewer: " + JoinComma(cl.reviewer) + "\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 + " " + getremote(ui, repo, {}).path))
 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 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 = getremote(ui, repo, {}).path
 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
 402def GoodCLName(name):
 403	typecheck(name, str)
 404	return re.match("^[0-9]+$", name)
 405
 406def 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
 472def SplitCommaSpace(s):
 473	typecheck(s, str)
 474	s = s.strip()
 475	if s == "":
 476		return []
 477	return re.split(", *", s)
 478
 479def CutDomain(s):
 480	typecheck(s, str)
 481	i = s.find('@')
 482	if i >= 0:
 483		s = s[0:i]
 484	return s
 485
 486def JoinComma(l):
 487	for s in l:
 488		typecheck(s, str)
 489	return ", ".join(l)
 490
 491def 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
 502def 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.
 506def 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	set_status("loaded CL " + name)
 546	return cl, ''
 547
 548global_status = None
 549
 550def set_status(s):
 551	# print >>sys.stderr, "\t", time.asctime(), s
 552	global global_status
 553	global_status = s
 554
 555class StatusThread(threading.Thread):
 556	def __init__(self):
 557		threading.Thread.__init__(self)
 558	def run(self):
 559		# pause a reasonable amount of time before
 560		# starting to display status messages, so that
 561		# most hg commands won't ever see them.
 562		time.sleep(30)
 563
 564		# now show status every 15 seconds
 565		while True:
 566			time.sleep(15 - time.time() % 15)
 567			s = global_status
 568			if s is None:
 569				continue
 570			if s == "":
 571				s = "(unknown status)"
 572			print >>sys.stderr, time.asctime(), s
 573
 574def start_status_thread():
 575	t = StatusThread()
 576	t.setDaemon(True)  # allowed to exit if t is still running
 577	t.start()
 578
 579class LoadCLThread(threading.Thread):
 580	def __init__(self, ui, repo, dir, f, web):
 581		threading.Thread.__init__(self)
 582		self.ui = ui
 583		self.repo = repo
 584		self.dir = dir
 585		self.f = f
 586		self.web = web
 587		self.cl = None
 588	def run(self):
 589		cl, err = LoadCL(self.ui, self.repo, self.f[3:], web=self.web)
 590		if err != '':
 591			self.ui.warn("loading "+self.dir+self.f+": " + err + "\n")
 592			return
 593		self.cl = cl
 594
 595# Load all the CLs from this repository.
 596def LoadAllCL(ui, repo, web=True):
 597	dir = CodeReviewDir(ui, repo)
 598	m = {}
 599	files = [f for f in os.listdir(dir) if f.startswith('cl.')]
 600	if not files:
 601		return m
 602	active = []
 603	first = True
 604	for f in files:
 605		t = LoadCLThread(ui, repo, dir, f, web)
 606		t.start()
 607		if web and first:
 608			# first request: wait in case it needs to authenticate
 609			# otherwise we get lots of user/password prompts
 610			# running in parallel.
 611			t.join()
 612			if t.cl:
 613				m[t.cl.name] = t.cl
 614			first = False
 615		else:
 616			active.append(t)
 617	for t in active:
 618		t.join()
 619		if t.cl:
 620			m[t.cl.name] = t.cl
 621	return m
 622
 623# Find repository root.  On error, ui.warn and return None
 624def RepoDir(ui, repo):
 625	url = repo.url();
 626	if not url.startswith('file:'):
 627		ui.warn("repository %s is not in local file system\n" % (url,))
 628		return None
 629	url = url[5:]
 630	if url.endswith('/'):
 631		url = url[:-1]
 632	typecheck(url, str)
 633	return url
 634
 635# Find (or make) code review directory.  On error, ui.warn and return None
 636def CodeReviewDir(ui, repo):
 637	dir = RepoDir(ui, repo)
 638	if dir == None:
 639		return None
 640	dir += '/.hg/codereview/'
 641	if not os.path.isdir(dir):
 642		try:
 643			os.mkdir(dir, 0700)
 644		except:
 645			ui.warn('cannot mkdir %s: %s\n' % (dir, ExceptionDetail()))
 646			return None
 647	typecheck(dir, str)
 648	return dir
 649
 650# Turn leading tabs into spaces, so that the common white space
 651# prefix doesn't get confused when people's editors write out 
 652# some lines with spaces, some with tabs.  Only a heuristic
 653# (some editors don't use 8 spaces either) but a useful one.
 654def TabsToSpaces(line):
 655	i = 0
 656	while i < len(line) and line[i] == '\t':
 657		i += 1
 658	return ' '*(8*i) + line[i:]
 659
 660# Strip maximal common leading white space prefix from text
 661def StripCommon(text):
 662	typecheck(text, str)
 663	ws = None
 664	for line in text.split('\n'):
 665		line = line.rstrip()
 666		if line == '':
 667			continue
 668		line = TabsToSpaces(line)
 669		white = line[:len(line)-len(line.lstrip())]
 670		if ws == None:
 671			ws = white
 672		else:
 673			common = ''
 674			for i in range(min(len(white), len(ws))+1):
 675				if white[0:i] == ws[0:i]:
 676					common = white[0:i]
 677			ws = common
 678		if ws == '':
 679			break
 680	if ws == None:
 681		return text
 682	t = ''
 683	for line in text.split('\n'):
 684		line = line.rstrip()
 685		line = TabsToSpaces(line)
 686		if line.startswith(ws):
 687			line = line[len(ws):]
 688		if line == '' and t == '':
 689			continue
 690		t += line + '\n'
 691	while len(t) >= 2 and t[-2:] == '\n\n':
 692		t = t[:-1]
 693	typecheck(t, str)
 694	return t
 695
 696# Indent text with indent.
 697def Indent(text, indent):
 698	typecheck(text, str)
 699	typecheck(indent, str)
 700	t = ''
 701	for line in text.split('\n'):
 702		t += indent + line + '\n'
 703	typecheck(t, str)
 704	return t
 705
 706# Return the first line of l
 707def line1(text):
 708	typecheck(text, str)
 709	return text.split('\n')[0]
 710
 711_change_prolog = """# Change list.
 712# Lines beginning with # are ignored.
 713# Multi-line values should be indented.
 714"""
 715
 716#######################################################################
 717# Mercurial helper functions
 718
 719# Get effective change nodes taking into account applied MQ patches
 720def effective_revpair(repo):
 721    try:
 722	return scmutil.revpair(repo, ['qparent'])
 723    except:
 724	return scmutil.revpair(repo, None)
 725
 726# Return list of changed files in repository that match pats.
 727# Warn about patterns that did not match.
 728def matchpats(ui, repo, pats, opts):
 729	matcher = scmutil.match(repo, pats, opts)
 730	node1, node2 = effective_revpair(repo)
 731	modified, added, removed, deleted, unknown, ignored, clean = repo.status(node1, node2, matcher, ignored=True, clean=True, unknown=True)
 732	return (modified, added, removed, deleted, unknown, ignored, clean)
 733
 734# Return list of changed files in repository that match pats.
 735# The patterns came from the command line, so we warn
 736# if they have no effect or cannot be understood.
 737def ChangedFiles(ui, repo, pats, opts, taken=None):
 738	taken = taken or {}
 739	# Run each pattern separately so that we can warn about
 740	# patterns that didn't do anything useful.
 741	for p in pats:
 742		modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, [p], opts)
 743		redo = False
 744		for f in unknown:
 745			promptadd(ui, repo, f)
 746			redo = True
 747		for f in deleted:
 748			promptremove(ui, repo, f)
 749			redo = True
 750		if redo:
 751			modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, [p], opts)
 752		for f in modified + added + removed:
 753			if f in taken:
 754				ui.warn("warning: %s already in CL %s\n" % (f, taken[f].name))
 755		if not modified and not added and not removed:
 756			ui.warn("warning: %s did not match any modified files\n" % (p,))
 757
 758	# Again, all at once (eliminates duplicates)
 759	modified, added, removed = matchpats(ui, repo, pats, opts)[:3]
 760	l = modified + added + removed
 761	l.sort()
 762	if taken:
 763		l = Sub(l, taken.keys())
 764	return l
 765
 766# Return list of changed files in repository that match pats and still exist.
 767def ChangedExistingFiles(ui, repo, pats, opts):
 768	modified, added = matchpats(ui, repo, pats, opts)[:2]
 769	l = modified + added
 770	l.sort()
 771	return l
 772
 773# Return list of files claimed by existing CLs
 774def Taken(ui, repo):
 775	all = LoadAllCL(ui, repo, web=False)
 776	taken = {}
 777	for _, cl in all.items():
 778		for f in cl.files:
 779			taken[f] = cl
 780	return taken
 781
 782# Return list of changed files that are not claimed by other CLs
 783def DefaultFiles(ui, repo, pats, opts):
 784	return ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
 785
 786def Sub(l1, l2):
 787	return [l for l in l1 if l not in l2]
 788
 789def Add(l1, l2):
 790	l = l1 + Sub(l2, l1)
 791	l.sort()
 792	return l
 793
 794def Intersect(l1, l2):
 795	return [l for l in l1 if l in l2]
 796
 797def getremote(ui, repo, opts):
 798	# save $http_proxy; creating the HTTP repo object will
 799	# delete it in an attempt to "help"
 800	proxy = os.environ.get('http_proxy')
 801	source = hg.parseurl(ui.expandpath("default"), None)[0]
 802	try:
 803		remoteui = hg.remoteui # hg 1.6
 804	except:
 805		remoteui = cmdutil.remoteui
 806	other = hg.repository(remoteui(repo, opts), source)
 807	if proxy is not None:
 808		os.environ['http_proxy'] = proxy
 809	return other
 810
 811def Incoming(ui, repo, opts):
 812	_, incoming, _ = findcommonincoming(repo, getremote(ui, repo, opts))
 813	return incoming
 814
 815desc_re = '^(.+: |(tag )?(release|weekly)\.|fix build|undo CL)'
 816
 817desc_msg = '''Your CL description appears not to use the standard form.
 818
 819The first line of your change description is conventionally a
 820one-line summary of the change, prefixed by the primary affected package,
 821and is used as the subject for code review mail; the rest of the description
 822elaborates.
 823
 824Examples:
 825
 826	encoding/rot13: new package
 827
 828	math: add IsInf, IsNaN
 829	
 830	net: fix cname in LookupHost
 831
 832	unicode: update to Unicode 5.0.2
 833
 834'''
 835
 836
 837def promptremove(ui, repo, f):
 838	if promptyesno(ui, "hg remove %s (y/n)?" % (f,)):
 839		if commands.remove(ui, repo, 'path:'+f) != 0:
 840			ui.warn("error removing %s" % (f,))
 841
 842def promptadd(ui, repo, f):
 843	if promptyesno(ui, "hg add %s (y/n)?" % (f,)):
 844		if commands.add(ui, repo, 'path:'+f) != 0:
 845			ui.warn("error adding %s" % (f,))
 846
 847def EditCL(ui, repo, cl):
 848	set_status(None)	# do not show status
 849	s = cl.EditorText()
 850	while True:
 851		s = ui.edit(s, ui.username())
 852		clx, line, err = ParseCL(s, cl.name)
 853		if err != '':
 854			if not promptyesno(ui, "error parsing change list: line %d: %s\nre-edit (y/n)?" % (line, err)):
 855				return "change list not modified"
 856			continue
 857		
 858		# Check description.
 859		if clx.desc == '':
 860			if promptyesno(ui, "change list should have a description\nre-edit (y/n)?"):
 861				continue
 862		elif re.search('<enter reason for undo>', clx.desc):
 863			if promptyesno(ui, "change list description omits reason for undo\nre-edit (y/n)?"):
 864				continue
 865		elif not re.match(desc_re, clx.desc.split('\n')[0]):
 866			if promptyesno(ui, desc_msg + "re-edit (y/n)?"):
 867				continue
 868
 869		# Check file list for files that need to be hg added or hg removed
 870		# or simply aren't understood.
 871		pats = ['path:'+f for f in clx.files]
 872		modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, pats, {})
 873		files = []
 874		for f in clx.files:
 875			if f in modified or f in added or f in removed:
 876				files.append(f)
 877				continue
 878			if f in deleted:
 879				promptremove(ui, repo, f)
 880				files.append(f)
 881				continue
 882			if f in unknown:
 883				promptadd(ui, repo, f)
 884				files.append(f)
 885				continue
 886			if f in ignored:
 887				ui.warn("error: %s is excluded by .hgignore; omitting\n" % (f,))
 888				continue
 889			if f in clean:
 890				ui.warn("warning: %s is listed in the CL but unchanged\n" % (f,))
 891				files.append(f)
 892				continue
 893			p = repo.root + '/' + f
 894			if os.path.isfile(p):
 895				ui.warn("warning: %s is a file but not known to hg\n" % (f,))
 896				files.append(f)
 897				continue
 898			if os.path.isdir(p):
 899				ui.warn("error: %s is a directory, not a file; omitting\n" % (f,))
 900				continue
 901			ui.warn("error: %s does not exist; omitting\n" % (f,))
 902		clx.files = files
 903
 904		cl.desc = clx.desc
 905		cl.reviewer = clx.reviewer
 906		cl.cc = clx.cc
 907		cl.files = clx.files
 908		cl.private = clx.private
 909		break
 910	return ""
 911
 912# For use by submit, etc. (NOT by change)
 913# Get change list number or list of files from command line.
 914# If files are given, make a new change list.
 915def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
 916	if len(pats) > 0 and GoodCLName(pats[0]):
 917		if len(pats) != 1:
 918			return None, "cannot specify change number and file names"
 919		if opts.get('message'):
 920			return None, "cannot use -m with existing CL"
 921		cl, err = LoadCL(ui, repo, pats[0], web=True)
 922		if err != "":
 923			return None, err
 924	else:
 925		cl = CL("new")
 926		cl.local = True
 927		cl.files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
 928		if not cl.files:
 929			return None, "no files changed"
 930	if opts.get('reviewer'):
 931		cl.reviewer = Add(cl.reviewer, SplitCommaSpace(opts.get('reviewer')))
 932	if opts.get('cc'):
 933		cl.cc = Add(cl.cc, SplitCommaSpace(opts.get('cc')))
 934	if defaultcc:
 935		cl.cc = Add(cl.cc, defaultcc)
 936	if cl.name == "new":
 937		if opts.get('message'):
 938			cl.desc = opts.get('message')
 939		else:
 940			err = EditCL(ui, repo, cl)
 941			if err != '':
 942				return None, err
 943	return cl, ""
 944
 945# reposetup replaces cmdutil.match with this wrapper,
 946# which expands the syntax @clnumber to mean the files
 947# in that CL.
 948original_match = None
 949def ReplacementForCmdutilMatch(repo, pats=None, opts=None, globbed=False, default='relpath'):
 950	taken = []
 951	files = []
 952	pats = pats or []
 953	opts = opts or {}
 954	for p in pats:
 955		if p.startswith('@'):
 956			taken.append(p)
 957			clname = p[1:]
 958			if not GoodCLName(clname):
 959				raise util.Abort("invalid CL name " + clname)
 960			cl, err = LoadCL(repo.ui, repo, clname, web=False)
 961			if err != '':
 962				raise util.Abort("loading CL " + clname + ": " + err)
 963			if not cl.files:
 964				raise util.Abort("no files in CL " + clname)
 965			files = Add(files, cl.files)
 966	pats = Sub(pats, taken) + ['path:'+f for f in files]
 967
 968	# work-around for http://selenic.com/hg/rev/785bbc8634f8
 969	if hgversion >= '1.9' and not hasattr(repo, 'match'):
 970		repo = repo[None]
 971
 972	return original_match(repo, pats=pats, opts=opts, globbed=globbed, default=default)
 973
 974def RelativePath(path, cwd):
 975	n = len(cwd)
 976	if path.startswith(cwd) and path[n] == '/':
 977		return path[n+1:]
 978	return path
 979
 980def CheckFormat(ui, repo, files, just_warn=False):
 981	set_status("running gofmt")
 982	CheckGofmt(ui, repo, files, just_warn)
 983	CheckTabfmt(ui, repo, files, just_warn)
 984
 985# Check that gofmt run on the list of files does not change them
 986def CheckGofmt(ui, repo, files, just_warn):
 987	files = [f for f in files if (f.startswith('src/') or f.startswith('test/bench/')) and f.endswith('.go')]
 988	if not files:
 989		return
 990	cwd = os.getcwd()
 991	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
 992	files = [f for f in files if os.access(f, 0)]
 993	if not files:
 994		return
 995	try:
 996		cmd = subprocess.Popen(["gofmt", "-l"] + files, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=sys.platform != "win32")
 997		cmd.stdin.close()
 998	except:
 999		raise util.Abort("gofmt: " + ExceptionDetail())
1000	data = cmd.stdout.read()
1001	errors = cmd.stderr.read()
1002	cmd.wait()
1003	set_status("done with gofmt")
1004	if len(errors) > 0:
1005		ui.warn("gofmt errors:\n" + errors.rstrip() + "\n")
1006		return
1007	if len(data) > 0:
1008		msg = "gofmt needs to format these files (run hg gofmt):\n" + Indent(data, "\t").rstrip()
1009		if just_warn:
1010			ui.warn("warning: " + msg + "\n")
1011		else:
1012			raise util.Abort(msg)
1013	return
1014
1015# Check that *.[chys] files indent using tabs.
1016def CheckTabfmt(ui, repo, files, just_warn):
1017	files = [f for f in files if f.startswith('src/') and re.search(r"\.[chys]$", f)]
1018	if not files:
1019		return
1020	cwd = os.getcwd()
1021	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
1022	files = [f for f in files if os.access(f, 0)]
1023	badfiles = []
1024	for f in files:
1025		try:
1026			for line in open(f, 'r'):
1027				# Four leading spaces is enough to complain about,
1028				# except that some Plan 9 code uses four spaces as the label indent,
1029				# so allow that.
1030				if line.startswith('    ') and not re.match('    [A-Za-z0-9_]+:', line):
1031					badfiles.append(f)
1032					break
1033		except:
1034			# ignore cannot open file, etc.
1035			pass
1036	if len(badfiles) > 0:
1037		msg = "these files use spaces for indentation (use tabs instead):\n\t" + "\n\t".join(badfiles)
1038		if just_warn:
1039			ui.warn("warning: " + msg + "\n")
1040		else:
1041			raise util.Abort(msg)
1042	return
1043
1044#######################################################################
1045# Mercurial commands
1046
1047# every command must take a ui and and repo as arguments.
1048# opts is a dict where you can find other command line flags
1049#
1050# Other parameters are taken in order from items on the command line that
1051# don't start with a dash.  If no default value is given in the parameter list,
1052# they are required.
1053#
1054
1055def change(ui, repo, *pats, **opts):
1056	"""create, edit or delete a change list
1057
1058	Create, edit or delete a change list.
1059	A change list is a group of files to be reviewed and submitted together,
1060	plus a textual description of the change.
1061	Change lists are referred to by simple alphanumeric names.
1062
1063	Changes must be reviewed before they can be submitted.
1064
1065	In the absence of options, the change command opens the
1066	change list for editing in the default editor.
1067
1068	Deleting a change with the -d or -D flag does not affect
1069	the contents of the files listed in that change.  To revert
1070	the files listed in a change, use
1071
1072		hg revert @123456
1073
1074	before running hg change -d 123456.
1075	"""
1076
1077	if missing_codereview:
1078		return missing_codereview
1079	
1080	dirty = {}
1081	if len(pats) > 0 and GoodCLName(pats[0]):
1082		name = pats[0]
1083		if len(pats) != 1:
1084			return "cannot specify CL name and file patterns"
1085		pats = pats[1:]
1086		cl, err = LoadCL(ui, repo, name, web=True)
1087		if err != '':
1088			return err
1089		if not cl.local and (opts["stdin"] or not opts["stdout"]):
1090			return "cannot change non-local CL " + name
1091	else:
1092		if repo[None].branch() != "default":
1093			return "cannot run hg change outside default branch"
1094		name = "new"
1095		cl = CL("new")
1096		dirty[cl] = True
1097		files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
1098
1099	if opts["delete"] or opts["deletelocal"]:
1100		if opts["delete"] and opts["deletelocal"]:
1101			return "cannot use -d and -D together"
1102		flag = "-d"
1103		if opts["deletelocal"]:
1104			flag = "-D"
1105		if name == "new":
1106			return "cannot use "+flag+" with file patterns"
1107		if opts["stdin"] or opts["stdout"]:
1108			return "cannot use "+flag+" with -i or -o"
1109		if not cl.local:
1110			return "cannot change non-local CL " + name
1111		if opts["delete"]:
1112			if cl.copied_from:
1113				return "original author must delete CL; hg change -D will remove locally"
1114			PostMessage(ui, cl.name, "*** Abandoned ***", send_mail=cl.mailed)
1115			EditDesc(cl.name, closed=True, private=cl.private)
1116		cl.Delete(ui, repo)
1117		return
1118
1119	if opts["stdin"]:
1120		s = sys.stdin.read()
1121		clx, line, err = ParseCL(s, name)
1122		if err != '':
1123			return "error parsing change list: line %d: %s" % (line, err)
1124		if clx.desc is not None:
1125			cl.desc = clx.desc;
1126			dirty[cl] = True
1127		if clx.reviewer is not None:
1128			cl.reviewer = clx.reviewer
1129			dirty[cl] = True
1130		if clx.cc is not None:
1131			cl.cc = clx.cc
1132			dirty[cl] = True
1133		if clx.files is not None:
1134			cl.files = clx.files
1135			dirty[cl] = True
1136		if clx.private != cl.private:
1137			cl.private = clx.private
1138			dirty[cl] = True
1139
1140	if not opts["stdin"] and not opts["stdout"]:
1141		if name == "new":
1142			cl.files = files
1143		err = EditCL(ui, repo, cl)
1144		if err != "":
1145			return err
1146		dirty[cl] = True
1147
1148	for d, _ in dirty.items():
1149		name = d.name
1150		d.Flush(ui, repo)
1151		if name == "new":
1152			d.Upload(ui, repo, quiet=True)
1153
1154	if opts["stdout"]:
1155		ui.write(cl.EditorText())
1156	elif opts["pending"]:
1157		ui.write(cl.PendingText())
1158	elif name == "new":
1159		if ui.quiet:
1160			ui.write(cl.name)
1161		else:
1162			ui.write("CL created: " + cl.url + "\n")
1163	return
1164
1165def code_login(ui, repo, **opts):
1166	"""log in to code review server
1167
1168	Logs in to the code review server, saving a cookie in
1169	a file in your home directory.
1170	"""
1171	if missing_codereview:
1172		return missing_codereview
1173
1174	MySend(None)
1175
1176def clpatch(ui, repo, clname, **opts):
1177	"""import a patch from the code review server
1178
1179	Imports a patch from the code review server into the local client.
1180	If the local client has already modified any of the files that the
1181	patch modifies, this command will refuse to apply the patch.
1182
1183	Submitting an imported patch will keep the original author's
1184	name as the Author: line but add your own name to a Committer: line.
1185	"""
1186	if repo[None].branch() != "default":
1187		return "cannot run hg clpatch outside default branch"
1188	return clpatch_or_undo(ui, repo, clname, opts, mode="clpatch")
1189
1190def undo(ui, repo, clname, **opts):
1191	"""undo the effect of a CL
1192	
1193	Creates a new CL that undoes an earlier CL.
1194	After creating the CL, opens the CL text for editing so that
1195	you can add the reason for the undo to the description.
1196	"""
1197	if repo[None].branch() != "default":
1198		return "cannot run hg undo outside default branch"
1199	return clpatch_or_undo(ui, repo, clname, opts, mode="undo")
1200
1201def release_apply(ui, repo, clname, **opts):
1202	"""apply a CL to the release branch
1203
1204	Creates a new CL copying a previously committed change
1205	from the main branch to the release branch.
1206	The current client must either be clean or already be in
1207	the release branch.
1208	
1209	The release branch must be created by starting with a
1210	clean client, disabling the code review plugin, and running:
1211	
1212		hg update weekly.YYYY-MM-DD
1213		hg branch release-branch.rNN
1214		hg commit -m 'create release-branch.rNN'
1215		hg push --new-branch
1216	
1217	Then re-enable the code review plugin.
1218	
1219	People can test the release branch by running
1220	
1221		hg update release-branch.rNN
1222	
1223	in a clean client.  To return to the normal tree,
1224	
1225		hg update default
1226	
1227	Move changes since the weekly into the release branch 
1228	using hg release-apply followed by the usual code review
1229	process and hg submit.
1230
1231	When it comes time to tag the release, record the
1232	final long-form tag of the release-branch.rNN
1233	in the *default* branch's .hgtags file.  That is, run
1234	
1235		hg update default
1236	
1237	and then edit .hgtags as you would for a weekly.
1238		
1239	"""
1240	c = repo[None]
1241	if not releaseBranch:
1242		return "no active release branches"
1243	if c.branch() != releaseBranch:
1244		if c.modified() or c.added() or c.removed():
1245			raise util.Abort("uncommitted local changes - cannot switch branches")
1246		err = hg.clean(repo, releaseBranch)
1247		if err:
1248			return err
1249	try:
1250		err = clpatch_or_undo(ui, repo, clname, opts, mode="backport")
1251		if err:
1252			raise util.Abort(err)
1253	except Exception, e:
1254		hg.clean(repo, "default")
1255		raise e
1256	return None
1257
1258def rev2clname(rev):
1259	# Extract CL name from revision description.
1260	# The last line in the description that is a codereview URL is the real one.
1261	# Earlier lines might be part of the user-written description.
1262	all = re.findall('(?m)^http://codereview.appspot.com/([0-9]+)$', rev.description())
1263	if len(all) > 0:
1264		return all[-1]
1265	return ""
1266
1267undoHeader = """undo CL %s / %s
1268
1269<enter reason for undo>
1270
1271««« original CL description
1272"""
1273
1274undoFooter = """
1275»»»
1276"""
1277
1278backportHeader = """[%s] %s
1279
1280««« CL %s / %s
1281"""
1282
1283backportFooter = """
1284»»»
1285"""
1286
1287# Implementation of clpatch/undo.
1288def clpatch_or_undo(ui, repo, clname, opts, mode):
1289	if missing_codereview:
1290		return missing_codereview
1291
1292	if mode == "undo" or mode == "backport":
1293		if hgversion < '1.4':
1294			# Don't have cmdutil.match (see implementation of sync command).
1295			return "hg is too old to run hg %s - update to 1.4 or newer" % mode
1296
1297		# Find revision in Mercurial repository.
1298		# Assume CL number is 7+ decimal digits.
1299		# Otherwise is either change log sequence number (fewer decimal digits),
1300		# hexadecimal hash, or tag name.
1301		# Mercurial will fall over long before the change log
1302		# sequence numbers get to be 7 digits long.
1303		if re.match('^[0-9]{7,}$', clname):
1304			found = False
1305			matchfn = scmutil.match(repo, [], {'rev': None})
1306			def prep(ctx, fns):
1307				pass
1308			for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev': None}, prep):
1309				rev = repo[ctx.rev()]
1310				# Last line with a code review URL is the actual review URL.
1311				# Earlier ones might be part of the CL description.
1312				n = rev2clname(rev)
1313				if n == clname:
1314					found = True
1315					break
1316			if not found:
1317				return "cannot find CL %s in local repository" % clname
1318		else:
1319			rev = repo[clname]
1320			if not rev:
1321				return "unknown revision %s" % clname
1322			clname = rev2clname(rev)
1323			if clname == "":
1324				return "cannot find CL name in revision description"
1325		
1326		# Create fresh CL and start with patch that would reverse the change.
1327		vers = short(rev.node())
1328		cl = CL("new")
1329		desc = str(rev.description())
1330		if mode == "undo":
1331			cl.desc = (undoHeader % (clname, vers)) + desc + undoFooter
1332		else:
1333			cl.desc = (backportHeader % (releaseBranch, line1(desc), clname, vers)) + desc + undoFooter
1334		v1 = vers
1335		v0 = short(rev.parents()[0].node())
1336		if mode == "undo":
1337			arg = v1 + ":" + v0
1338		else:
1339			vers = v0
1340			arg = v0 + ":" + v1
1341		patch = RunShell(["hg", "diff", "--git", "-r", arg])
1342
1343	else:  # clpatch
1344		cl, vers, patch, err = DownloadCL(ui, repo, clname)
1345		if err != "":
1346			return err
1347		if patch == emptydiff:
1348			return "codereview issue %s has no diff" % clname
1349
1350	# find current hg version (hg identify)
1351	ctx = repo[None]
1352	parents = ctx.parents()
1353	id = '+'.join([short(p.node()) for p in parents])
1354
1355	# if version does not match the patch version,
1356	# try to update the patch line numbers.
1357	if vers != "" and id != vers:
1358		# "vers in repo" gives the wrong answer
1359		# on some versions of Mercurial.  Instead, do the actual
1360		# lookup and catch the exception.
1361		try:
1362			repo[vers].description()
1363		except:
1364			return "local repository is out of date; sync to get %s" % (vers)
1365		patch1, err = portPatch(repo, patch, vers, id)
1366		if err != "":
1367			if not opts["ignore_hgpatch_failure"]:
1368				return "codereview issue %s is out of date: %s (%s->%s)" % (clname, err, vers, id)
1369		else:
1370			patch = patch1
1371	argv = ["hgpatch"]
1372	if opts["no_incoming"] or mode == "backport":
1373		argv += ["--checksync=false"]
1374	try:
1375		cmd = subprocess.Popen(argv, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, close_fds=sys.platform != "win32")
1376	except:
1377		return "hgpatch: " + ExceptionDetail()
1378
1379	out, err = cmd.communicate(patch)
1380	if cmd.returncode != 0 and not opts["ignore_hgpatch_failure"]:
1381		return "hgpatch failed"
1382	cl.local = True
1383	cl.files = out.strip().split()
1384	if not cl.files and not opts["ignore_hgpatch_failure"]:
1385		return "codereview issue %s has no changed files" % clname
1386	files = ChangedFiles(ui, repo, [], opts)
1387	extra = Sub(cl.files, files)
1388	if extra:
1389		ui.warn("warning: these files were listed in the patch but not changed:\n\t" + "\n\t".join(extra) + "\n")
1390	cl.Flush(ui, repo)
1391	if mode == "undo":
1392		err = EditCL(ui, repo, cl)
1393		if err != "":
1394			return "CL created, but error editing: " + err
1395		cl.Flush(ui, repo)
1396	else:
1397		ui.write(cl.PendingText() + "\n")
1398
1399# portPatch rewrites patch from being a patch against
1400# oldver to being a patch against newver.
1401def portPatch(repo, patch, oldver, newver):
1402	lines = patch.splitlines(True) # True = keep \n
1403	delta = None
1404	for i in range(len(lines)):
1405		line = lines[i]
1406		if line.startswith('--- a/'):
1407			file = line[6:-1]
1408			delta = fileDeltas(repo, file, oldver, newver)
1409		if not delta or not line.startswith('@@ '):
1410			continue
1411		# @@ -x,y +z,w @@ means the patch chunk replaces
1412		# the original file's line numbers x up to x+y with the
1413		# line numbers z up to z+w in the new file.
1414		# Find the delta from x in the original to the same
1415		# line in the current version and add that delta to both
1416		# x and z.
1417		m = re.match('@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@', line)
1418		if not m:
1419			return None, "error parsing patch line numbers"
1420		n1, len1, n2, len2 = int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
1421		d, err = lineDelta(delta, n1, len1)
1422		if err != "":
1423			return "", err
1424		n1 += d
1425		n2 += d
1426		lines[i] = "@@ -%d,%d +%d,%d @@\n" % (n1, len1, n2, len2)
1427		
1428	newpatch = ''.join(lines)
1429	return newpatch, ""
1430
1431# fileDelta returns the line number deltas for the given file's
1432# changes from oldver to newver.
1433# The deltas are a list of (n, len, newdelta) triples that say
1434# lines [n, n+len) were modified, and after that range the
1435# line numbers are +newdelta from what they were before.
1436def fileDeltas(repo, file, oldver, newver):
1437	cmd = ["hg", "diff", "--git", "-r", oldver + ":" + newver, "path:" + file]
1438	data = RunShell(cmd, silent_ok=True)
1439	deltas = []
1440	for line in data.splitlines():
1441		m = re.match('@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@', line)
1442		if not m:
1443			continue
1444		n1, len1, n2, len2 = int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
1445		deltas.append((n1, len1, n2+len2-(n1+len1)))
1446	return deltas
1447
1448# lineDelta finds the appropriate line number delta to apply to the lines [n, n+len).
1449# It returns an error if those lines were rewritten by the patch.
1450def lineDelta(deltas, n, len):
1451	d = 0
1452	for (old, oldlen, newdelta) in deltas:
1453		if old >= n+len:
1454			break
1455		if old+len > n:
1456			return 0, "patch and recent changes conflict"
1457		d = newdelta
1458	return d, ""
1459
1460def download(ui, repo, clname, **opts):
1461	"""download a change from the code review server
1462
1463	Download prints a description of the given change list
1464	followed by its diff, downloaded from the code review server.
1465	"""
1466	if missing_codereview:
1467		return missing_codereview
1468
1469	cl, vers, patch, err = DownloadCL(ui, repo, clname)
1470	if err != "":
1471		return err
1472	ui.write(cl.EditorText() + "\n")
1473	ui.write(patch + "\n")
1474	return
1475
1476def file(ui, repo, clname, pat, *pats, **opts):
1477	"""assign files to or remove files from a change list
1478
1479	Assign files to or (with -d) remove files from a change list.
1480
1481	The -d option only removes files from the change list.
1482	It does not edit them or remove them from the repository.
1483	"""
1484	if missing_codereview:
1485		return missing_codereview
1486
1487	pats = tuple([pat] + list(pats))
1488	if not GoodCLName(clname):
1489		return "invalid CL name " + clname
1490
1491	dirty = {}
1492	cl, err = LoadCL(ui, repo, clname, web=False)
1493	if err != '':
1494		return err
1495	if not cl.local:
1496		return "cannot change non-local CL " + clname
1497
1498	files = ChangedFiles(ui, repo, pats, opts)
1499
1500	if opts["delete"]:
1501		oldfiles = Intersect(files, cl.files)
1502		if oldfiles:
1503			if not ui.quiet:
1504				ui.status("# Removing files from CL.  To undo:\n")
1505				ui.status("#	cd %s\n" % (repo.root))
1506				for f in oldfiles:
1507					ui.status("#	hg file %s %s\n" % (cl.name, f))
1508			cl.files = Sub(cl.files, oldfiles)
1509			cl.Flush(ui, repo)
1510		else:
1511			ui.status("no such files in CL")
1512		return
1513
1514	if not files:
1515		return "no such modified files"
1516
1517	files = Sub(files, cl.files)
1518	taken = Taken(ui, repo)
1519	warned = False
1520	for f in files:
1521		if f in taken:
1522			if not warned and not ui.quiet:
1523				ui.status("# Taking files from other CLs.  To undo:\n")
1524				ui.status("#	cd %s\n" % (repo.root))
1525				warned = True
1526			ocl = taken[f]
1527			if not ui.quiet:
1528				ui.status("#	hg file %s %s\n" % (ocl.name, f))
1529			if ocl not in dirty:
1530				ocl.files = Sub(ocl.files, files)
1531				dirty[ocl] = True
1532	cl.files = Add(cl.files, files)
1533	dirty[cl] = True
1534	for d, _ in dirty.items():
1535		d.Flush(ui, repo)
1536	return
1537
1538def gofmt(ui, repo, *pats, **opts):
1539	"""apply gofmt to modified files
1540
1541	Applies gofmt to the modified files in the repository that match
1542	the given patterns.
1543	"""
1544	if missing_codereview:
1545		return missing_codereview
1546
1547	files = ChangedExistingFiles(ui, repo, pats, opts)
1548	files = [f for f in files if f.endswith(".go")]
1549	if not files:
1550		return "no modified go files"
1551	cwd = os.getcwd()
1552	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
1553	try:
1554		cmd = ["gofmt", "-l"]
1555		if not opts["list"]:
1556			cmd += ["-w"]
1557		if os.spawnvp(os.P_WAIT, "gofmt", cmd + files) != 0:
1558			raise util.Abort("gofmt did not exit cleanly")
1559	except error.Abort, e:
1560		raise
1561	except:
1562		raise util.Abort("gofmt: " + ExceptionDetail())
1563	return
1564
1565def mail(ui, repo, *pats, **opts):
1566	"""mail a change for review
1567
1568	Uploads a patch to the code review server and then sends mail
1569	to the reviewer and CC list asking for a review.
1570	"""
1571	if missing_codereview:
1572		return missing_codereview
1573
1574	cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc)
1575	if err != "":
1576		return err
1577	cl.Upload(ui, repo, gofmt_just_warn=True)
1578	if not cl.reviewer:
1579		# If no reviewer is listed, assign the review to defaultcc.
1580		# This makes sure that it appears in the 
1581		# codereview.appspot.com/user/defaultcc
1582		# page, so that it doesn't get dropped on the floor.
1583		if not defaultcc:
1584			return "no reviewers listed in CL"
1585		cl.cc = Sub(cl.cc, defaultcc)
1586		cl.reviewer = defaultcc
1587		cl.Flush(ui, repo)
1588
1589	if cl.files == []:
1590		return "no changed files, not sending mail"
1591
1592	cl.Mail(ui, repo)		
1593
1594def pending(ui, repo, *pats, **opts):
1595	"""show pending changes
1596
1597	Lists pending changes followed by a list of unassigned but modified files.
1598	"""
1599	if missing_codereview:
1600		return missing_codereview
1601
1602	m = LoadAllCL(ui, repo, web=True)
1603	names = m.keys()
1604	names.sort()
1605	for name in names:
1606		cl = m[name]
1607		ui.write(cl.PendingText() + "\n")
1608
1609	files = DefaultFiles(ui, repo, [], opts)
1610	if len(files) > 0:
1611		s = "Changed files not in any CL:\n"
1612		for f in files:
1613			s += "\t" + f + "\n"
1614		ui.write(s)
1615
1616def reposetup(ui, repo):
1617	global original_match
1618	if original_match is None:
1619		start_status_thread()
1620		original_match = scmutil.match
1621		scmutil.match = ReplacementForCmdutilMatch
1622		RietveldSetup(ui, repo)
1623
1624def CheckContributor(ui, repo, user=None):
1625	set_status("checking CONTRIBUTORS file")
1626	user, userline = FindContributor(ui, repo, user, warn=False)
1627	if not userline:
1628		raise util.Abort("cannot find %s in CONTRIBUTORS" % (user,))
1629	return userline
1630
1631def FindContributor(ui, repo, user=None, warn=True):
1632	if not user:
1633		user = ui.config("ui", "username")
1634		if not user:
1635			raise util.Abort("[ui] username is not configured in .hgrc")
1636	user = user.lower()
1637	m = re.match(r".*<(.*)>", user)
1638	if m:
1639		user = m.group(1)
1640
1641	if user not in contributors:
1642		if warn:
1643			ui.warn("warning: cannot find %s in CONTRIBUTORS\n" % (user,))
1644		return user, None
1645	
1646	user, email = contributors[user]
1647	return email, "%s <%s>" % (user, email)
1648
1649def submit(ui, repo, *pats, **opts):
1650	"""submit change to remote repository
1651
1652	Submits change to remote repository.
1653	Bails out if the local repository is not in sync with the remote one.
1654	"""
1655	if missing_codereview:
1656		return missing_codereview
1657
1658	# We already called this on startup but sometimes Mercurial forgets.
1659	set_mercurial_encoding_to_utf8()
1660
1661	repo.ui.quiet = True
1662	if not opts["no_incoming"] and Incoming(ui, repo, opts):
1663		return "local repository out of date; must sync before submit"
1664
1665	cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc)
1666	if err != "":
1667		return err
1668
1669	user = None
1670	if cl.copied_from:
1671		user = cl.copied_from
1672	userline = CheckContributor(ui, repo, user)
1673	typecheck(userline, str)
1674
1675	about = ""
1676	if cl.reviewer:
1677		about += "R=" + JoinComma([CutDomain(s) for s in cl.reviewer]) + "\n"
1678	if opts.get('tbr'):
1679		tbr = SplitCommaSpace(opts.get('tbr'))
1680		cl.reviewer = Add(cl.reviewer, tbr)
1681		about += "TBR=" + JoinComma([CutDomain(s) for s in tbr]) + "\n"
1682	if cl.cc:
1683		about += "CC=" + JoinComma([CutDomain(s) for s in cl.cc]) + "\n"
1684
1685	if not cl.reviewer:
1686		return "no reviewers listed in CL"
1687
1688	if not cl.local:
1689		return "cannot submit non-local CL"
1690
1691	# upload, to sync current patch and also get change number if CL is new.
1692	if not cl.copied_from:
1693		cl.Upload(ui, repo, gofmt_just_warn=True)
1694
1695	# check gofmt for real; allowed upload to warn in order to save CL.
1696	cl.Flush(ui, repo)
1697	CheckFormat(ui, repo, cl.files)
1698
1699	about += "%s%s\n" % (server_url_base, cl.name)
1700
1701	if cl.copied_from:
1702		about += "\nCommitter: " + CheckContributor(ui, repo, None) + "\n"
1703	typecheck(about, str)
1704
1705	if not cl.mailed and not cl.copied_from:

Large files files are truncated, but you can click here to view the full file