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