blob: bd9a179f82e1f705af1e88397db986d7fe84cbc8 [file] [log] [blame]
Russ Cox3b6ddd92010-11-04 13:58:32 -04001# 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
Russ Cox79a63722009-10-22 11:12:39 -07005# 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#
Russ Coxf7d87f32010-08-26 18:56:29 -040011# http://www.apache.org/licenses/LICENSE-2.0
Russ Cox79a63722009-10-22 11:12:39 -070012#
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
Russ Cox790c9b52009-11-05 14:44:57 -080021To configure, set the following options in
Russ Cox79a63722009-10-22 11:12:39 -070022your repository's .hg/hgrc file.
23
Russ Cox790c9b52009-11-05 14:44:57 -080024 [extensions]
25 codereview = path/to/codereview.py
Russ Cox79a63722009-10-22 11:12:39 -070026
Russ Cox790c9b52009-11-05 14:44:57 -080027 [codereview]
Russ Cox45495242009-11-01 05:49:35 -080028 server = codereview.appspot.com
Russ Cox79a63722009-10-22 11:12:39 -070029
Russ Cox45495242009-11-01 05:49:35 -080030The server should be running Rietveld; see http://code.google.com/p/rietveld/.
Russ Cox790c9b52009-11-05 14:44:57 -080031
32In addition to the new commands, this extension introduces
33the file pattern syntax @nnnnnn, where nnnnnn is a change list
Russ Cox72a59ce2009-11-07 17:30:40 -080034number, to mean the files included in that change list, which
Russ Cox790c9b52009-11-05 14:44:57 -080035must 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".
Russ Cox79a63722009-10-22 11:12:39 -070039'''
40
41from mercurial import cmdutil, commands, hg, util, error, match
42from mercurial.node import nullrev, hex, nullid, short
Russ Cox7db2c792009-11-17 23:23:18 -080043import os, re, time
Russ Cox79a63722009-10-22 11:12:39 -070044import stat
Russ Cox790c9b52009-11-05 14:44:57 -080045import subprocess
Russ Cox45495242009-11-01 05:49:35 -080046import threading
Russ Cox79a63722009-10-22 11:12:39 -070047from HTMLParser import HTMLParser
Devon H. O'Dell742221d2009-12-02 01:16:38 -080048try:
49 from xml.etree import ElementTree as ET
50except:
Devon H. O'Delle9a8ab02009-12-02 08:18:26 -080051 from elementtree import ElementTree as ET
Russ Cox79a63722009-10-22 11:12:39 -070052
53try:
54 hgversion = util.version()
Russ Cox45495242009-11-01 05:49:35 -080055except:
Russ Cox79a63722009-10-22 11:12:39 -070056 from mercurial.version import version as v
57 hgversion = v.get_version()
58
Russ Cox08e65f72010-07-16 18:54:38 -070059try:
60 from mercurial.discovery import findcommonincoming
61except:
62 def findcommonincoming(repo, remote):
63 return repo.findcommonincoming(remote)
64
Russ Cox72a59ce2009-11-07 17:30:40 -080065oldMessage = """
66The code review extension requires Mercurial 1.3 or newer.
67
68To install a new Mercurial,
69
70 sudo easy_install mercurial
71
72works on most systems.
73"""
74
75linuxMessage = """
76You may need to clear your current Mercurial installation by running:
77
78 sudo apt-get remove mercurial mercurial-common
79 sudo rm -rf /etc/mercurial
80"""
81
82if hgversion < '1.3':
83 msg = oldMessage
84 if os.access("/etc/mercurial", 0):
85 msg += linuxMessage
86 raise util.Abort(msg)
Russ Cox79a63722009-10-22 11:12:39 -070087
Russ Coxff7343f2010-01-20 09:49:35 -080088def promptyesno(ui, msg):
89 # Arguments to ui.prompt changed between 1.3 and 1.3.1.
90 # Even so, some 1.3.1 distributions seem to have the old prompt!?!?
91 # What a terrible way to maintain software.
92 try:
93 return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0
94 except AttributeError:
95 return ui.prompt(msg, ["&yes", "&no"], "y") != "n"
96
Russ Cox45495242009-11-01 05:49:35 -080097# To experiment with Mercurial in the python interpreter:
Russ Cox79a63722009-10-22 11:12:39 -070098# >>> repo = hg.repository(ui.ui(), path = ".")
99
100#######################################################################
101# Normally I would split this into multiple files, but it simplifies
102# import path headaches to keep it all in one file. Sorry.
103
104import sys
105if __name__ == "__main__":
106 print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly."
107 sys.exit(2)
108
Russ Cox82747422009-12-15 13:36:05 -0800109server = "codereview.appspot.com"
110server_url_base = None
Russ Cox84ac3572010-01-13 09:09:06 -0800111defaultcc = None
Russ Cox93f614f2010-06-30 23:34:11 -0700112contributors = {}
Russ Coxe6308652010-08-25 17:52:25 -0400113missing_codereview = None
Russ Cox82747422009-12-15 13:36:05 -0800114
Russ Cox79a63722009-10-22 11:12:39 -0700115#######################################################################
Russ Cox3b6ddd92010-11-04 13:58:32 -0400116# RE: UNICODE STRING HANDLING
117#
118# Python distinguishes between the str (string of bytes)
119# and unicode (string of code points) types. Most operations
120# work on either one just fine, but some (like regexp matching)
121# require unicode, and others (like write) require str.
122#
123# As befits the language, Python hides the distinction between
124# unicode and str by converting between them silently, but
125# *only* if all the bytes/code points involved are 7-bit ASCII.
126# This means that if you're not careful, your program works
127# fine on "hello, world" and fails on "hello, 世界". And of course,
128# the obvious way to be careful - use static types - is unavailable.
129# So the only way is trial and error to find where to put explicit
130# conversions.
131#
132# Because more functions do implicit conversion to str (string of bytes)
133# than do implicit conversion to unicode (string of code points),
134# the convention in this module is to represent all text as str,
135# converting to unicode only when calling a unicode-only function
136# and then converting back to str as soon as possible.
137
138def typecheck(s, t):
139 if type(s) != t:
140 raise util.Abort("type check failed: %s has type %s != %s" % (repr(s), type(s), t))
141
Russ Coxdc9a02f2011-02-01 14:17:41 -0500142# If we have to pass unicode instead of str, ustr does that conversion clearly.
143def ustr(s):
144 typecheck(s, str)
145 return s.decode("utf-8")
146
147# Even with those, Mercurial still sometimes turns unicode into str
148# and then tries to use it as ascii. Change Mercurial's default.
149def set_mercurial_encoding_to_utf8():
150 from mercurial import encoding
151 encoding.encoding = 'utf-8'
152
153set_mercurial_encoding_to_utf8()
154
155# Even with those we still run into problems.
156# I tried to do things by the book but could not convince
157# Mercurial to let me check in a change with UTF-8 in the
158# CL description or author field, no matter how many conversions
159# between str and unicode I inserted and despite changing the
160# default encoding. I'm tired of this game, so set the default
161# encoding for all of Python to 'utf-8', not 'ascii'.
162def default_to_utf8():
163 import sys
164 reload(sys) # site.py deleted setdefaultencoding; get it back
165 sys.setdefaultencoding('utf-8')
166
167default_to_utf8()
Russ Cox3b6ddd92010-11-04 13:58:32 -0400168
169#######################################################################
Russ Cox79a63722009-10-22 11:12:39 -0700170# Change list parsing.
171#
172# Change lists are stored in .hg/codereview/cl.nnnnnn
173# where nnnnnn is the number assigned by the code review server.
174# Most data about a change list is stored on the code review server
175# too: the description, reviewer, and cc list are all stored there.
176# The only thing in the cl.nnnnnn file is the list of relevant files.
177# Also, the existence of the cl.nnnnnn file marks this repository
178# as the one where the change list lives.
179
Russ Cox82747422009-12-15 13:36:05 -0800180emptydiff = """Index: ~rietveld~placeholder~
181===================================================================
182diff --git a/~rietveld~placeholder~ b/~rietveld~placeholder~
183new file mode 100644
184"""
185
Russ Cox79a63722009-10-22 11:12:39 -0700186class CL(object):
187 def __init__(self, name):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400188 typecheck(name, str)
Russ Cox79a63722009-10-22 11:12:39 -0700189 self.name = name
190 self.desc = ''
191 self.files = []
192 self.reviewer = []
193 self.cc = []
194 self.url = ''
195 self.local = False
196 self.web = False
Russ Cox752b1702010-01-09 09:47:14 -0800197 self.copied_from = None # None means current user
Russ Cox15947302010-01-07 18:23:30 -0800198 self.mailed = False
Russ Cox79a63722009-10-22 11:12:39 -0700199
200 def DiskText(self):
201 cl = self
202 s = ""
Russ Cox752b1702010-01-09 09:47:14 -0800203 if cl.copied_from:
204 s += "Author: " + cl.copied_from + "\n\n"
Russ Cox15947302010-01-07 18:23:30 -0800205 s += "Mailed: " + str(self.mailed) + "\n"
Russ Cox79a63722009-10-22 11:12:39 -0700206 s += "Description:\n"
207 s += Indent(cl.desc, "\t")
208 s += "Files:\n"
209 for f in cl.files:
210 s += "\t" + f + "\n"
Russ Cox3b6ddd92010-11-04 13:58:32 -0400211 typecheck(s, str)
Russ Cox79a63722009-10-22 11:12:39 -0700212 return s
213
214 def EditorText(self):
215 cl = self
216 s = _change_prolog
217 s += "\n"
Russ Cox752b1702010-01-09 09:47:14 -0800218 if cl.copied_from:
219 s += "Author: " + cl.copied_from + "\n"
Russ Cox79a63722009-10-22 11:12:39 -0700220 if cl.url != '':
221 s += 'URL: ' + cl.url + ' # cannot edit\n\n'
222 s += "Reviewer: " + JoinComma(cl.reviewer) + "\n"
223 s += "CC: " + JoinComma(cl.cc) + "\n"
224 s += "\n"
225 s += "Description:\n"
226 if cl.desc == '':
227 s += "\t<enter description here>\n"
228 else:
229 s += Indent(cl.desc, "\t")
230 s += "\n"
Russ Cox790c9b52009-11-05 14:44:57 -0800231 if cl.local or cl.name == "new":
232 s += "Files:\n"
233 for f in cl.files:
234 s += "\t" + f + "\n"
235 s += "\n"
Russ Cox3b6ddd92010-11-04 13:58:32 -0400236 typecheck(s, str)
Russ Cox79a63722009-10-22 11:12:39 -0700237 return s
Russ Cox45495242009-11-01 05:49:35 -0800238
Russ Cox79a63722009-10-22 11:12:39 -0700239 def PendingText(self):
240 cl = self
241 s = cl.name + ":" + "\n"
242 s += Indent(cl.desc, "\t")
243 s += "\n"
Russ Cox752b1702010-01-09 09:47:14 -0800244 if cl.copied_from:
245 s += "\tAuthor: " + cl.copied_from + "\n"
Russ Cox79a63722009-10-22 11:12:39 -0700246 s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n"
247 s += "\tCC: " + JoinComma(cl.cc) + "\n"
248 s += "\tFiles:\n"
249 for f in cl.files:
250 s += "\t\t" + f + "\n"
Russ Cox3b6ddd92010-11-04 13:58:32 -0400251 typecheck(s, str)
Russ Cox79a63722009-10-22 11:12:39 -0700252 return s
Russ Cox45495242009-11-01 05:49:35 -0800253
Russ Cox79a63722009-10-22 11:12:39 -0700254 def Flush(self, ui, repo):
255 if self.name == "new":
Russ Coxfdb46fb2011-02-02 16:39:31 -0500256 self.Upload(ui, repo, gofmt_just_warn=True, creating=True)
Russ Cox79a63722009-10-22 11:12:39 -0700257 dir = CodeReviewDir(ui, repo)
258 path = dir + '/cl.' + self.name
259 f = open(path+'!', "w")
260 f.write(self.DiskText())
261 f.close()
Hector Chu31645cc2009-12-13 12:21:44 -0800262 if sys.platform == "win32" and os.path.isfile(path):
263 os.remove(path)
Russ Cox79a63722009-10-22 11:12:39 -0700264 os.rename(path+'!', path)
Russ Cox752b1702010-01-09 09:47:14 -0800265 if self.web and not self.copied_from:
Russ Coxdde666d2009-11-01 18:46:07 -0800266 EditDesc(self.name, desc=self.desc,
Russ Cox79a63722009-10-22 11:12:39 -0700267 reviewers=JoinComma(self.reviewer), cc=JoinComma(self.cc))
Russ Cox45495242009-11-01 05:49:35 -0800268
Russ Cox79a63722009-10-22 11:12:39 -0700269 def Delete(self, ui, repo):
270 dir = CodeReviewDir(ui, repo)
271 os.unlink(dir + "/cl." + self.name)
272
Russ Coxd8e0d9a2009-11-04 23:43:55 -0800273 def Subject(self):
Russ Coxdde666d2009-11-01 18:46:07 -0800274 s = line1(self.desc)
Russ Cox9c132152009-11-02 11:37:21 -0800275 if len(s) > 60:
276 s = s[0:55] + "..."
Russ Coxdde666d2009-11-01 18:46:07 -0800277 if self.name != "new":
Russ Cox9c132152009-11-02 11:37:21 -0800278 s = "code review %s: %s" % (self.name, s)
Russ Cox3b6ddd92010-11-04 13:58:32 -0400279 typecheck(s, str)
Russ Coxdde666d2009-11-01 18:46:07 -0800280 return s
281
Russ Coxfdb46fb2011-02-02 16:39:31 -0500282 def Upload(self, ui, repo, send_mail=False, gofmt=True, gofmt_just_warn=False, creating=False, quiet=False):
283 if not self.files and not creating:
Russ Cox82747422009-12-15 13:36:05 -0800284 ui.warn("no files in change list\n")
Russ Cox043486e2009-11-06 09:45:24 -0800285 if ui.configbool("codereview", "force_gofmt", True) and gofmt:
Russ Cox49bff2d2010-10-06 18:10:23 -0400286 CheckFormat(ui, repo, self.files, just_warn=gofmt_just_warn)
Russ Coxe3ac0b52010-08-26 16:27:42 -0400287 set_status("uploading CL metadata + diffs")
Russ Cox79a63722009-10-22 11:12:39 -0700288 os.chdir(repo.root)
289 form_fields = [
290 ("content_upload", "1"),
291 ("reviewers", JoinComma(self.reviewer)),
292 ("cc", JoinComma(self.cc)),
293 ("description", self.desc),
294 ("base_hashes", ""),
Russ Cox79a63722009-10-22 11:12:39 -0700295 ]
296
Russ Cox79a63722009-10-22 11:12:39 -0700297 if self.name != "new":
298 form_fields.append(("issue", self.name))
Russ Cox82747422009-12-15 13:36:05 -0800299 vcs = None
Russ Coxfdb46fb2011-02-02 16:39:31 -0500300 # We do not include files when creating the issue,
301 # because we want the patch sets to record the repository
302 # and base revision they are diffs against. We use the patch
303 # set message for that purpose, but there is no message with
304 # the first patch set. Instead the message gets used as the
305 # new CL's overall subject. So omit the diffs when creating
306 # and then we'll run an immediate upload.
307 # This has the effect that every CL begins with an empty "Patch set 1".
308 if self.files and not creating:
Russ Coxf7d87f32010-08-26 18:56:29 -0400309 vcs = MercurialVCS(upload_options, ui, repo)
Russ Cox82747422009-12-15 13:36:05 -0800310 data = vcs.GenerateDiff(self.files)
311 files = vcs.GetBaseFiles(data)
312 if len(data) > MAX_UPLOAD_SIZE:
313 uploaded_diff_file = []
314 form_fields.append(("separate_patches", "1"))
315 else:
316 uploaded_diff_file = [("data", "data.diff", data)]
Russ Cox79a63722009-10-22 11:12:39 -0700317 else:
Russ Cox82747422009-12-15 13:36:05 -0800318 uploaded_diff_file = [("data", "data.diff", emptydiff)]
Russ Coxfdb46fb2011-02-02 16:39:31 -0500319
320 if vcs and self.name != "new":
321 form_fields.append(("subject", "diff -r " + vcs.base_rev + " " + getremote(ui, repo, {}).path))
322 else:
323 # First upload sets the subject for the CL itself.
324 form_fields.append(("subject", self.Subject()))
Russ Cox79a63722009-10-22 11:12:39 -0700325 ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file)
326 response_body = MySend("/upload", body, content_type=ctype)
327 patchset = None
328 msg = response_body
329 lines = msg.splitlines()
330 if len(lines) >= 2:
331 msg = lines[0]
332 patchset = lines[1].strip()
333 patches = [x.split(" ", 1) for x in lines[2:]]
Russ Coxfdb46fb2011-02-02 16:39:31 -0500334 if response_body.startswith("Issue updated.") and quiet:
335 pass
336 else:
337 ui.status(msg + "\n")
Russ Coxe3ac0b52010-08-26 16:27:42 -0400338 set_status("uploaded CL metadata + diffs")
Russ Cox79a63722009-10-22 11:12:39 -0700339 if not response_body.startswith("Issue created.") and not response_body.startswith("Issue updated."):
Russ Cox506ce112009-11-04 03:15:24 -0800340 raise util.Abort("failed to update issue: " + response_body)
Russ Cox79a63722009-10-22 11:12:39 -0700341 issue = msg[msg.rfind("/")+1:]
342 self.name = issue
Russ Cox45495242009-11-01 05:49:35 -0800343 if not self.url:
344 self.url = server_url_base + self.name
Russ Cox79a63722009-10-22 11:12:39 -0700345 if not uploaded_diff_file:
Russ Coxe3ac0b52010-08-26 16:27:42 -0400346 set_status("uploading patches")
Russ Cox79a63722009-10-22 11:12:39 -0700347 patches = UploadSeparatePatches(issue, rpc, patchset, data, upload_options)
Russ Cox82747422009-12-15 13:36:05 -0800348 if vcs:
Russ Coxe3ac0b52010-08-26 16:27:42 -0400349 set_status("uploading base files")
Russ Cox82747422009-12-15 13:36:05 -0800350 vcs.UploadBaseFiles(issue, rpc, patches, patchset, upload_options, files)
Russ Cox79a63722009-10-22 11:12:39 -0700351 if send_mail:
Russ Coxe3ac0b52010-08-26 16:27:42 -0400352 set_status("sending mail")
Russ Cox79a63722009-10-22 11:12:39 -0700353 MySend("/" + issue + "/mail", payload="")
354 self.web = True
Russ Coxe3ac0b52010-08-26 16:27:42 -0400355 set_status("flushing changes to disk")
Russ Cox79a63722009-10-22 11:12:39 -0700356 self.Flush(ui, repo)
357 return
358
Russ Coxfdb46fb2011-02-02 16:39:31 -0500359 def Mail(self, ui, repo):
Russ Cox15947302010-01-07 18:23:30 -0800360 pmsg = "Hello " + JoinComma(self.reviewer)
361 if self.cc:
362 pmsg += " (cc: %s)" % (', '.join(self.cc),)
363 pmsg += ",\n"
364 pmsg += "\n"
Russ Coxfdb46fb2011-02-02 16:39:31 -0500365 repourl = getremote(ui, repo, {}).path
Russ Cox15947302010-01-07 18:23:30 -0800366 if not self.mailed:
Russ Coxfdb46fb2011-02-02 16:39:31 -0500367 pmsg += "I'd like you to review this change to\n" + repourl + "\n"
Russ Cox15947302010-01-07 18:23:30 -0800368 else:
369 pmsg += "Please take another look.\n"
Russ Cox3b6ddd92010-11-04 13:58:32 -0400370 typecheck(pmsg, str)
Russ Cox15947302010-01-07 18:23:30 -0800371 PostMessage(ui, self.name, pmsg, subject=self.Subject())
372 self.mailed = True
373 self.Flush(ui, repo)
374
Russ Cox79a63722009-10-22 11:12:39 -0700375def GoodCLName(name):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400376 typecheck(name, str)
Russ Cox45495242009-11-01 05:49:35 -0800377 return re.match("^[0-9]+$", name)
Russ Cox79a63722009-10-22 11:12:39 -0700378
379def ParseCL(text, name):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400380 typecheck(text, str)
381 typecheck(name, str)
Russ Cox79a63722009-10-22 11:12:39 -0700382 sname = None
383 lineno = 0
384 sections = {
Russ Cox790c9b52009-11-05 14:44:57 -0800385 'Author': '',
Russ Cox79a63722009-10-22 11:12:39 -0700386 'Description': '',
387 'Files': '',
388 'URL': '',
389 'Reviewer': '',
390 'CC': '',
Russ Cox15947302010-01-07 18:23:30 -0800391 'Mailed': '',
Russ Cox79a63722009-10-22 11:12:39 -0700392 }
393 for line in text.split('\n'):
394 lineno += 1
395 line = line.rstrip()
396 if line != '' and line[0] == '#':
397 continue
398 if line == '' or line[0] == ' ' or line[0] == '\t':
399 if sname == None and line != '':
400 return None, lineno, 'text outside section'
401 if sname != None:
402 sections[sname] += line + '\n'
403 continue
404 p = line.find(':')
405 if p >= 0:
406 s, val = line[:p].strip(), line[p+1:].strip()
407 if s in sections:
408 sname = s
409 if val != '':
410 sections[sname] += val + '\n'
411 continue
412 return None, lineno, 'malformed section header'
413
414 for k in sections:
415 sections[k] = StripCommon(sections[k]).rstrip()
416
417 cl = CL(name)
Russ Cox790c9b52009-11-05 14:44:57 -0800418 if sections['Author']:
Russ Cox752b1702010-01-09 09:47:14 -0800419 cl.copied_from = sections['Author']
Russ Cox79a63722009-10-22 11:12:39 -0700420 cl.desc = sections['Description']
421 for line in sections['Files'].split('\n'):
422 i = line.find('#')
423 if i >= 0:
424 line = line[0:i].rstrip()
Ivan Krasine8b8aeb2010-08-12 00:04:17 -0700425 line = line.strip()
Russ Cox79a63722009-10-22 11:12:39 -0700426 if line == '':
427 continue
428 cl.files.append(line)
429 cl.reviewer = SplitCommaSpace(sections['Reviewer'])
430 cl.cc = SplitCommaSpace(sections['CC'])
431 cl.url = sections['URL']
Russ Cox15947302010-01-07 18:23:30 -0800432 if sections['Mailed'] != 'False':
433 # Odd default, but avoids spurious mailings when
434 # reading old CLs that do not have a Mailed: line.
435 # CLs created with this update will always have
436 # Mailed: False on disk.
437 cl.mailed = True
Russ Cox79a63722009-10-22 11:12:39 -0700438 if cl.desc == '<enter description here>':
Russ Cox15947302010-01-07 18:23:30 -0800439 cl.desc = ''
Russ Cox79a63722009-10-22 11:12:39 -0700440 return cl, 0, ''
441
442def SplitCommaSpace(s):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400443 typecheck(s, str)
Russ Cox84ac3572010-01-13 09:09:06 -0800444 s = s.strip()
445 if s == "":
446 return []
447 return re.split(", *", s)
Russ Cox79a63722009-10-22 11:12:39 -0700448
Russ Cox506ce112009-11-04 03:15:24 -0800449def CutDomain(s):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400450 typecheck(s, str)
Russ Cox506ce112009-11-04 03:15:24 -0800451 i = s.find('@')
452 if i >= 0:
453 s = s[0:i]
454 return s
455
Russ Cox79a63722009-10-22 11:12:39 -0700456def JoinComma(l):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400457 for s in l:
458 typecheck(s, str)
Russ Cox79a63722009-10-22 11:12:39 -0700459 return ", ".join(l)
460
Russ Cox45495242009-11-01 05:49:35 -0800461def ExceptionDetail():
462 s = str(sys.exc_info()[0])
463 if s.startswith("<type '") and s.endswith("'>"):
464 s = s[7:-2]
465 elif s.startswith("<class '") and s.endswith("'>"):
466 s = s[8:-2]
467 arg = str(sys.exc_info()[1])
468 if len(arg) > 0:
469 s += ": " + arg
470 return s
471
Russ Coxe414fda2009-11-04 15:17:01 -0800472def IsLocalCL(ui, repo, name):
473 return GoodCLName(name) and os.access(CodeReviewDir(ui, repo) + "/cl." + name, 0)
474
Russ Cox79a63722009-10-22 11:12:39 -0700475# Load CL from disk and/or the web.
476def LoadCL(ui, repo, name, web=True):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400477 typecheck(name, str)
Russ Coxe3ac0b52010-08-26 16:27:42 -0400478 set_status("loading CL " + name)
Russ Cox79a63722009-10-22 11:12:39 -0700479 if not GoodCLName(name):
480 return None, "invalid CL name"
481 dir = CodeReviewDir(ui, repo)
482 path = dir + "cl." + name
Russ Cox45495242009-11-01 05:49:35 -0800483 if os.access(path, 0):
Russ Cox79a63722009-10-22 11:12:39 -0700484 ff = open(path)
485 text = ff.read()
486 ff.close()
487 cl, lineno, err = ParseCL(text, name)
488 if err != "":
Russ Cox45495242009-11-01 05:49:35 -0800489 return None, "malformed CL data: "+err
Russ Cox79a63722009-10-22 11:12:39 -0700490 cl.local = True
Russ Cox45495242009-11-01 05:49:35 -0800491 else:
Russ Cox79a63722009-10-22 11:12:39 -0700492 cl = CL(name)
493 if web:
494 try:
495 f = GetSettings(name)
Russ Cox45495242009-11-01 05:49:35 -0800496 except:
Russ Cox790c9b52009-11-05 14:44:57 -0800497 return None, "cannot load CL %s from code review server: %s" % (name, ExceptionDetail())
Russ Cox45495242009-11-01 05:49:35 -0800498 if 'reviewers' not in f:
499 return None, "malformed response loading CL data from code review server"
Russ Cox79a63722009-10-22 11:12:39 -0700500 cl.reviewer = SplitCommaSpace(f['reviewers'])
501 cl.cc = SplitCommaSpace(f['cc'])
Russ Cox752b1702010-01-09 09:47:14 -0800502 if cl.local and cl.copied_from and cl.desc:
Russ Coxbe32c6a2009-11-30 10:28:48 -0800503 # local copy of CL written by someone else
504 # and we saved a description. use that one,
505 # so that committers can edit the description
506 # before doing hg submit.
507 pass
508 else:
509 cl.desc = f['description']
Russ Cox79a63722009-10-22 11:12:39 -0700510 cl.url = server_url_base + name
511 cl.web = True
Russ Coxe3ac0b52010-08-26 16:27:42 -0400512 set_status("loaded CL " + name)
Russ Cox79a63722009-10-22 11:12:39 -0700513 return cl, ''
514
Eoghan Sherry2f8ff0b2010-12-15 11:49:43 -0500515global_status = None
Russ Coxe3ac0b52010-08-26 16:27:42 -0400516
517def set_status(s):
518 # print >>sys.stderr, "\t", time.asctime(), s
519 global global_status
520 global_status = s
521
522class StatusThread(threading.Thread):
523 def __init__(self):
524 threading.Thread.__init__(self)
525 def run(self):
526 # pause a reasonable amount of time before
527 # starting to display status messages, so that
528 # most hg commands won't ever see them.
529 time.sleep(30)
530
531 # now show status every 15 seconds
532 while True:
533 time.sleep(15 - time.time() % 15)
534 s = global_status
535 if s is None:
536 continue
537 if s == "":
538 s = "(unknown status)"
539 print >>sys.stderr, time.asctime(), s
540
541def start_status_thread():
Russ Cox4ae2b432010-08-26 17:06:36 -0400542 t = StatusThread()
543 t.setDaemon(True) # allowed to exit if t is still running
544 t.start()
Russ Coxe3ac0b52010-08-26 16:27:42 -0400545
Russ Cox45495242009-11-01 05:49:35 -0800546class LoadCLThread(threading.Thread):
547 def __init__(self, ui, repo, dir, f, web):
548 threading.Thread.__init__(self)
549 self.ui = ui
550 self.repo = repo
Russ Coxdde666d2009-11-01 18:46:07 -0800551 self.dir = dir
Russ Cox45495242009-11-01 05:49:35 -0800552 self.f = f
553 self.web = web
554 self.cl = None
555 def run(self):
556 cl, err = LoadCL(self.ui, self.repo, self.f[3:], web=self.web)
557 if err != '':
558 self.ui.warn("loading "+self.dir+self.f+": " + err + "\n")
559 return
560 self.cl = cl
561
Russ Cox79a63722009-10-22 11:12:39 -0700562# Load all the CLs from this repository.
563def LoadAllCL(ui, repo, web=True):
564 dir = CodeReviewDir(ui, repo)
565 m = {}
Russ Cox45495242009-11-01 05:49:35 -0800566 files = [f for f in os.listdir(dir) if f.startswith('cl.')]
567 if not files:
568 return m
Russ Cox45495242009-11-01 05:49:35 -0800569 active = []
Russ Coxe67161e2009-11-07 18:56:29 -0800570 first = True
Russ Cox45495242009-11-01 05:49:35 -0800571 for f in files:
572 t = LoadCLThread(ui, repo, dir, f, web)
573 t.start()
Russ Coxe67161e2009-11-07 18:56:29 -0800574 if web and first:
575 # first request: wait in case it needs to authenticate
576 # otherwise we get lots of user/password prompts
577 # running in parallel.
578 t.join()
579 if t.cl:
580 m[t.cl.name] = t.cl
581 first = False
582 else:
583 active.append(t)
Russ Cox45495242009-11-01 05:49:35 -0800584 for t in active:
585 t.join()
586 if t.cl:
587 m[t.cl.name] = t.cl
Russ Cox79a63722009-10-22 11:12:39 -0700588 return m
589
590# Find repository root. On error, ui.warn and return None
591def RepoDir(ui, repo):
592 url = repo.url();
Hector Chucd9d72b2009-11-30 11:53:11 -0800593 if not url.startswith('file:'):
Russ Cox79a63722009-10-22 11:12:39 -0700594 ui.warn("repository %s is not in local file system\n" % (url,))
595 return None
596 url = url[5:]
597 if url.endswith('/'):
598 url = url[:-1]
Russ Cox3b6ddd92010-11-04 13:58:32 -0400599 typecheck(url, str)
Russ Cox79a63722009-10-22 11:12:39 -0700600 return url
601
602# Find (or make) code review directory. On error, ui.warn and return None
603def CodeReviewDir(ui, repo):
604 dir = RepoDir(ui, repo)
605 if dir == None:
606 return None
607 dir += '/.hg/codereview/'
608 if not os.path.isdir(dir):
609 try:
610 os.mkdir(dir, 0700)
Russ Cox45495242009-11-01 05:49:35 -0800611 except:
612 ui.warn('cannot mkdir %s: %s\n' % (dir, ExceptionDetail()))
Russ Cox79a63722009-10-22 11:12:39 -0700613 return None
Russ Cox3b6ddd92010-11-04 13:58:32 -0400614 typecheck(dir, str)
Russ Cox79a63722009-10-22 11:12:39 -0700615 return dir
616
Russ Cox17fc373a2011-01-24 14:14:26 -0500617# Turn leading tabs into spaces, so that the common white space
618# prefix doesn't get confused when people's editors write out
619# some lines with spaces, some with tabs. Only a heuristic
620# (some editors don't use 8 spaces either) but a useful one.
621def TabsToSpaces(line):
622 i = 0
623 while i < len(line) and line[i] == '\t':
624 i += 1
625 return ' '*(8*i) + line[i:]
626
Russ Cox79a63722009-10-22 11:12:39 -0700627# Strip maximal common leading white space prefix from text
628def StripCommon(text):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400629 typecheck(text, str)
Russ Cox79a63722009-10-22 11:12:39 -0700630 ws = None
631 for line in text.split('\n'):
632 line = line.rstrip()
633 if line == '':
634 continue
Russ Cox17fc373a2011-01-24 14:14:26 -0500635 line = TabsToSpaces(line)
Russ Cox79a63722009-10-22 11:12:39 -0700636 white = line[:len(line)-len(line.lstrip())]
637 if ws == None:
638 ws = white
639 else:
640 common = ''
641 for i in range(min(len(white), len(ws))+1):
642 if white[0:i] == ws[0:i]:
643 common = white[0:i]
644 ws = common
645 if ws == '':
646 break
647 if ws == None:
648 return text
649 t = ''
650 for line in text.split('\n'):
651 line = line.rstrip()
Russ Cox17fc373a2011-01-24 14:14:26 -0500652 line = TabsToSpaces(line)
Russ Cox79a63722009-10-22 11:12:39 -0700653 if line.startswith(ws):
654 line = line[len(ws):]
655 if line == '' and t == '':
656 continue
657 t += line + '\n'
658 while len(t) >= 2 and t[-2:] == '\n\n':
659 t = t[:-1]
Russ Cox3b6ddd92010-11-04 13:58:32 -0400660 typecheck(t, str)
Russ Cox79a63722009-10-22 11:12:39 -0700661 return t
662
663# Indent text with indent.
664def Indent(text, indent):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400665 typecheck(text, str)
666 typecheck(indent, str)
Russ Cox79a63722009-10-22 11:12:39 -0700667 t = ''
668 for line in text.split('\n'):
669 t += indent + line + '\n'
Russ Cox3b6ddd92010-11-04 13:58:32 -0400670 typecheck(t, str)
Russ Cox79a63722009-10-22 11:12:39 -0700671 return t
672
673# Return the first line of l
674def line1(text):
Russ Cox3b6ddd92010-11-04 13:58:32 -0400675 typecheck(text, str)
Russ Cox79a63722009-10-22 11:12:39 -0700676 return text.split('\n')[0]
677
678_change_prolog = """# Change list.
679# Lines beginning with # are ignored.
680# Multi-line values should be indented.
681"""
682
683#######################################################################
684# Mercurial helper functions
685
Peter Williams1d6eb742010-05-24 14:37:00 -0700686# Get effective change nodes taking into account applied MQ patches
687def effective_revpair(repo):
688 try:
689 return cmdutil.revpair(repo, ['qparent'])
690 except:
691 return cmdutil.revpair(repo, None)
692
Russ Cox79a63722009-10-22 11:12:39 -0700693# Return list of changed files in repository that match pats.
Russ Cox17fc373a2011-01-24 14:14:26 -0500694# Warn about patterns that did not match.
695def matchpats(ui, repo, pats, opts):
Russ Cox45495242009-11-01 05:49:35 -0800696 matcher = cmdutil.match(repo, pats, opts)
Peter Williams1d6eb742010-05-24 14:37:00 -0700697 node1, node2 = effective_revpair(repo)
Russ Cox17fc373a2011-01-24 14:14:26 -0500698 modified, added, removed, deleted, unknown, ignored, clean = repo.status(node1, node2, matcher, ignored=True, clean=True, unknown=True)
699 return (modified, added, removed, deleted, unknown, ignored, clean)
700
701# Return list of changed files in repository that match pats.
702# The patterns came from the command line, so we warn
703# if they have no effect or cannot be understood.
704def ChangedFiles(ui, repo, pats, opts, taken=None):
705 taken = taken or {}
706 # Run each pattern separately so that we can warn about
707 # patterns that didn't do anything useful.
708 for p in pats:
709 modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, [p], opts)
710 redo = False
711 for f in unknown:
712 promptadd(ui, repo, f)
713 redo = True
714 for f in deleted:
715 promptremove(ui, repo, f)
716 redo = True
717 if redo:
718 modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, [p], opts)
719 for f in modified + added + removed:
720 if f in taken:
721 ui.warn("warning: %s already in CL %s\n" % (f, taken[f].name))
722 if not modified and not added and not removed:
723 ui.warn("warning: %s did not match any modified files\n" % (p,))
724
725 # Again, all at once (eliminates duplicates)
726 modified, added, removed = matchpats(ui, repo, pats, opts)[:3]
Russ Cox45495242009-11-01 05:49:35 -0800727 l = modified + added + removed
728 l.sort()
Russ Cox17fc373a2011-01-24 14:14:26 -0500729 if taken:
730 l = Sub(l, taken.keys())
Russ Cox45495242009-11-01 05:49:35 -0800731 return l
Russ Cox79a63722009-10-22 11:12:39 -0700732
Russ Coxd8e0d9a2009-11-04 23:43:55 -0800733# Return list of changed files in repository that match pats and still exist.
734def ChangedExistingFiles(ui, repo, pats, opts):
Russ Cox17fc373a2011-01-24 14:14:26 -0500735 modified, added = matchpats(ui, repo, pats, opts)[:2]
Russ Coxd8e0d9a2009-11-04 23:43:55 -0800736 l = modified + added
737 l.sort()
738 return l
739
Russ Cox79a63722009-10-22 11:12:39 -0700740# Return list of files claimed by existing CLs
Russ Cox45495242009-11-01 05:49:35 -0800741def Taken(ui, repo):
Russ Cox79a63722009-10-22 11:12:39 -0700742 all = LoadAllCL(ui, repo, web=False)
743 taken = {}
744 for _, cl in all.items():
745 for f in cl.files:
746 taken[f] = cl
747 return taken
748
749# Return list of changed files that are not claimed by other CLs
750def DefaultFiles(ui, repo, pats, opts):
Russ Cox17fc373a2011-01-24 14:14:26 -0500751 return ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
Russ Cox79a63722009-10-22 11:12:39 -0700752
753def Sub(l1, l2):
754 return [l for l in l1 if l not in l2]
755
756def Add(l1, l2):
Russ Cox45495242009-11-01 05:49:35 -0800757 l = l1 + Sub(l2, l1)
758 l.sort()
759 return l
Russ Cox79a63722009-10-22 11:12:39 -0700760
761def Intersect(l1, l2):
762 return [l for l in l1 if l in l2]
763
Russ Coxdde666d2009-11-01 18:46:07 -0800764def getremote(ui, repo, opts):
765 # save $http_proxy; creating the HTTP repo object will
766 # delete it in an attempt to "help"
767 proxy = os.environ.get('http_proxy')
Andrew Gerrande678afa2010-03-03 09:03:31 +1100768 source = hg.parseurl(ui.expandpath("default"), None)[0]
Evan Shawe8fcf602010-07-14 17:17:04 -0700769 try:
770 remoteui = hg.remoteui # hg 1.6
Russ Coxf7d87f32010-08-26 18:56:29 -0400771 except:
Evan Shawe8fcf602010-07-14 17:17:04 -0700772 remoteui = cmdutil.remoteui
773 other = hg.repository(remoteui(repo, opts), source)
Russ Coxdde666d2009-11-01 18:46:07 -0800774 if proxy is not None:
775 os.environ['http_proxy'] = proxy
776 return other
777
778def Incoming(ui, repo, opts):
Russ Cox08e65f72010-07-16 18:54:38 -0700779 _, incoming, _ = findcommonincoming(repo, getremote(ui, repo, opts))
Russ Cox79a63722009-10-22 11:12:39 -0700780 return incoming
781
Andrew Gerrand5dd08692011-03-17 09:11:08 +1100782desc_re = '^(.+: |(tag )?(release|weekly)\.|fix build)'
Russ Cox17fc373a2011-01-24 14:14:26 -0500783
784desc_msg = '''Your CL description appears not to use the standard form.
785
786The first line of your change description is conventionally a
787one-line summary of the change, prefixed by the primary affected package,
788and is used as the subject for code review mail; the rest of the description
789elaborates.
790
791Examples:
792
793 encoding/rot13: new package
794
795 math: add IsInf, IsNaN
796
797 net: fix cname in LookupHost
798
799 unicode: update to Unicode 5.0.2
800
801'''
802
803
804
805def promptremove(ui, repo, f):
806 if promptyesno(ui, "hg remove %s (y/n)?" % (f,)):
807 if commands.remove(ui, repo, 'path:'+f) != 0:
808 ui.warn("error removing %s" % (f,))
809
810def promptadd(ui, repo, f):
811 if promptyesno(ui, "hg add %s (y/n)?" % (f,)):
812 if commands.add(ui, repo, 'path:'+f) != 0:
813 ui.warn("error adding %s" % (f,))
814
Russ Cox79a63722009-10-22 11:12:39 -0700815def EditCL(ui, repo, cl):
Russ Coxe3ac0b52010-08-26 16:27:42 -0400816 set_status(None) # do not show status
Russ Cox79a63722009-10-22 11:12:39 -0700817 s = cl.EditorText()
818 while True:
819 s = ui.edit(s, ui.username())
820 clx, line, err = ParseCL(s, cl.name)
821 if err != '':
Russ Coxff7343f2010-01-20 09:49:35 -0800822 if not promptyesno(ui, "error parsing change list: line %d: %s\nre-edit (y/n)?" % (line, err)):
Russ Cox79a63722009-10-22 11:12:39 -0700823 return "change list not modified"
824 continue
Russ Cox17fc373a2011-01-24 14:14:26 -0500825
826 # Check description.
827 if clx.desc == '':
828 if promptyesno(ui, "change list should have a description\nre-edit (y/n)?"):
829 continue
830 elif not re.match(desc_re, clx.desc.split('\n')[0]):
831 if promptyesno(ui, desc_msg + "re-edit (y/n)?"):
832 continue
833
834 # Check file list for files that need to be hg added or hg removed
835 # or simply aren't understood.
836 pats = ['path:'+f for f in clx.files]
837 modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, pats, {})
838 files = []
839 for f in clx.files:
840 if f in modified or f in added or f in removed:
841 files.append(f)
842 continue
843 if f in deleted:
844 promptremove(ui, repo, f)
845 files.append(f)
846 continue
847 if f in unknown:
848 promptadd(ui, repo, f)
849 files.append(f)
850 continue
851 if f in ignored:
852 ui.warn("error: %s is excluded by .hgignore; omitting\n" % (f,))
853 continue
854 if f in clean:
855 ui.warn("warning: %s is listed in the CL but unchanged\n" % (f,))
856 files.append(f)
857 continue
858 p = repo.root + '/' + f
859 if os.path.isfile(p):
860 ui.warn("warning: %s is a file but not known to hg\n" % (f,))
861 files.append(f)
862 continue
863 if os.path.isdir(p):
864 ui.warn("error: %s is a directory, not a file; omitting\n" % (f,))
865 continue
866 ui.warn("error: %s does not exist; omitting\n" % (f,))
867 clx.files = files
868
869 cl.desc = clx.desc
Russ Cox79a63722009-10-22 11:12:39 -0700870 cl.reviewer = clx.reviewer
871 cl.cc = clx.cc
872 cl.files = clx.files
Russ Cox79a63722009-10-22 11:12:39 -0700873 break
874 return ""
875
876# For use by submit, etc. (NOT by change)
877# Get change list number or list of files from command line.
878# If files are given, make a new change list.
Russ Cox82747422009-12-15 13:36:05 -0800879def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
Russ Cox79a63722009-10-22 11:12:39 -0700880 if len(pats) > 0 and GoodCLName(pats[0]):
881 if len(pats) != 1:
882 return None, "cannot specify change number and file names"
883 if opts.get('message'):
884 return None, "cannot use -m with existing CL"
885 cl, err = LoadCL(ui, repo, pats[0], web=True)
Russ Cox790c9b52009-11-05 14:44:57 -0800886 if err != "":
887 return None, err
Russ Cox79a63722009-10-22 11:12:39 -0700888 else:
889 cl = CL("new")
890 cl.local = True
Russ Cox17fc373a2011-01-24 14:14:26 -0500891 cl.files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
Russ Cox79a63722009-10-22 11:12:39 -0700892 if not cl.files:
893 return None, "no files changed"
894 if opts.get('reviewer'):
895 cl.reviewer = Add(cl.reviewer, SplitCommaSpace(opts.get('reviewer')))
896 if opts.get('cc'):
897 cl.cc = Add(cl.cc, SplitCommaSpace(opts.get('cc')))
Russ Cox82747422009-12-15 13:36:05 -0800898 if defaultcc:
899 cl.cc = Add(cl.cc, defaultcc)
Russ Cox79a63722009-10-22 11:12:39 -0700900 if cl.name == "new":
901 if opts.get('message'):
902 cl.desc = opts.get('message')
903 else:
904 err = EditCL(ui, repo, cl)
905 if err != '':
906 return None, err
907 return cl, ""
908
Russ Cox45495242009-11-01 05:49:35 -0800909# reposetup replaces cmdutil.match with this wrapper,
910# which expands the syntax @clnumber to mean the files
911# in that CL.
912original_match = None
Russ Cox17fc373a2011-01-24 14:14:26 -0500913def ReplacementForCmdutilMatch(repo, pats=None, opts=None, globbed=False, default='relpath'):
Russ Cox45495242009-11-01 05:49:35 -0800914 taken = []
915 files = []
Russ Coxf7d87f32010-08-26 18:56:29 -0400916 pats = pats or []
Russ Cox17fc373a2011-01-24 14:14:26 -0500917 opts = opts or {}
Russ Cox45495242009-11-01 05:49:35 -0800918 for p in pats:
919 if p.startswith('@'):
920 taken.append(p)
921 clname = p[1:]
922 if not GoodCLName(clname):
923 raise util.Abort("invalid CL name " + clname)
924 cl, err = LoadCL(repo.ui, repo, clname, web=False)
925 if err != '':
926 raise util.Abort("loading CL " + clname + ": " + err)
Russ Coxf7d87f32010-08-26 18:56:29 -0400927 if not cl.files:
Russ Cox82747422009-12-15 13:36:05 -0800928 raise util.Abort("no files in CL " + clname)
Russ Cox45495242009-11-01 05:49:35 -0800929 files = Add(files, cl.files)
Paolo Giarrusso3ca72452010-06-09 21:37:11 -0700930 pats = Sub(pats, taken) + ['path:'+f for f in files]
Russ Cox45495242009-11-01 05:49:35 -0800931 return original_match(repo, pats=pats, opts=opts, globbed=globbed, default=default)
932
Russ Coxd8e0d9a2009-11-04 23:43:55 -0800933def RelativePath(path, cwd):
934 n = len(cwd)
935 if path.startswith(cwd) and path[n] == '/':
936 return path[n+1:]
937 return path
938
Russ Cox49bff2d2010-10-06 18:10:23 -0400939def CheckFormat(ui, repo, files, just_warn=False):
Russ Coxe3ac0b52010-08-26 16:27:42 -0400940 set_status("running gofmt")
Russ Cox49bff2d2010-10-06 18:10:23 -0400941 CheckGofmt(ui, repo, files, just_warn)
942 CheckTabfmt(ui, repo, files, just_warn)
943
944# Check that gofmt run on the list of files does not change them
945def CheckGofmt(ui, repo, files, just_warn):
Russ Cox19dae072009-11-20 13:11:42 -0800946 files = [f for f in files if (f.startswith('src/') or f.startswith('test/bench/')) and f.endswith('.go')]
Russ Cox9df7d6e2009-11-05 08:11:44 -0800947 if not files:
Russ Coxd8e0d9a2009-11-04 23:43:55 -0800948 return
949 cwd = os.getcwd()
950 files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
Russ Cox9a86cc62009-12-03 17:23:11 -0800951 files = [f for f in files if os.access(f, 0)]
Russ Coxad665e42010-07-15 16:43:06 -0700952 if not files:
953 return
Russ Coxd8e0d9a2009-11-04 23:43:55 -0800954 try:
Hector Chu59a63952011-01-24 14:16:24 -0500955 cmd = subprocess.Popen(["gofmt", "-l"] + files, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=sys.platform != "win32")
Russ Cox72a59ce2009-11-07 17:30:40 -0800956 cmd.stdin.close()
Russ Coxd8e0d9a2009-11-04 23:43:55 -0800957 except:
958 raise util.Abort("gofmt: " + ExceptionDetail())
Russ Cox72a59ce2009-11-07 17:30:40 -0800959 data = cmd.stdout.read()
960 errors = cmd.stderr.read()
961 cmd.wait()
Russ Coxe3ac0b52010-08-26 16:27:42 -0400962 set_status("done with gofmt")
Russ Coxd8e0d9a2009-11-04 23:43:55 -0800963 if len(errors) > 0:
964 ui.warn("gofmt errors:\n" + errors.rstrip() + "\n")
965 return
966 if len(data) > 0:
Russ Coxf74beeb2009-11-06 18:40:30 -0800967 msg = "gofmt needs to format these files (run hg gofmt):\n" + Indent(data, "\t").rstrip()
Russ Cox043486e2009-11-06 09:45:24 -0800968 if just_warn:
Russ Coxf74beeb2009-11-06 18:40:30 -0800969 ui.warn("warning: " + msg + "\n")
Russ Cox043486e2009-11-06 09:45:24 -0800970 else:
Russ Coxf74beeb2009-11-06 18:40:30 -0800971 raise util.Abort(msg)
Russ Coxd8e0d9a2009-11-04 23:43:55 -0800972 return
973
Russ Cox49bff2d2010-10-06 18:10:23 -0400974# Check that *.[chys] files indent using tabs.
975def CheckTabfmt(ui, repo, files, just_warn):
976 files = [f for f in files if f.startswith('src/') and re.search(r"\.[chys]$", f)]
977 if not files:
978 return
979 cwd = os.getcwd()
980 files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
981 files = [f for f in files if os.access(f, 0)]
982 badfiles = []
983 for f in files:
984 try:
985 for line in open(f, 'r'):
986 if line.startswith(' '):
987 badfiles.append(f)
988 break
989 except:
990 # ignore cannot open file, etc.
991 pass
992 if len(badfiles) > 0:
993 msg = "these files use spaces for indentation (use tabs instead):\n\t" + "\n\t".join(badfiles)
994 if just_warn:
995 ui.warn("warning: " + msg + "\n")
996 else:
997 raise util.Abort(msg)
998 return
999
Russ Cox79a63722009-10-22 11:12:39 -07001000#######################################################################
1001# Mercurial commands
1002
Russ Cox79a63722009-10-22 11:12:39 -07001003# every command must take a ui and and repo as arguments.
1004# opts is a dict where you can find other command line flags
1005#
1006# Other parameters are taken in order from items on the command line that
1007# don't start with a dash. If no default value is given in the parameter list,
1008# they are required.
Russ Cox45495242009-11-01 05:49:35 -08001009#
Russ Cox79a63722009-10-22 11:12:39 -07001010
Russ Cox79a63722009-10-22 11:12:39 -07001011def change(ui, repo, *pats, **opts):
David Symondsee75ffa2010-04-10 01:53:43 -07001012 """create, edit or delete a change list
Russ Cox45495242009-11-01 05:49:35 -08001013
David Symondsee75ffa2010-04-10 01:53:43 -07001014 Create, edit or delete a change list.
Russ Cox79a63722009-10-22 11:12:39 -07001015 A change list is a group of files to be reviewed and submitted together,
1016 plus a textual description of the change.
1017 Change lists are referred to by simple alphanumeric names.
1018
1019 Changes must be reviewed before they can be submitted.
Russ Cox79a63722009-10-22 11:12:39 -07001020
Russ Cox45495242009-11-01 05:49:35 -08001021 In the absence of options, the change command opens the
1022 change list for editing in the default editor.
Russ Cox72a59ce2009-11-07 17:30:40 -08001023
Russ Cox790c9b52009-11-05 14:44:57 -08001024 Deleting a change with the -d or -D flag does not affect
1025 the contents of the files listed in that change. To revert
1026 the files listed in a change, use
Russ Cox72a59ce2009-11-07 17:30:40 -08001027
Russ Cox790c9b52009-11-05 14:44:57 -08001028 hg revert @123456
Russ Cox72a59ce2009-11-07 17:30:40 -08001029
Russ Cox790c9b52009-11-05 14:44:57 -08001030 before running hg change -d 123456.
Russ Cox45495242009-11-01 05:49:35 -08001031 """
Russ Cox79a63722009-10-22 11:12:39 -07001032
Russ Coxe6308652010-08-25 17:52:25 -04001033 if missing_codereview:
1034 return missing_codereview
1035
Russ Cox79a63722009-10-22 11:12:39 -07001036 dirty = {}
1037 if len(pats) > 0 and GoodCLName(pats[0]):
1038 name = pats[0]
Russ Cox45495242009-11-01 05:49:35 -08001039 if len(pats) != 1:
1040 return "cannot specify CL name and file patterns"
Russ Cox79a63722009-10-22 11:12:39 -07001041 pats = pats[1:]
1042 cl, err = LoadCL(ui, repo, name, web=True)
1043 if err != '':
1044 return err
Russ Cox45495242009-11-01 05:49:35 -08001045 if not cl.local and (opts["stdin"] or not opts["stdout"]):
Russ Cox79a63722009-10-22 11:12:39 -07001046 return "cannot change non-local CL " + name
1047 else:
Russ Cox79a63722009-10-22 11:12:39 -07001048 name = "new"
1049 cl = CL("new")
1050 dirty[cl] = True
Russ Cox17fc373a2011-01-24 14:14:26 -05001051 files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001052
Russ Cox790c9b52009-11-05 14:44:57 -08001053 if opts["delete"] or opts["deletelocal"]:
1054 if opts["delete"] and opts["deletelocal"]:
1055 return "cannot use -d and -D together"
1056 flag = "-d"
1057 if opts["deletelocal"]:
1058 flag = "-D"
Russ Cox45495242009-11-01 05:49:35 -08001059 if name == "new":
Russ Cox790c9b52009-11-05 14:44:57 -08001060 return "cannot use "+flag+" with file patterns"
Russ Cox45495242009-11-01 05:49:35 -08001061 if opts["stdin"] or opts["stdout"]:
Russ Cox790c9b52009-11-05 14:44:57 -08001062 return "cannot use "+flag+" with -i or -o"
Russ Cox45495242009-11-01 05:49:35 -08001063 if not cl.local:
1064 return "cannot change non-local CL " + name
Russ Cox790c9b52009-11-05 14:44:57 -08001065 if opts["delete"]:
Russ Cox752b1702010-01-09 09:47:14 -08001066 if cl.copied_from:
Russ Cox790c9b52009-11-05 14:44:57 -08001067 return "original author must delete CL; hg change -D will remove locally"
Russ Cox78863182010-08-12 14:58:38 -07001068 PostMessage(ui, cl.name, "*** Abandoned ***", send_mail=cl.mailed)
Russ Cox790c9b52009-11-05 14:44:57 -08001069 EditDesc(cl.name, closed="checked")
Russ Cox45495242009-11-01 05:49:35 -08001070 cl.Delete(ui, repo)
1071 return
Russ Cox79a63722009-10-22 11:12:39 -07001072
1073 if opts["stdin"]:
1074 s = sys.stdin.read()
1075 clx, line, err = ParseCL(s, name)
1076 if err != '':
1077 return "error parsing change list: line %d: %s" % (line, err)
1078 if clx.desc is not None:
1079 cl.desc = clx.desc;
1080 dirty[cl] = True
1081 if clx.reviewer is not None:
1082 cl.reviewer = clx.reviewer
1083 dirty[cl] = True
1084 if clx.cc is not None:
1085 cl.cc = clx.cc
1086 dirty[cl] = True
1087 if clx.files is not None:
1088 cl.files = clx.files
1089 dirty[cl] = True
1090
Russ Cox45495242009-11-01 05:49:35 -08001091 if not opts["stdin"] and not opts["stdout"]:
Russ Cox79a63722009-10-22 11:12:39 -07001092 if name == "new":
1093 cl.files = files
1094 err = EditCL(ui, repo, cl)
1095 if err != "":
1096 return err
1097 dirty[cl] = True
1098
1099 for d, _ in dirty.items():
Russ Coxfdb46fb2011-02-02 16:39:31 -05001100 name = d.name
Russ Cox79a63722009-10-22 11:12:39 -07001101 d.Flush(ui, repo)
Russ Coxfdb46fb2011-02-02 16:39:31 -05001102 if name == "new":
1103 d.Upload(ui, repo, quiet=True)
Russ Cox45495242009-11-01 05:49:35 -08001104
Russ Cox79a63722009-10-22 11:12:39 -07001105 if opts["stdout"]:
1106 ui.write(cl.EditorText())
1107 elif name == "new":
1108 if ui.quiet:
1109 ui.write(cl.name)
1110 else:
Russ Cox45495242009-11-01 05:49:35 -08001111 ui.write("CL created: " + cl.url + "\n")
Russ Cox79a63722009-10-22 11:12:39 -07001112 return
1113
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001114def code_login(ui, repo, **opts):
Russ Cox45495242009-11-01 05:49:35 -08001115 """log in to code review server
1116
1117 Logs in to the code review server, saving a cookie in
1118 a file in your home directory.
1119 """
Russ Coxe6308652010-08-25 17:52:25 -04001120 if missing_codereview:
1121 return missing_codereview
1122
Russ Cox45495242009-11-01 05:49:35 -08001123 MySend(None)
1124
Russ Cox790c9b52009-11-05 14:44:57 -08001125def clpatch(ui, repo, clname, **opts):
1126 """import a patch from the code review server
Russ Cox72a59ce2009-11-07 17:30:40 -08001127
Russ Cox790c9b52009-11-05 14:44:57 -08001128 Imports a patch from the code review server into the local client.
1129 If the local client has already modified any of the files that the
1130 patch modifies, this command will refuse to apply the patch.
Russ Cox72a59ce2009-11-07 17:30:40 -08001131
Russ Cox790c9b52009-11-05 14:44:57 -08001132 Submitting an imported patch will keep the original author's
1133 name as the Author: line but add your own name to a Committer: line.
1134 """
Russ Coxe6308652010-08-25 17:52:25 -04001135 if missing_codereview:
1136 return missing_codereview
1137
Russ Cox790c9b52009-11-05 14:44:57 -08001138 cl, patch, err = DownloadCL(ui, repo, clname)
Gustavo Niemeyer73aacbd2011-02-23 11:48:40 -05001139 if err != "":
1140 return err
1141 if patch == emptydiff:
1142 return "codereview issue %s has no diff" % clname
1143
Russ Cox790c9b52009-11-05 14:44:57 -08001144 argv = ["hgpatch"]
1145 if opts["no_incoming"]:
1146 argv += ["--checksync=false"]
Russ Cox790c9b52009-11-05 14:44:57 -08001147 try:
Yasuhiro Matsumoto27191b52011-02-02 22:43:40 -05001148 cmd = subprocess.Popen(argv, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, close_fds=sys.platform != "win32")
Russ Cox790c9b52009-11-05 14:44:57 -08001149 except:
1150 return "hgpatch: " + ExceptionDetail()
Yasuhiro Matsumoto27191b52011-02-02 22:43:40 -05001151
Yasuhiro Matsumoto3108f3f2011-02-08 22:30:06 -05001152 out, err = cmd.communicate(patch)
1153 if cmd.returncode != 0 and not opts["ignore_hgpatch_failure"]:
Russ Cox790c9b52009-11-05 14:44:57 -08001154 return "hgpatch failed"
1155 cl.local = True
1156 cl.files = out.strip().split()
Gustavo Niemeyer73aacbd2011-02-23 11:48:40 -05001157 if not cl.files:
1158 return "codereview issue %s has no diff" % clname
Russ Cox790c9b52009-11-05 14:44:57 -08001159 files = ChangedFiles(ui, repo, [], opts)
1160 extra = Sub(cl.files, files)
1161 if extra:
1162 ui.warn("warning: these files were listed in the patch but not changed:\n\t" + "\n\t".join(extra) + "\n")
1163 cl.Flush(ui, repo)
1164 ui.write(cl.PendingText() + "\n")
Russ Cox72a59ce2009-11-07 17:30:40 -08001165
Russ Cox790c9b52009-11-05 14:44:57 -08001166def download(ui, repo, clname, **opts):
1167 """download a change from the code review server
Russ Cox72a59ce2009-11-07 17:30:40 -08001168
Russ Cox790c9b52009-11-05 14:44:57 -08001169 Download prints a description of the given change list
1170 followed by its diff, downloaded from the code review server.
1171 """
Russ Coxe6308652010-08-25 17:52:25 -04001172 if missing_codereview:
1173 return missing_codereview
1174
Russ Cox790c9b52009-11-05 14:44:57 -08001175 cl, patch, err = DownloadCL(ui, repo, clname)
1176 if err != "":
1177 return err
1178 ui.write(cl.EditorText() + "\n")
1179 ui.write(patch + "\n")
1180 return
1181
Russ Cox45495242009-11-01 05:49:35 -08001182def file(ui, repo, clname, pat, *pats, **opts):
1183 """assign files to or remove files from a change list
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001184
Russ Cox45495242009-11-01 05:49:35 -08001185 Assign files to or (with -d) remove files from a change list.
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001186
Russ Cox45495242009-11-01 05:49:35 -08001187 The -d option only removes files from the change list.
1188 It does not edit them or remove them from the repository.
1189 """
Russ Coxe6308652010-08-25 17:52:25 -04001190 if missing_codereview:
1191 return missing_codereview
1192
Russ Cox45495242009-11-01 05:49:35 -08001193 pats = tuple([pat] + list(pats))
1194 if not GoodCLName(clname):
1195 return "invalid CL name " + clname
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001196
Russ Cox45495242009-11-01 05:49:35 -08001197 dirty = {}
1198 cl, err = LoadCL(ui, repo, clname, web=False)
1199 if err != '':
1200 return err
1201 if not cl.local:
1202 return "cannot change non-local CL " + clname
1203
1204 files = ChangedFiles(ui, repo, pats, opts)
1205
1206 if opts["delete"]:
1207 oldfiles = Intersect(files, cl.files)
1208 if oldfiles:
1209 if not ui.quiet:
1210 ui.status("# Removing files from CL. To undo:\n")
1211 ui.status("# cd %s\n" % (repo.root))
1212 for f in oldfiles:
1213 ui.status("# hg file %s %s\n" % (cl.name, f))
1214 cl.files = Sub(cl.files, oldfiles)
1215 cl.Flush(ui, repo)
1216 else:
1217 ui.status("no such files in CL")
1218 return
1219
1220 if not files:
1221 return "no such modified files"
1222
1223 files = Sub(files, cl.files)
1224 taken = Taken(ui, repo)
1225 warned = False
1226 for f in files:
1227 if f in taken:
1228 if not warned and not ui.quiet:
1229 ui.status("# Taking files from other CLs. To undo:\n")
1230 ui.status("# cd %s\n" % (repo.root))
1231 warned = True
1232 ocl = taken[f]
1233 if not ui.quiet:
1234 ui.status("# hg file %s %s\n" % (ocl.name, f))
1235 if ocl not in dirty:
1236 ocl.files = Sub(ocl.files, files)
1237 dirty[ocl] = True
1238 cl.files = Add(cl.files, files)
1239 dirty[cl] = True
1240 for d, _ in dirty.items():
1241 d.Flush(ui, repo)
1242 return
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001243
1244def gofmt(ui, repo, *pats, **opts):
1245 """apply gofmt to modified files
1246
1247 Applies gofmt to the modified files in the repository that match
1248 the given patterns.
1249 """
Russ Coxe6308652010-08-25 17:52:25 -04001250 if missing_codereview:
1251 return missing_codereview
1252
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001253 files = ChangedExistingFiles(ui, repo, pats, opts)
1254 files = [f for f in files if f.endswith(".go")]
1255 if not files:
1256 return "no modified go files"
1257 cwd = os.getcwd()
1258 files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
1259 try:
Russ Cox9df7d6e2009-11-05 08:11:44 -08001260 cmd = ["gofmt", "-l"]
1261 if not opts["list"]:
1262 cmd += ["-w"]
1263 if os.spawnvp(os.P_WAIT, "gofmt", cmd + files) != 0:
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001264 raise util.Abort("gofmt did not exit cleanly")
1265 except error.Abort, e:
1266 raise
1267 except:
1268 raise util.Abort("gofmt: " + ExceptionDetail())
1269 return
1270
Russ Cox45495242009-11-01 05:49:35 -08001271def mail(ui, repo, *pats, **opts):
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001272 """mail a change for review
1273
1274 Uploads a patch to the code review server and then sends mail
1275 to the reviewer and CC list asking for a review.
1276 """
Russ Coxe6308652010-08-25 17:52:25 -04001277 if missing_codereview:
1278 return missing_codereview
1279
Russ Cox82747422009-12-15 13:36:05 -08001280 cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc)
Russ Cox45495242009-11-01 05:49:35 -08001281 if err != "":
1282 return err
Russ Coxb1a52ce2009-11-08 22:13:10 -08001283 cl.Upload(ui, repo, gofmt_just_warn=True)
Russ Cox4967f852010-01-28 12:48:21 -08001284 if not cl.reviewer:
1285 # If no reviewer is listed, assign the review to defaultcc.
1286 # This makes sure that it appears in the
1287 # codereview.appspot.com/user/defaultcc
1288 # page, so that it doesn't get dropped on the floor.
1289 if not defaultcc:
1290 return "no reviewers listed in CL"
1291 cl.cc = Sub(cl.cc, defaultcc)
1292 cl.reviewer = defaultcc
Ryan Hitchman30c85bf2011-01-19 14:46:06 -05001293 cl.Flush(ui, repo)
1294
1295 if cl.files == []:
1296 return "no changed files, not sending mail"
1297
1298 cl.Mail(ui, repo)
Russ Cox45495242009-11-01 05:49:35 -08001299
1300def nocommit(ui, repo, *pats, **opts):
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001301 """(disabled when using this extension)"""
Russ Cox45495242009-11-01 05:49:35 -08001302 return "The codereview extension is enabled; do not use commit."
1303
Russ Cox79a63722009-10-22 11:12:39 -07001304def pending(ui, repo, *pats, **opts):
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001305 """show pending changes
1306
1307 Lists pending changes followed by a list of unassigned but modified files.
1308 """
Russ Coxe6308652010-08-25 17:52:25 -04001309 if missing_codereview:
1310 return missing_codereview
1311
Russ Cox79a63722009-10-22 11:12:39 -07001312 m = LoadAllCL(ui, repo, web=True)
1313 names = m.keys()
1314 names.sort()
1315 for name in names:
1316 cl = m[name]
1317 ui.write(cl.PendingText() + "\n")
1318
1319 files = DefaultFiles(ui, repo, [], opts)
1320 if len(files) > 0:
1321 s = "Changed files not in any CL:\n"
1322 for f in files:
1323 s += "\t" + f + "\n"
1324 ui.write(s)
1325
Russ Cox45495242009-11-01 05:49:35 -08001326def reposetup(ui, repo):
1327 global original_match
Russ Coxe414fda2009-11-04 15:17:01 -08001328 if original_match is None:
Russ Coxe3ac0b52010-08-26 16:27:42 -04001329 start_status_thread()
Russ Coxe414fda2009-11-04 15:17:01 -08001330 original_match = cmdutil.match
1331 cmdutil.match = ReplacementForCmdutilMatch
1332 RietveldSetup(ui, repo)
Russ Cox79a63722009-10-22 11:12:39 -07001333
Russ Cox790c9b52009-11-05 14:44:57 -08001334def CheckContributor(ui, repo, user=None):
Russ Coxe3ac0b52010-08-26 16:27:42 -04001335 set_status("checking CONTRIBUTORS file")
Russ Cox506ce112009-11-04 03:15:24 -08001336 if not user:
Russ Cox790c9b52009-11-05 14:44:57 -08001337 user = ui.config("ui", "username")
1338 if not user:
1339 raise util.Abort("[ui] username is not configured in .hgrc")
Russ Cox88e365c2009-11-06 17:02:47 -08001340 _, userline = FindContributor(ui, repo, user, warn=False)
Russ Cox790c9b52009-11-05 14:44:57 -08001341 if not userline:
1342 raise util.Abort("cannot find %s in CONTRIBUTORS" % (user,))
1343 return userline
1344
1345def FindContributor(ui, repo, user, warn=True):
Russ Coxb2a65582010-09-11 23:42:29 -04001346 user = user.lower()
Russ Cox93f614f2010-06-30 23:34:11 -07001347 m = re.match(r".*<(.*)>", user)
1348 if m:
Russ Coxb2a65582010-09-11 23:42:29 -04001349 user = m.group(1)
Russ Cox93f614f2010-06-30 23:34:11 -07001350
1351 if user not in contributors:
1352 if warn:
1353 ui.warn("warning: cannot find %s in CONTRIBUTORS\n" % (user,))
1354 return None, None
1355
1356 user, email = contributors[user]
1357 return email, "%s <%s>" % (user, email)
Russ Cox506ce112009-11-04 03:15:24 -08001358
Russ Cox79a63722009-10-22 11:12:39 -07001359def submit(ui, repo, *pats, **opts):
1360 """submit change to remote repository
Russ Cox45495242009-11-01 05:49:35 -08001361
Russ Cox79a63722009-10-22 11:12:39 -07001362 Submits change to remote repository.
1363 Bails out if the local repository is not in sync with the remote one.
1364 """
Russ Coxe6308652010-08-25 17:52:25 -04001365 if missing_codereview:
1366 return missing_codereview
1367
Russ Coxdc9a02f2011-02-01 14:17:41 -05001368 # We already called this on startup but sometimes Mercurial forgets.
1369 set_mercurial_encoding_to_utf8()
1370
Russ Cox79a63722009-10-22 11:12:39 -07001371 repo.ui.quiet = True
Russ Coxdde666d2009-11-01 18:46:07 -08001372 if not opts["no_incoming"] and Incoming(ui, repo, opts):
Russ Cox79a63722009-10-22 11:12:39 -07001373 return "local repository out of date; must sync before submit"
1374
Russ Cox15947302010-01-07 18:23:30 -08001375 cl, err = CommandLineCL(ui, repo, pats, opts, defaultcc=defaultcc)
Russ Cox79a63722009-10-22 11:12:39 -07001376 if err != "":
1377 return err
Russ Cox45495242009-11-01 05:49:35 -08001378
Russ Cox790c9b52009-11-05 14:44:57 -08001379 user = None
Russ Cox752b1702010-01-09 09:47:14 -08001380 if cl.copied_from:
1381 user = cl.copied_from
Russ Cox790c9b52009-11-05 14:44:57 -08001382 userline = CheckContributor(ui, repo, user)
Russ Coxdc9a02f2011-02-01 14:17:41 -05001383 typecheck(userline, str)
Russ Cox790c9b52009-11-05 14:44:57 -08001384
Russ Cox79a63722009-10-22 11:12:39 -07001385 about = ""
1386 if cl.reviewer:
Russ Cox506ce112009-11-04 03:15:24 -08001387 about += "R=" + JoinComma([CutDomain(s) for s in cl.reviewer]) + "\n"
Russ Cox79a63722009-10-22 11:12:39 -07001388 if opts.get('tbr'):
1389 tbr = SplitCommaSpace(opts.get('tbr'))
1390 cl.reviewer = Add(cl.reviewer, tbr)
Russ Cox506ce112009-11-04 03:15:24 -08001391 about += "TBR=" + JoinComma([CutDomain(s) for s in tbr]) + "\n"
Russ Cox79a63722009-10-22 11:12:39 -07001392 if cl.cc:
Russ Cox506ce112009-11-04 03:15:24 -08001393 about += "CC=" + JoinComma([CutDomain(s) for s in cl.cc]) + "\n"
Russ Cox79a63722009-10-22 11:12:39 -07001394
1395 if not cl.reviewer:
1396 return "no reviewers listed in CL"
1397
1398 if not cl.local:
1399 return "cannot submit non-local CL"
1400
1401 # upload, to sync current patch and also get change number if CL is new.
Russ Cox752b1702010-01-09 09:47:14 -08001402 if not cl.copied_from:
Russ Coxb1a52ce2009-11-08 22:13:10 -08001403 cl.Upload(ui, repo, gofmt_just_warn=True)
1404
1405 # check gofmt for real; allowed upload to warn in order to save CL.
1406 cl.Flush(ui, repo)
Russ Cox49bff2d2010-10-06 18:10:23 -04001407 CheckFormat(ui, repo, cl.files)
Russ Coxb1a52ce2009-11-08 22:13:10 -08001408
Russ Cox79a63722009-10-22 11:12:39 -07001409 about += "%s%s\n" % (server_url_base, cl.name)
1410
Russ Cox752b1702010-01-09 09:47:14 -08001411 if cl.copied_from:
Russ Cox790c9b52009-11-05 14:44:57 -08001412 about += "\nCommitter: " + CheckContributor(ui, repo, None) + "\n"
Russ Coxdc9a02f2011-02-01 14:17:41 -05001413 typecheck(about, str)
Russ Cox790c9b52009-11-05 14:44:57 -08001414
Russ Cox752b1702010-01-09 09:47:14 -08001415 if not cl.mailed and not cl.copied_from: # in case this is TBR
Russ Cox15947302010-01-07 18:23:30 -08001416 cl.Mail(ui, repo)
1417
Russ Cox79a63722009-10-22 11:12:39 -07001418 # submit changes locally
1419 date = opts.get('date')
1420 if date:
1421 opts['date'] = util.parsedate(date)
Russ Coxdc9a02f2011-02-01 14:17:41 -05001422 typecheck(opts['date'], str)
Russ Cox79a63722009-10-22 11:12:39 -07001423 opts['message'] = cl.desc.rstrip() + "\n\n" + about
Russ Coxdc9a02f2011-02-01 14:17:41 -05001424 typecheck(opts['message'], str)
Russ Cox790c9b52009-11-05 14:44:57 -08001425
1426 if opts['dryrun']:
1427 print "NOT SUBMITTING:"
1428 print "User: ", userline
1429 print "Message:"
1430 print Indent(opts['message'], "\t")
1431 print "Files:"
1432 print Indent('\n'.join(cl.files), "\t")
1433 return "dry run; not submitted"
1434
Russ Cox45495242009-11-01 05:49:35 -08001435 m = match.exact(repo.root, repo.getcwd(), cl.files)
Russ Coxdc9a02f2011-02-01 14:17:41 -05001436 node = repo.commit(ustr(opts['message']), ustr(userline), opts.get('date'), m)
Russ Cox79a63722009-10-22 11:12:39 -07001437 if not node:
1438 return "nothing changed"
1439
Russ Cox7db2c792009-11-17 23:23:18 -08001440 # push to remote; if it fails for any reason, roll back
1441 try:
1442 log = repo.changelog
1443 rev = log.rev(node)
1444 parents = log.parentrevs(rev)
1445 if (rev-1 not in parents and
1446 (parents == (nullrev, nullrev) or
1447 len(log.heads(log.node(parents[0]))) > 1 and
1448 (parents[1] == nullrev or len(log.heads(log.node(parents[1]))) > 1))):
1449 # created new head
1450 raise util.Abort("local repository out of date; must sync before submit")
Russ Cox79a63722009-10-22 11:12:39 -07001451
Russ Cox7db2c792009-11-17 23:23:18 -08001452 # push changes to remote.
1453 # if it works, we're committed.
1454 # if not, roll back
1455 other = getremote(ui, repo, opts)
1456 r = repo.push(other, False, None)
1457 if r == 0:
1458 raise util.Abort("local repository out of date; must sync before submit")
1459 except:
Russ Cox79a63722009-10-22 11:12:39 -07001460 repo.rollback()
Russ Cox7db2c792009-11-17 23:23:18 -08001461 raise
Russ Cox79a63722009-10-22 11:12:39 -07001462
1463 # we're committed. upload final patch, close review, add commit message
1464 changeURL = short(node)
1465 url = other.url()
Russ Cox5b0ef4a2011-04-06 23:07:08 -04001466 m = re.match("^https?://([^@/]+@)?([^.]+)\.googlecode\.com/hg/?", url)
Russ Cox79a63722009-10-22 11:12:39 -07001467 if m:
1468 changeURL = "http://code.google.com/p/%s/source/detail?r=%s" % (m.group(2), changeURL)
1469 else:
1470 print >>sys.stderr, "URL: ", url
1471 pmsg = "*** Submitted as " + changeURL + " ***\n\n" + opts['message']
Russ Cox69f893a2009-12-02 09:10:59 -08001472
1473 # When posting, move reviewers to CC line,
1474 # so that the issue stops showing up in their "My Issues" page.
1475 PostMessage(ui, cl.name, pmsg, reviewers="", cc=JoinComma(cl.reviewer+cl.cc))
1476
Russ Cox752b1702010-01-09 09:47:14 -08001477 if not cl.copied_from:
Russ Cox790c9b52009-11-05 14:44:57 -08001478 EditDesc(cl.name, closed="checked")
Russ Cox79a63722009-10-22 11:12:39 -07001479 cl.Delete(ui, repo)
1480
1481def sync(ui, repo, **opts):
1482 """synchronize with remote repository
Russ Cox45495242009-11-01 05:49:35 -08001483
Russ Cox79a63722009-10-22 11:12:39 -07001484 Incorporates recent changes from the remote repository
1485 into the local repository.
Russ Cox79a63722009-10-22 11:12:39 -07001486 """
Russ Coxe6308652010-08-25 17:52:25 -04001487 if missing_codereview:
1488 return missing_codereview
1489
Russ Coxe414fda2009-11-04 15:17:01 -08001490 if not opts["local"]:
1491 ui.status = sync_note
1492 ui.note = sync_note
1493 other = getremote(ui, repo, opts)
1494 modheads = repo.pull(other)
1495 err = commands.postincoming(ui, repo, modheads, True, "tip")
1496 if err:
1497 return err
Russ Cox19dae072009-11-20 13:11:42 -08001498 commands.update(ui, repo)
Russ Cox45495242009-11-01 05:49:35 -08001499 sync_changes(ui, repo)
Russ Cox79a63722009-10-22 11:12:39 -07001500
Russ Cox45495242009-11-01 05:49:35 -08001501def sync_note(msg):
Russ Cox830813f2009-11-08 21:08:27 -08001502 # we run sync (pull -u) in verbose mode to get the
1503 # list of files being updated, but that drags along
1504 # a bunch of messages we don't care about.
1505 # omit them.
1506 if msg == 'resolving manifests\n':
1507 return
1508 if msg == 'searching for changes\n':
1509 return
1510 if msg == "couldn't find merge tool hgmerge\n":
Russ Cox45495242009-11-01 05:49:35 -08001511 return
1512 sys.stdout.write(msg)
Russ Cox439f9ca2009-10-22 14:14:17 -07001513
Russ Cox45495242009-11-01 05:49:35 -08001514def sync_changes(ui, repo):
Russ Coxe414fda2009-11-04 15:17:01 -08001515 # Look through recent change log descriptions to find
1516 # potential references to http://.*/our-CL-number.
1517 # Double-check them by looking at the Rietveld log.
Russ Coxc614ffe2009-11-20 00:30:38 -08001518 def Rev(rev):
Russ Coxe414fda2009-11-04 15:17:01 -08001519 desc = repo[rev].description().strip()
1520 for clname in re.findall('(?m)^http://(?:[^\n]+)/([0-9]+)$', desc):
1521 if IsLocalCL(ui, repo, clname) and IsRietveldSubmitted(ui, clname, repo[rev].hex()):
1522 ui.warn("CL %s submitted as %s; closing\n" % (clname, repo[rev]))
1523 cl, err = LoadCL(ui, repo, clname, web=False)
1524 if err != "":
1525 ui.warn("loading CL %s: %s\n" % (clname, err))
1526 continue
Russ Cox752b1702010-01-09 09:47:14 -08001527 if not cl.copied_from:
Russ Coxc614ffe2009-11-20 00:30:38 -08001528 EditDesc(cl.name, closed="checked")
Russ Coxe414fda2009-11-04 15:17:01 -08001529 cl.Delete(ui, repo)
1530
Russ Coxc614ffe2009-11-20 00:30:38 -08001531 if hgversion < '1.4':
1532 get = util.cachefunc(lambda r: repo[r].changeset())
1533 changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, [], get, {'rev': None})
1534 n = 0
1535 for st, rev, fns in changeiter:
1536 if st != 'iter':
1537 continue
1538 n += 1
1539 if n > 100:
1540 break
1541 Rev(rev)
1542 else:
1543 matchfn = cmdutil.match(repo, [], {'rev': None})
1544 def prep(ctx, fns):
1545 pass
1546 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev': None}, prep):
1547 Rev(ctx.rev())
1548
Russ Coxe414fda2009-11-04 15:17:01 -08001549 # Remove files that are not modified from the CLs in which they appear.
1550 all = LoadAllCL(ui, repo, web=False)
1551 changed = ChangedFiles(ui, repo, [], {})
1552 for _, cl in all.items():
1553 extra = Sub(cl.files, changed)
1554 if extra:
1555 ui.warn("Removing unmodified files from CL %s:\n" % (cl.name,))
1556 for f in extra:
1557 ui.warn("\t%s\n" % (f,))
1558 cl.files = Sub(cl.files, extra)
1559 cl.Flush(ui, repo)
1560 if not cl.files:
Robert Hencke1ddc2782011-03-08 12:23:06 -05001561 if not cl.copied_from:
1562 ui.warn("CL %s has no files; delete with hg change -d %s\n" % (cl.name, cl.name))
1563 else:
1564 ui.warn("CL %s has no files; delete locally with hg change -D %s\n" % (cl.name, cl.name))
Russ Coxe414fda2009-11-04 15:17:01 -08001565 return
Russ Cox439f9ca2009-10-22 14:14:17 -07001566
Russ Cox45495242009-11-01 05:49:35 -08001567def upload(ui, repo, name, **opts):
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001568 """upload diffs to the code review server
1569
1570 Uploads the current modifications for a given change to the server.
1571 """
Russ Coxe6308652010-08-25 17:52:25 -04001572 if missing_codereview:
1573 return missing_codereview
1574
Russ Cox45495242009-11-01 05:49:35 -08001575 repo.ui.quiet = True
1576 cl, err = LoadCL(ui, repo, name, web=True)
1577 if err != "":
1578 return err
1579 if not cl.local:
1580 return "cannot upload non-local change"
1581 cl.Upload(ui, repo)
1582 print "%s%s\n" % (server_url_base, cl.name)
1583 return
Russ Cox79a63722009-10-22 11:12:39 -07001584
1585review_opts = [
1586 ('r', 'reviewer', '', 'add reviewer'),
1587 ('', 'cc', '', 'add cc'),
1588 ('', 'tbr', '', 'add future reviewer'),
1589 ('m', 'message', '', 'change description (for new change)'),
1590]
1591
1592cmdtable = {
1593 # The ^ means to show this command in the help text that
1594 # is printed when running hg with no arguments.
Russ Cox79a63722009-10-22 11:12:39 -07001595 "^change": (
1596 change,
1597 [
Russ Cox45495242009-11-01 05:49:35 -08001598 ('d', 'delete', None, 'delete existing change list'),
Russ Cox790c9b52009-11-05 14:44:57 -08001599 ('D', 'deletelocal', None, 'delete locally, but do not change CL on server'),
Russ Cox79a63722009-10-22 11:12:39 -07001600 ('i', 'stdin', None, 'read change list from standard input'),
Russ Cox45495242009-11-01 05:49:35 -08001601 ('o', 'stdout', None, 'print change list to standard output'),
Russ Cox79a63722009-10-22 11:12:39 -07001602 ],
Russ Cox790c9b52009-11-05 14:44:57 -08001603 "[-d | -D] [-i] [-o] change# or FILE ..."
1604 ),
1605 "^clpatch": (
1606 clpatch,
1607 [
Russ Cox1a2418f2009-11-17 08:47:48 -08001608 ('', 'ignore_hgpatch_failure', None, 'create CL metadata even if hgpatch fails'),
Russ Cox790c9b52009-11-05 14:44:57 -08001609 ('', 'no_incoming', None, 'disable check for incoming changes'),
1610 ],
1611 "change#"
Russ Cox45495242009-11-01 05:49:35 -08001612 ),
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001613 # Would prefer to call this codereview-login, but then
1614 # hg help codereview prints the help for this command
1615 # instead of the help for the extension.
1616 "code-login": (
1617 code_login,
Russ Cox45495242009-11-01 05:49:35 -08001618 [],
1619 "",
1620 ),
Russ Cox790c9b52009-11-05 14:44:57 -08001621 "^download": (
1622 download,
1623 [],
1624 "change#"
1625 ),
Russ Cox45495242009-11-01 05:49:35 -08001626 "^file": (
1627 file,
1628 [
1629 ('d', 'delete', None, 'delete files from change list (but not repository)'),
1630 ],
1631 "[-d] change# FILE ..."
Russ Cox79a63722009-10-22 11:12:39 -07001632 ),
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001633 "^gofmt": (
1634 gofmt,
Russ Cox9df7d6e2009-11-05 08:11:44 -08001635 [
1636 ('l', 'list', None, 'list files that would change, but do not edit them'),
1637 ],
Russ Coxd8e0d9a2009-11-04 23:43:55 -08001638 "FILE ..."
1639 ),
Russ Cox79a63722009-10-22 11:12:39 -07001640 "^pending|p": (
1641 pending,
1642 [],
1643 "[FILE ...]"
1644 ),
Russ Cox79a63722009-10-22 11:12:39 -07001645 "^mail": (
1646 mail,
1647 review_opts + [
1648 ] + commands.walkopts,
1649 "[-r reviewer] [--cc cc] [change# | file ...]"
1650 ),
Russ Cox79a63722009-10-22 11:12:39 -07001651 "^submit": (
1652 submit,
1653 review_opts + [
1654 ('', 'no_incoming', None, 'disable initial incoming check (for testing)'),
Russ Cox790c9b52009-11-05 14:44:57 -08001655 ('n', 'dryrun', None, 'make change only locally (for testing)'),
Russ Cox79a63722009-10-22 11:12:39 -07001656 ] + commands.walkopts + commands.commitopts + commands.commitopts2,
1657 "[-r reviewer] [--cc cc] [change# | file ...]"
1658 ),
Russ Cox79a63722009-10-22 11:12:39 -07001659 "^sync": (
1660 sync,
Russ Coxe414fda2009-11-04 15:17:01 -08001661 [
1662 ('', 'local', None, 'do not pull changes from remote repository')
1663 ],
1664 "[--local]",
Russ Cox79a63722009-10-22 11:12:39 -07001665 ),
Russ Cox45495242009-11-01 05:49:35 -08001666 "^upload": (
1667 upload,
Russ Cox79a63722009-10-22 11:12:39 -07001668 [],
Russ Cox45495242009-11-01 05:49:35 -08001669 "change#"
Russ Cox439f9ca2009-10-22 14:14:17 -07001670 ),
Russ Cox79a63722009-10-22 11:12:39 -07001671}
1672
1673
1674#######################################################################
1675# Wrappers around upload.py for interacting with Rietveld
1676
Russ Cox79a63722009-10-22 11:12:39 -07001677# HTML form parser
1678class FormParser(HTMLParser):
1679 def __init__(self):
1680 self.map = {}
1681 self.curtag = None
1682 self.curdata = None
1683 HTMLParser.__init__(self)
1684 def handle_starttag(self, tag, attrs):
1685 if tag == "input":
1686 key = None
1687 value = ''
1688 for a in attrs:
1689 if a[0] == 'name':
1690 key = a[1]
1691 if a[0] == 'value':
1692 value = a[1]
1693 if key is not None:
1694 self.map[key] = value
1695 if tag == "textarea":
1696 key = None
1697 for a in attrs:
1698 if a[0] == 'name':
1699 key = a[1]
1700 if key is not None:
1701 self.curtag = key
1702 self.curdata = ''
1703 def handle_endtag(self, tag):
1704 if tag == "textarea" and self.curtag is not None:
1705 self.map[self.curtag] = self.curdata
1706 self.curtag = None
1707 self.curdata = None
1708 def handle_charref(self, name):
Russ Coxeea25732009-10-22 11:21:13 -07001709 self.handle_data(unichr(int(name)))
Russ Cox79a63722009-10-22 11:12:39 -07001710 def handle_entityref(self, name):
1711 import htmlentitydefs
1712 if name in htmlentitydefs.entitydefs:
1713 self.handle_data(htmlentitydefs.entitydefs[name])
1714 else:
1715 self.handle_data("&" + name + ";")
1716 def handle_data(self, data):
1717 if self.curdata is not None:
Russ Cox3b6ddd92010-11-04 13:58:32 -04001718 self.curdata += data
Russ Cox79a63722009-10-22 11:12:39 -07001719
Russ Coxe414fda2009-11-04 15:17:01 -08001720# XML parser
1721def XMLGet(ui, path):
1722 try:
1723 data = MySend(path, force_auth=False);
1724 except:
1725 ui.warn("XMLGet %s: %s\n" % (path, ExceptionDetail()))
1726 return None
1727 return ET.XML(data)
1728
1729def IsRietveldSubmitted(ui, clname, hex):
1730 feed = XMLGet(ui, "/rss/issue/" + clname)
1731 if feed is None:
1732 return False
1733 for sum in feed.findall("{http://www.w3.org/2005/Atom}entry/{http://www.w3.org/2005/Atom}summary"):
Fazlul Shahriar0e816f52010-11-01 16:37:17 -04001734 text = sum.text.strip()
Russ Coxe414fda2009-11-04 15:17:01 -08001735 m = re.match('\*\*\* Submitted as [^*]*?([0-9a-f]+) \*\*\*', text)
1736 if m is not None and len(m.group(1)) >= 8 and hex.startswith(m.group(1)):
1737 return True
1738 return False
1739
Russ Cox790c9b52009-11-05 14:44:57 -08001740def DownloadCL(ui, repo, clname):
Russ Coxe3ac0b52010-08-26 16:27:42 -04001741 set_status("downloading CL " + clname)
Russ Cox790c9b52009-11-05 14:44:57 -08001742 cl, err = LoadCL(ui, repo, clname)
1743 if err != "":
Russ Cox36c009d2011-04-04 15:47:32 -04001744 return None, None, "error loading CL %s: %s" % (clname, err)
Russ Cox72a59ce2009-11-07 17:30:40 -08001745
Russ Cox790c9b52009-11-05 14:44:57 -08001746 # Grab RSS feed to learn about CL
1747 feed = XMLGet(ui, "/rss/issue/" + clname)
1748 if feed is None:
1749 return None, None, "cannot download CL"
Russ Cox72a59ce2009-11-07 17:30:40 -08001750
Russ Cox790c9b52009-11-05 14:44:57 -08001751 # Find most recent diff
1752 diff = None
1753 prefix = 'http://' + server + '/'
1754 for link in feed.findall("{http://www.w3.org/2005/Atom}entry/{http://www.w3.org/2005/Atom}link"):
1755 if link.get('rel') != 'alternate':
1756 continue
1757 text = link.get('href')
1758 if not text.startswith(prefix) or not text.endswith('.diff'):
1759 continue
1760 diff = text[len(prefix)-1:]
1761 if diff is None:
1762 return None, None, "CL has no diff"
1763 diffdata = MySend(diff, force_auth=False)
Russ Cox72a59ce2009-11-07 17:30:40 -08001764
Russ Cox790c9b52009-11-05 14:44:57 -08001765 # Find author - first entry will be author who created CL.
1766 nick = None
1767 for author in feed.findall("{http://www.w3.org/2005/Atom}entry/{http://www.w3.org/2005/Atom}author/{http://www.w3.org/2005/Atom}name"):
Fazlul Shahriar0e816f52010-11-01 16:37:17 -04001768 nick = author.text.strip()
Russ Cox790c9b52009-11-05 14:44:57 -08001769 break
1770 if not nick:
1771 return None, None, "CL has no author"
1772
1773 # The author is just a nickname: get the real email address.
1774 try:
Russ Coxaae0aef2009-11-17 16:52:36 -08001775 # want URL-encoded nick, but without a=, and rietveld rejects + for %20.
1776 url = "/user_popup/" + urllib.urlencode({"a": nick})[2:].replace("+", "%20")
1777 data = MySend(url, force_auth=False)
Russ Cox790c9b52009-11-05 14:44:57 -08001778 except:
Russ Cox780dbdc2009-11-12 18:37:39 -08001779 ui.warn("error looking up %s: %s\n" % (nick, ExceptionDetail()))
Russ Cox752b1702010-01-09 09:47:14 -08001780 cl.copied_from = nick+"@needtofix"
Russ Cox780dbdc2009-11-12 18:37:39 -08001781 return cl, diffdata, ""
Russ Cox790c9b52009-11-05 14:44:57 -08001782 match = re.match(r"<b>(.*) \((.*)\)</b>", data)
Russ Cox780dbdc2009-11-12 18:37:39 -08001783 if not match:
1784 return None, None, "error looking up %s: cannot parse result %s" % (nick, repr(data))
1785 if match.group(1) != nick and match.group(2) != nick:
1786 return None, None, "error looking up %s: got info for %s, %s" % (nick, match.group(1), match.group(2))
Russ Cox790c9b52009-11-05 14:44:57 -08001787 email = match.group(1)
Russ Cox72a59ce2009-11-07 17:30:40 -08001788
Russ Cox790c9b52009-11-05 14:44:57 -08001789 # Print warning if email is not in CONTRIBUTORS file.
Russ Cox710028d2011-04-07 13:03:06 -04001790 him = FindContributor(ui, repo, email)
1791 me = FindContributor(ui, repo, None)
1792 if him != me:
1793 cl.copied_from = email
Russ Cox790c9b52009-11-05 14:44:57 -08001794
1795 return cl, diffdata, ""
1796
Russ Cox7db2c792009-11-17 23:23:18 -08001797def MySend(request_path, payload=None,
Russ Coxf7d87f32010-08-26 18:56:29 -04001798 content_type="application/octet-stream",
1799 timeout=None, force_auth=True,
1800 **kwargs):
1801 """Run MySend1 maybe twice, because Rietveld is unreliable."""
1802 try:
1803 return MySend1(request_path, payload, content_type, timeout, force_auth, **kwargs)
1804 except Exception, e:
Russ Cox36c009d2011-04-04 15:47:32 -04001805 if type(e) != urllib2.HTTPError or e.code != 500: # only retry on HTTP 500 error
Russ Coxf7d87f32010-08-26 18:56:29 -04001806 raise
1807 print >>sys.stderr, "Loading "+request_path+": "+ExceptionDetail()+"; trying again in 2 seconds."
1808 time.sleep(2)
1809 return MySend1(request_path, payload, content_type, timeout, force_auth, **kwargs)
Russ Cox7db2c792009-11-17 23:23:18 -08001810
Russ Cox45495242009-11-01 05:49:35 -08001811# Like upload.py Send but only authenticates when the
Russ Cox79a63722009-10-22 11:12:39 -07001812# redirect is to www.google.com/accounts. This keeps
1813# unnecessary redirects from happening during testing.
Russ Cox7db2c792009-11-17 23:23:18 -08001814def MySend1(request_path, payload=None,
Russ Cox3b226f92010-08-26 18:24:14 -04001815 content_type="application/octet-stream",
1816 timeout=None, force_auth=True,
1817 **kwargs):
1818 """Sends an RPC and returns the response.
Russ Cox79a63722009-10-22 11:12:39 -07001819
Russ Cox3b226f92010-08-26 18:24:14 -04001820 Args:
1821 request_path: The path to send the request to, eg /api/appversion/create.
1822 payload: The body of the request, or None to send an empty request.
1823 content_type: The Content-Type header to use.
1824 timeout: timeout in seconds; default None i.e. no timeout.
1825 (Note: for large requests on OS X, the timeout doesn't work right.)
1826 kwargs: Any keyword arguments are converted into query string parameters.
Russ Cox79a63722009-10-22 11:12:39 -07001827
Russ Cox3b226f92010-08-26 18:24:14 -04001828 Returns:
1829 The response body, as a string.
1830 """
1831 # TODO: Don't require authentication. Let the server say
1832 # whether it is necessary.
1833 global rpc
1834 if rpc == None:
1835 rpc = GetRpcServer(upload_options)
1836 self = rpc
1837 if not self.authenticated and force_auth:
1838 self._Authenticate()
1839 if request_path is None:
1840 return
Russ Cox79a63722009-10-22 11:12:39 -07001841
Russ Cox3b226f92010-08-26 18:24:14 -04001842 old_timeout = socket.getdefaulttimeout()
1843 socket.setdefaulttimeout(timeout)
1844 try:
1845 tries = 0
1846 while True:
1847 tries += 1
1848 args = dict(kwargs)
1849 url = "http://%s%s" % (self.host, request_path)
1850 if args:
1851 url += "?" + urllib.urlencode(args)
1852 req = self._CreateRequest(url=url, data=payload)
1853 req.add_header("Content-Type", content_type)
1854 try:
1855 f = self.opener.open(req)
1856 response = f.read()
1857 f.close()
1858 # Translate \r\n into \n, because Rietveld doesn't.
1859 response = response.replace('\r\n', '\n')
Russ Cox3b6ddd92010-11-04 13:58:32 -04001860 # who knows what urllib will give us
1861 if type(response) == unicode:
1862 response = response.encode("utf-8")
1863 typecheck(response, str)
Russ Cox3b226f92010-08-26 18:24:14 -04001864 return response
1865 except urllib2.HTTPError, e:
1866 if tries > 3:
1867 raise
1868 elif e.code == 401:
1869 self._Authenticate()
1870 elif e.code == 302:
1871 loc = e.info()["location"]
1872 if not loc.startswith('https://www.google.com/a') or loc.find('/ServiceLogin') < 0:
1873 return ''
1874 self._Authenticate()
1875 else:
1876 raise
1877 finally:
1878 socket.setdefaulttimeout(old_timeout)
Russ Cox79a63722009-10-22 11:12:39 -07001879
1880def GetForm(url):
1881 f = FormParser()
Russ Coxdc9a02f2011-02-01 14:17:41 -05001882 f.feed(ustr(MySend(url))) # f.feed wants unicode
Russ Cox79a63722009-10-22 11:12:39 -07001883 f.close()
Russ Cox3b6ddd92010-11-04 13:58:32 -04001884 # convert back to utf-8 to restore sanity
1885 m = {}
Russ Cox79a63722009-10-22 11:12:39 -07001886 for k,v in f.map.items():
Russ Cox3b6ddd92010-11-04 13:58:32 -04001887 m[k.encode("utf-8")] = v.replace("\r\n", "\n").encode("utf-8")
1888 return m
Russ Cox79a63722009-10-22 11:12:39 -07001889
Russ Cox790c9b52009-11-05 14:44:57 -08001890# Fetch the settings for the CL, like reviewer and CC list, by
1891# scraping the Rietveld editing forms.
Russ Cox79a63722009-10-22 11:12:39 -07001892def GetSettings(issue):
Russ Coxe3ac0b52010-08-26 16:27:42 -04001893 set_status("getting issue metadata from web")
Russ Cox790c9b52009-11-05 14:44:57 -08001894 # The /issue/edit page has everything but only the
1895 # CL owner is allowed to fetch it (and submit it).
1896 f = None
1897 try:
1898 f = GetForm("/" + issue + "/edit")
1899 except:
1900 pass
Russ Cox45495242009-11-01 05:49:35 -08001901 if not f or 'reviewers' not in f:
Russ Cox790c9b52009-11-05 14:44:57 -08001902 # Maybe we're not the CL owner. Fall back to the
1903 # /publish page, which has the reviewer and CC lists,
1904 # and then fetch the description separately.
Russ Cox79a63722009-10-22 11:12:39 -07001905 f = GetForm("/" + issue + "/publish")
Russ Cox790c9b52009-11-05 14:44:57 -08001906 f['description'] = MySend("/"+issue+"/description", force_auth=False)
Russ Cox79a63722009-10-22 11:12:39 -07001907 return f
1908
Russ Cox79a63722009-10-22 11:12:39 -07001909def EditDesc(issue, subject=None, desc=None, reviewers=None, cc=None, closed=None):
Russ Coxe3ac0b52010-08-26 16:27:42 -04001910 set_status("uploading change to description")
Russ Cox79a63722009-10-22 11:12:39 -07001911 form_fields = GetForm("/" + issue + "/edit")
1912 if subject is not None:
1913 form_fields['subject'] = subject
1914 if desc is not None:
1915 form_fields['description'] = desc
1916 if reviewers is not None:
1917 form_fields['reviewers'] = reviewers
1918 if cc is not None:
1919 form_fields['cc'] = cc
1920 if closed is not None:
1921 form_fields['closed'] = closed
1922 ctype, body = EncodeMultipartFormData(form_fields.items(), [])
1923 response = MySend("/" + issue + "/edit", body, content_type=ctype)
1924 if response != "":
1925 print >>sys.stderr, "Error editing description:\n" + "Sent form: \n", form_fields, "\n", response
1926 sys.exit(2)
1927
Russ Cox69f893a2009-12-02 09:10:59 -08001928def PostMessage(ui, issue, message, reviewers=None, cc=None, send_mail=True, subject=None):
Russ Coxe3ac0b52010-08-26 16:27:42 -04001929 set_status("uploading message")
Russ Cox79a63722009-10-22 11:12:39 -07001930 form_fields = GetForm("/" + issue + "/publish")
1931 if reviewers is not None:
1932 form_fields['reviewers'] = reviewers
1933 if cc is not None:
1934 form_fields['cc'] = cc
Russ Cox69f893a2009-12-02 09:10:59 -08001935 if send_mail:
1936 form_fields['send_mail'] = "checked"
1937 else:
1938 del form_fields['send_mail']
Russ Cox79a63722009-10-22 11:12:39 -07001939 if subject is not None:
1940 form_fields['subject'] = subject
1941 form_fields['message'] = message
Russ Cox69f893a2009-12-02 09:10:59 -08001942
1943 form_fields['message_only'] = '1' # Don't include draft comments
1944 if reviewers is not None or cc is not None:
1945 form_fields['message_only'] = '' # Must set '' in order to override cc/reviewer
1946 ctype = "applications/x-www-form-urlencoded"
1947 body = urllib.urlencode(form_fields)
Russ Cox79a63722009-10-22 11:12:39 -07001948 response = MySend("/" + issue + "/publish", body, content_type=ctype)
1949 if response != "":
1950 print response
1951 sys.exit(2)
1952
1953class opt(object):
1954 pass
1955
Russ Coxe6308652010-08-25 17:52:25 -04001956def disabled(*opts, **kwopts):
1957 raise util.Abort("commit is disabled when codereview is in use")
1958
Russ Cox45495242009-11-01 05:49:35 -08001959def RietveldSetup(ui, repo):
Russ Cox93f614f2010-06-30 23:34:11 -07001960 global defaultcc, upload_options, rpc, server, server_url_base, force_google_account, verbosity, contributors
Russ Coxe6308652010-08-25 17:52:25 -04001961 global missing_codereview
Russ Cox84ac3572010-01-13 09:09:06 -08001962
Russ Coxe6308652010-08-25 17:52:25 -04001963 repo_config_path = ''
Russ Cox84ac3572010-01-13 09:09:06 -08001964 # Read repository-specific options from lib/codereview/codereview.cfg
1965 try:
Russ Coxe6308652010-08-25 17:52:25 -04001966 repo_config_path = repo.root + '/lib/codereview/codereview.cfg'
1967 f = open(repo_config_path)
Russ Cox84ac3572010-01-13 09:09:06 -08001968 for line in f:
1969 if line.startswith('defaultcc: '):
1970 defaultcc = SplitCommaSpace(line[10:])
1971 except:
Russ Coxbbf925a2010-07-26 17:33:50 -07001972 # If there are no options, chances are good this is not
1973 # a code review repository; stop now before we foul
1974 # things up even worse. Might also be that repo doesn't
1975 # even have a root. See issue 959.
Russ Coxe6308652010-08-25 17:52:25 -04001976 if repo_config_path == '':
1977 missing_codereview = 'codereview disabled: repository has no root'
1978 else:
1979 missing_codereview = 'codereview disabled: cannot open ' + repo_config_path
Russ Coxbbf925a2010-07-26 17:33:50 -07001980 return
Russ Cox79a63722009-10-22 11:12:39 -07001981
Russ Coxe6308652010-08-25 17:52:25 -04001982 # Should only modify repository with hg submit.
1983 # Disable the built-in Mercurial commands that might
1984 # trip things up.
1985 cmdutil.commit = disabled
1986
Russ Cox93f614f2010-06-30 23:34:11 -07001987 try:
1988 f = open(repo.root + '/CONTRIBUTORS', 'r')
1989 except:
1990 raise util.Abort("cannot open %s: %s" % (repo.root+'/CONTRIBUTORS', ExceptionDetail()))
1991 for line in f:
1992 # CONTRIBUTORS is a list of lines like:
1993 # Person <email>
1994 # Person <email> <alt-email>
1995 # The first email address is the one used in commit logs.
1996 if line.startswith('#'):
1997 continue
1998 m = re.match(r"([^<>]+\S)\s+(<[^<>\s]+>)((\s+<[^<>\s]+>)*)\s*$", line)
1999 if m:
2000 name = m.group(1)
2001 email = m.group(2)[1:-1]
2002 contributors[email.lower()] = (name, email)
2003 for extra in m.group(3).split():
2004 contributors[extra[1:-1].lower()] = (name, email)
Russ Coxd8e0d9a2009-11-04 23:43:55 -08002005
Russ Cox45495242009-11-01 05:49:35 -08002006 if not ui.verbose:
2007 verbosity = 0
Russ Cox79a63722009-10-22 11:12:39 -07002008
2009 # Config options.
2010 x = ui.config("codereview", "server")
2011 if x is not None:
2012 server = x
Russ Cox45495242009-11-01 05:49:35 -08002013
Russ Cox79a63722009-10-22 11:12:39 -07002014 # TODO(rsc): Take from ui.username?
2015 email = None
2016 x = ui.config("codereview", "email")
2017 if x is not None:
2018 email = x
2019
Russ Cox79a63722009-10-22 11:12:39 -07002020 server_url_base = "http://" + server + "/"
Russ Cox506ce112009-11-04 03:15:24 -08002021
Russ Cox79a63722009-10-22 11:12:39 -07002022 testing = ui.config("codereview", "testing")
Russ Cox45495242009-11-01 05:49:35 -08002023 force_google_account = ui.configbool("codereview", "force_google_account", False)
Russ Cox79a63722009-10-22 11:12:39 -07002024
2025 upload_options = opt()
2026 upload_options.email = email
2027 upload_options.host = None
2028 upload_options.verbose = 0
2029 upload_options.description = None
2030 upload_options.description_file = None
2031 upload_options.reviewers = None
Russ Cox84ac3572010-01-13 09:09:06 -08002032 upload_options.cc = None
Russ Cox79a63722009-10-22 11:12:39 -07002033 upload_options.message = None
2034 upload_options.issue = None
2035 upload_options.download_base = False
2036 upload_options.revision = None
2037 upload_options.send_mail = False
2038 upload_options.vcs = None
2039 upload_options.server = server
2040 upload_options.save_cookies = True
Russ Cox45495242009-11-01 05:49:35 -08002041
Russ Cox79a63722009-10-22 11:12:39 -07002042 if testing:
2043 upload_options.save_cookies = False
2044 upload_options.email = "test@example.com"
2045
2046 rpc = None
2047
2048#######################################################################
Russ Cox3b226f92010-08-26 18:24:14 -04002049# http://codereview.appspot.com/static/upload.py, heavily edited.
Russ Cox79a63722009-10-22 11:12:39 -07002050
Russ Cox79a63722009-10-22 11:12:39 -07002051#!/usr/bin/env python
2052#
2053# Copyright 2007 Google Inc.
2054#
2055# Licensed under the Apache License, Version 2.0 (the "License");
2056# you may not use this file except in compliance with the License.
2057# You may obtain a copy of the License at
2058#
Russ Coxf7d87f32010-08-26 18:56:29 -04002059# http://www.apache.org/licenses/LICENSE-2.0
Russ Cox79a63722009-10-22 11:12:39 -07002060#
2061# Unless required by applicable law or agreed to in writing, software
2062# distributed under the License is distributed on an "AS IS" BASIS,
2063# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2064# See the License for the specific language governing permissions and
2065# limitations under the License.
2066
2067"""Tool for uploading diffs from a version control system to the codereview app.
2068
2069Usage summary: upload.py [options] [-- diff_options]
2070
2071Diff options are passed to the diff command of the underlying system.
2072
2073Supported version control systems:
Russ Cox3b226f92010-08-26 18:24:14 -04002074 Git
2075 Mercurial
2076 Subversion
Russ Cox79a63722009-10-22 11:12:39 -07002077
2078It is important for Git/Mercurial users to specify a tree/node/branch to diff
2079against by using the '--rev' option.
2080"""
2081# This code is derived from appcfg.py in the App Engine SDK (open source),
2082# and from ASPN recipe #146306.
2083
2084import cookielib
2085import getpass
2086import logging
2087import mimetypes
2088import optparse
2089import os
2090import re
2091import socket
2092import subprocess
2093import sys
2094import urllib
2095import urllib2
2096import urlparse
2097
2098# The md5 module was deprecated in Python 2.5.
2099try:
Russ Cox3b226f92010-08-26 18:24:14 -04002100 from hashlib import md5
Russ Cox79a63722009-10-22 11:12:39 -07002101except ImportError:
Russ Cox3b226f92010-08-26 18:24:14 -04002102 from md5 import md5
Russ Cox79a63722009-10-22 11:12:39 -07002103
2104try:
Russ Cox3b226f92010-08-26 18:24:14 -04002105 import readline
Russ Cox79a63722009-10-22 11:12:39 -07002106except ImportError:
Russ Cox3b226f92010-08-26 18:24:14 -04002107 pass
Russ Cox79a63722009-10-22 11:12:39 -07002108
2109# The logging verbosity:
2110# 0: Errors only.
2111# 1: Status messages.
2112# 2: Info logs.
2113# 3: Debug logs.
2114verbosity = 1
2115
2116# Max size of patch or base file.
2117MAX_UPLOAD_SIZE = 900 * 1024
2118
Russ Cox79a63722009-10-22 11:12:39 -07002119# whitelist for non-binary filetypes which do not start with "text/"
2120# .mm (Objective-C) shows up as application/x-freemind on my Linux box.
Russ Cox3b226f92010-08-26 18:24:14 -04002121TEXT_MIMETYPES = [
2122 'application/javascript',
2123 'application/x-javascript',
2124 'application/x-freemind'
2125]
Russ Cox79a63722009-10-22 11:12:39 -07002126
2127def GetEmail(prompt):
Russ Cox3b226f92010-08-26 18:24:14 -04002128 """Prompts the user for their email address and returns it.
Russ Cox79a63722009-10-22 11:12:39 -07002129
Russ Cox3b226f92010-08-26 18:24:14 -04002130 The last used email address is saved to a file and offered up as a suggestion
2131 to the user. If the user presses enter without typing in anything the last
2132 used email address is used. If the user enters a new address, it is saved
2133 for next time we prompt.
Russ Cox79a63722009-10-22 11:12:39 -07002134
Russ Cox3b226f92010-08-26 18:24:14 -04002135 """
2136 last_email_file_name = os.path.expanduser("~/.last_codereview_email_address")
2137 last_email = ""
2138 if os.path.exists(last_email_file_name):
2139 try:
2140 last_email_file = open(last_email_file_name, "r")
2141 last_email = last_email_file.readline().strip("\n")
2142 last_email_file.close()
2143 prompt += " [%s]" % last_email
2144 except IOError, e:
2145 pass
2146 email = raw_input(prompt + ": ").strip()
2147 if email:
2148 try:
2149 last_email_file = open(last_email_file_name, "w")
2150 last_email_file.write(email)
2151 last_email_file.close()
2152 except IOError, e:
2153 pass
2154 else:
2155 email = last_email
2156 return email
Russ Cox79a63722009-10-22 11:12:39 -07002157
2158
2159def StatusUpdate(msg):
Russ Cox3b226f92010-08-26 18:24:14 -04002160 """Print a status message to stdout.
Russ Cox79a63722009-10-22 11:12:39 -07002161
Russ Cox3b226f92010-08-26 18:24:14 -04002162 If 'verbosity' is greater than 0, print the message.
Russ Cox79a63722009-10-22 11:12:39 -07002163
Russ Cox3b226f92010-08-26 18:24:14 -04002164 Args:
2165 msg: The string to print.
2166 """
2167 if verbosity > 0:
2168 print msg
Russ Cox79a63722009-10-22 11:12:39 -07002169
2170
2171def ErrorExit(msg):
Russ Cox3b226f92010-08-26 18:24:14 -04002172 """Print an error message to stderr and exit."""
2173 print >>sys.stderr, msg
2174 sys.exit(1)
Russ Cox79a63722009-10-22 11:12:39 -07002175
2176
2177class ClientLoginError(urllib2.HTTPError):
Russ Cox3b226f92010-08-26 18:24:14 -04002178 """Raised to indicate there was an error authenticating with ClientLogin."""
Russ Cox79a63722009-10-22 11:12:39 -07002179
Russ Cox3b226f92010-08-26 18:24:14 -04002180 def __init__(self, url, code, msg, headers, args):
2181 urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
2182 self.args = args
2183 self.reason = args["Error"]
Russ Cox79a63722009-10-22 11:12:39 -07002184
2185
2186class AbstractRpcServer(object):
Russ Cox3b226f92010-08-26 18:24:14 -04002187 """Provides a common interface for a simple RPC server."""
Russ Cox79a63722009-10-22 11:12:39 -07002188
Russ Cox3b226f92010-08-26 18:24:14 -04002189 def __init__(self, host, auth_function, host_override=None, extra_headers={}, save_cookies=False):
2190 """Creates a new HttpRpcServer.
Russ Cox79a63722009-10-22 11:12:39 -07002191
Russ Cox3b226f92010-08-26 18:24:14 -04002192 Args:
2193 host: The host to send requests to.
2194 auth_function: A function that takes no arguments and returns an
2195 (email, password) tuple when called. Will be called if authentication
2196 is required.
2197 host_override: The host header to send to the server (defaults to host).
2198 extra_headers: A dict of extra headers to append to every request.
2199 save_cookies: If True, save the authentication cookies to local disk.
2200 If False, use an in-memory cookiejar instead. Subclasses must
2201 implement this functionality. Defaults to False.
2202 """
2203 self.host = host
2204 self.host_override = host_override
2205 self.auth_function = auth_function
2206 self.authenticated = False
2207 self.extra_headers = extra_headers
2208 self.save_cookies = save_cookies
2209 self.opener = self._GetOpener()
2210 if self.host_override:
2211 logging.info("Server: %s; Host: %s", self.host, self.host_override)
2212 else:
2213 logging.info("Server: %s", self.host)
Russ Cox79a63722009-10-22 11:12:39 -07002214
Russ Cox3b226f92010-08-26 18:24:14 -04002215 def _GetOpener(self):
2216 """Returns an OpenerDirector for making HTTP requests.
Russ Cox79a63722009-10-22 11:12:39 -07002217
Russ Cox3b226f92010-08-26 18:24:14 -04002218 Returns:
2219 A urllib2.OpenerDirector object.
2220 """
2221 raise NotImplementedError()
Russ Cox79a63722009-10-22 11:12:39 -07002222
Russ Cox3b226f92010-08-26 18:24:14 -04002223 def _CreateRequest(self, url, data=None):
2224 """Creates a new urllib request."""
2225 logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
2226 req = urllib2.Request(url, data=data)
2227 if self.host_override:
2228 req.add_header("Host", self.host_override)
2229 for key, value in self.extra_headers.iteritems():
2230 req.add_header(key, value)
2231 return req
Russ Cox79a63722009-10-22 11:12:39 -07002232
Russ Cox3b226f92010-08-26 18:24:14 -04002233 def _GetAuthToken(self, email, password):
2234 """Uses ClientLogin to authenticate the user, returning an auth token.
Russ Cox79a63722009-10-22 11:12:39 -07002235
Russ Cox3b226f92010-08-26 18:24:14 -04002236 Args:
2237 email: The user's email address
2238 password: The user's password
Russ Cox79a63722009-10-22 11:12:39 -07002239
Russ Cox3b226f92010-08-26 18:24:14 -04002240 Raises:
2241 ClientLoginError: If there was an error authenticating with ClientLogin.
2242 HTTPError: If there was some other form of HTTP error.
Russ Cox79a63722009-10-22 11:12:39 -07002243
Russ Cox3b226f92010-08-26 18:24:14 -04002244 Returns:
2245 The authentication token returned by ClientLogin.
2246 """
2247 account_type = "GOOGLE"
2248 if self.host.endswith(".google.com") and not force_google_account:
2249 # Needed for use inside Google.
2250 account_type = "HOSTED"
2251 req = self._CreateRequest(
2252 url="https://www.google.com/accounts/ClientLogin",
2253 data=urllib.urlencode({
2254 "Email": email,
2255 "Passwd": password,
2256 "service": "ah",
2257 "source": "rietveld-codereview-upload",
2258 "accountType": account_type,
2259 }),
2260 )
2261 try:
2262 response = self.opener.open(req)
2263 response_body = response.read()
2264 response_dict = dict(x.split("=") for x in response_body.split("\n") if x)
2265 return response_dict["Auth"]
2266 except urllib2.HTTPError, e:
2267 if e.code == 403:
2268 body = e.read()
2269 response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
2270 raise ClientLoginError(req.get_full_url(), e.code, e.msg, e.headers, response_dict)
2271 else:
2272 raise
Russ Cox79a63722009-10-22 11:12:39 -07002273
Russ Cox3b226f92010-08-26 18:24:14 -04002274 def _GetAuthCookie(self, auth_token):
2275 """Fetches authentication cookies for an authentication token.
Russ Cox79a63722009-10-22 11:12:39 -07002276
Russ Cox3b226f92010-08-26 18:24:14 -04002277 Args:
2278 auth_token: The authentication token returned by ClientLogin.
Russ Cox79a63722009-10-22 11:12:39 -07002279
Russ Cox3b226f92010-08-26 18:24:14 -04002280 Raises:
2281 HTTPError: If there was an error fetching the authentication cookies.
2282 """
2283 # This is a dummy value to allow us to identify when we're successful.
2284 continue_location = "http://localhost/"
2285 args = {"continue": continue_location, "auth": auth_token}
2286 req = self._CreateRequest("http://%s/_ah/login?%s" % (self.host, urllib.urlencode(args)))
2287 try:
2288 response = self.opener.open(req)
2289 except urllib2.HTTPError, e:
2290 response = e
2291 if (response.code != 302 or
2292 response.info()["location"] != continue_location):
2293 raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg, response.headers, response.fp)
2294 self.authenticated = True
Russ Cox79a63722009-10-22 11:12:39 -07002295
Russ Cox3b226f92010-08-26 18:24:14 -04002296 def _Authenticate(self):
2297 """Authenticates the user.
Russ Cox79a63722009-10-22 11:12:39 -07002298
Russ Cox3b226f92010-08-26 18:24:14 -04002299 The authentication process works as follows:
2300 1) We get a username and password from the user
2301 2) We use ClientLogin to obtain an AUTH token for the user
2302 (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
2303 3) We pass the auth token to /_ah/login on the server to obtain an
2304 authentication cookie. If login was successful, it tries to redirect
2305 us to the URL we provided.
Russ Cox79a63722009-10-22 11:12:39 -07002306
Russ Cox3b226f92010-08-26 18:24:14 -04002307 If we attempt to access the upload API without first obtaining an
2308 authentication cookie, it returns a 401 response (or a 302) and
2309 directs us to authenticate ourselves with ClientLogin.
2310 """
2311 for i in range(3):
2312 credentials = self.auth_function()
2313 try:
2314 auth_token = self._GetAuthToken(credentials[0], credentials[1])
2315 except ClientLoginError, e:
2316 if e.reason == "BadAuthentication":
2317 print >>sys.stderr, "Invalid username or password."
2318 continue
2319 if e.reason == "CaptchaRequired":
2320 print >>sys.stderr, (
2321 "Please go to\n"
2322 "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
2323 "and verify you are a human. Then try again.")
2324 break
2325 if e.reason == "NotVerified":
2326 print >>sys.stderr, "Account not verified."
2327 break
2328 if e.reason == "TermsNotAgreed":
2329 print >>sys.stderr, "User has not agreed to TOS."
2330 break
2331 if e.reason == "AccountDeleted":
2332 print >>sys.stderr, "The user account has been deleted."
2333 break
2334 if e.reason == "AccountDisabled":
2335 print >>sys.stderr, "The user account has been disabled."
2336 break
2337 if e.reason == "ServiceDisabled":
2338 print >>sys.stderr, "The user's access to the service has been disabled."
2339 break
2340 if e.reason == "ServiceUnavailable":
2341 print >>sys.stderr, "The service is not available; try again later."
2342 break
2343 raise
2344 self._GetAuthCookie(auth_token)
2345 return
Russ Cox79a63722009-10-22 11:12:39 -07002346
Russ Cox3b226f92010-08-26 18:24:14 -04002347 def Send(self, request_path, payload=None,
2348 content_type="application/octet-stream",
2349 timeout=None,
2350 **kwargs):
2351 """Sends an RPC and returns the response.
Russ Cox79a63722009-10-22 11:12:39 -07002352
Russ Cox3b226f92010-08-26 18:24:14 -04002353 Args:
2354 request_path: The path to send the request to, eg /api/appversion/create.
2355 payload: The body of the request, or None to send an empty request.
2356 content_type: The Content-Type header to use.
2357 timeout: timeout in seconds; default None i.e. no timeout.
2358 (Note: for large requests on OS X, the timeout doesn't work right.)
2359 kwargs: Any keyword arguments are converted into query string parameters.
Russ Cox79a63722009-10-22 11:12:39 -07002360
Russ Cox3b226f92010-08-26 18:24:14 -04002361 Returns:
2362 The response body, as a string.
2363 """
2364 # TODO: Don't require authentication. Let the server say
2365 # whether it is necessary.
2366 if not self.authenticated:
2367 self._Authenticate()
Russ Cox79a63722009-10-22 11:12:39 -07002368
Russ Cox3b226f92010-08-26 18:24:14 -04002369 old_timeout = socket.getdefaulttimeout()
2370 socket.setdefaulttimeout(timeout)
2371 try:
2372 tries = 0
2373 while True:
2374 tries += 1
2375 args = dict(kwargs)
2376 url = "http://%s%s" % (self.host, request_path)
2377 if args:
2378 url += "?" + urllib.urlencode(args)
2379 req = self._CreateRequest(url=url, data=payload)
2380 req.add_header("Content-Type", content_type)
2381 try:
2382 f = self.opener.open(req)
2383 response = f.read()
2384 f.close()
2385 return response
2386 except urllib2.HTTPError, e:
2387 if tries > 3:
2388 raise
2389 elif e.code == 401 or e.code == 302:
2390 self._Authenticate()
2391 else:
2392 raise
2393 finally:
2394 socket.setdefaulttimeout(old_timeout)
Russ Cox79a63722009-10-22 11:12:39 -07002395
2396
2397class HttpRpcServer(AbstractRpcServer):
Russ Cox3b226f92010-08-26 18:24:14 -04002398 """Provides a simplified RPC-style interface for HTTP requests."""
Russ Cox79a63722009-10-22 11:12:39 -07002399
Russ Cox3b226f92010-08-26 18:24:14 -04002400 def _Authenticate(self):
2401 """Save the cookie jar after authentication."""
2402 super(HttpRpcServer, self)._Authenticate()
2403 if self.save_cookies:
2404 StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
2405 self.cookie_jar.save()
Russ Cox79a63722009-10-22 11:12:39 -07002406
Russ Cox3b226f92010-08-26 18:24:14 -04002407 def _GetOpener(self):
2408 """Returns an OpenerDirector that supports cookies and ignores redirects.
Russ Cox79a63722009-10-22 11:12:39 -07002409
Russ Cox3b226f92010-08-26 18:24:14 -04002410 Returns:
2411 A urllib2.OpenerDirector object.
2412 """
2413 opener = urllib2.OpenerDirector()
2414 opener.add_handler(urllib2.ProxyHandler())
2415 opener.add_handler(urllib2.UnknownHandler())
2416 opener.add_handler(urllib2.HTTPHandler())
2417 opener.add_handler(urllib2.HTTPDefaultErrorHandler())
2418 opener.add_handler(urllib2.HTTPSHandler())
2419 opener.add_handler(urllib2.HTTPErrorProcessor())
2420 if self.save_cookies:
2421 self.cookie_file = os.path.expanduser("~/.codereview_upload_cookies_" + server)
2422 self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
2423 if os.path.exists(self.cookie_file):
2424 try:
2425 self.cookie_jar.load()
2426 self.authenticated = True
2427 StatusUpdate("Loaded authentication cookies from %s" % self.cookie_file)
2428 except (cookielib.LoadError, IOError):
2429 # Failed to load cookies - just ignore them.
2430 pass
2431 else:
2432 # Create an empty cookie file with mode 600
2433 fd = os.open(self.cookie_file, os.O_CREAT, 0600)
2434 os.close(fd)
2435 # Always chmod the cookie file
2436 os.chmod(self.cookie_file, 0600)
2437 else:
2438 # Don't save cookies across runs of update.py.
2439 self.cookie_jar = cookielib.CookieJar()
2440 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
2441 return opener
Russ Cox79a63722009-10-22 11:12:39 -07002442
2443
2444def GetRpcServer(options):
Russ Cox3b226f92010-08-26 18:24:14 -04002445 """Returns an instance of an AbstractRpcServer.
Russ Cox79a63722009-10-22 11:12:39 -07002446
Russ Cox3b226f92010-08-26 18:24:14 -04002447 Returns:
2448 A new AbstractRpcServer, on which RPC calls can be made.
2449 """
Russ Cox79a63722009-10-22 11:12:39 -07002450
Russ Cox3b226f92010-08-26 18:24:14 -04002451 rpc_server_class = HttpRpcServer
Russ Cox79a63722009-10-22 11:12:39 -07002452
Russ Cox3b226f92010-08-26 18:24:14 -04002453 def GetUserCredentials():
2454 """Prompts the user for a username and password."""
Russ Cox17fc373a2011-01-24 14:14:26 -05002455 # Disable status prints so they don't obscure the password prompt.
2456 global global_status
2457 st = global_status
2458 global_status = None
2459
Russ Cox3b226f92010-08-26 18:24:14 -04002460 email = options.email
2461 if email is None:
2462 email = GetEmail("Email (login for uploading to %s)" % options.server)
2463 password = getpass.getpass("Password for %s: " % email)
Russ Cox17fc373a2011-01-24 14:14:26 -05002464
2465 # Put status back.
2466 global_status = st
Russ Cox3b226f92010-08-26 18:24:14 -04002467 return (email, password)
Russ Cox79a63722009-10-22 11:12:39 -07002468
Russ Cox3b226f92010-08-26 18:24:14 -04002469 # If this is the dev_appserver, use fake authentication.
2470 host = (options.host or options.server).lower()
2471 if host == "localhost" or host.startswith("localhost:"):
2472 email = options.email
2473 if email is None:
2474 email = "test@example.com"
2475 logging.info("Using debug user %s. Override with --email" % email)
2476 server = rpc_server_class(
2477 options.server,
2478 lambda: (email, "password"),
2479 host_override=options.host,
2480 extra_headers={"Cookie": 'dev_appserver_login="%s:False"' % email},
2481 save_cookies=options.save_cookies)
2482 # Don't try to talk to ClientLogin.
2483 server.authenticated = True
2484 return server
Russ Cox79a63722009-10-22 11:12:39 -07002485
Russ Cox3b226f92010-08-26 18:24:14 -04002486 return rpc_server_class(options.server, GetUserCredentials,
2487 host_override=options.host, save_cookies=options.save_cookies)
Russ Cox79a63722009-10-22 11:12:39 -07002488
2489
2490def EncodeMultipartFormData(fields, files):
Russ Cox3b226f92010-08-26 18:24:14 -04002491 """Encode form fields for multipart/form-data.
Russ Cox79a63722009-10-22 11:12:39 -07002492
Russ Cox3b226f92010-08-26 18:24:14 -04002493 Args:
2494 fields: A sequence of (name, value) elements for regular form fields.
2495 files: A sequence of (name, filename, value) elements for data to be
2496 uploaded as files.
2497 Returns:
2498 (content_type, body) ready for httplib.HTTP instance.
Russ Cox79a63722009-10-22 11:12:39 -07002499
Russ Cox3b226f92010-08-26 18:24:14 -04002500 Source:
2501 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
2502 """
2503 BOUNDARY = '-M-A-G-I-C---B-O-U-N-D-A-R-Y-'
2504 CRLF = '\r\n'
2505 lines = []
2506 for (key, value) in fields:
Russ Cox3b6ddd92010-11-04 13:58:32 -04002507 typecheck(key, str)
2508 typecheck(value, str)
Russ Cox3b226f92010-08-26 18:24:14 -04002509 lines.append('--' + BOUNDARY)
2510 lines.append('Content-Disposition: form-data; name="%s"' % key)
2511 lines.append('')
Russ Cox3b226f92010-08-26 18:24:14 -04002512 lines.append(value)
2513 for (key, filename, value) in files:
Russ Cox3b6ddd92010-11-04 13:58:32 -04002514 typecheck(key, str)
2515 typecheck(filename, str)
2516 typecheck(value, str)
Russ Cox3b226f92010-08-26 18:24:14 -04002517 lines.append('--' + BOUNDARY)
2518 lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
2519 lines.append('Content-Type: %s' % GetContentType(filename))
2520 lines.append('')
2521 lines.append(value)
2522 lines.append('--' + BOUNDARY + '--')
2523 lines.append('')
2524 body = CRLF.join(lines)
2525 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
2526 return content_type, body
Russ Cox79a63722009-10-22 11:12:39 -07002527
2528
2529def GetContentType(filename):
Russ Cox3b226f92010-08-26 18:24:14 -04002530 """Helper to guess the content-type from the filename."""
2531 return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
Russ Cox79a63722009-10-22 11:12:39 -07002532
2533
2534# Use a shell for subcommands on Windows to get a PATH search.
2535use_shell = sys.platform.startswith("win")
2536
2537def RunShellWithReturnCode(command, print_output=False,
Russ Cox3b226f92010-08-26 18:24:14 -04002538 universal_newlines=True, env=os.environ):
2539 """Executes a command and returns the output from stdout and the return code.
Russ Cox79a63722009-10-22 11:12:39 -07002540
Russ Cox3b226f92010-08-26 18:24:14 -04002541 Args:
2542 command: Command to execute.
2543 print_output: If True, the output is printed to stdout.
2544 If False, both stdout and stderr are ignored.
2545 universal_newlines: Use universal_newlines flag (default: True).
Russ Cox79a63722009-10-22 11:12:39 -07002546
Russ Cox3b226f92010-08-26 18:24:14 -04002547 Returns:
2548 Tuple (output, return code)
2549 """
2550 logging.info("Running %s", command)
2551 p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
2552 shell=use_shell, universal_newlines=universal_newlines, env=env)
2553 if print_output:
2554 output_array = []
2555 while True:
2556 line = p.stdout.readline()
2557 if not line:
2558 break
2559 print line.strip("\n")
2560 output_array.append(line)
2561 output = "".join(output_array)
2562 else:
2563 output = p.stdout.read()
2564 p.wait()
2565 errout = p.stderr.read()
2566 if print_output and errout:
2567 print >>sys.stderr, errout
2568 p.stdout.close()
2569 p.stderr.close()
2570 return output, p.returncode
Russ Cox79a63722009-10-22 11:12:39 -07002571
2572
2573def RunShell(command, silent_ok=False, universal_newlines=True,
Russ Cox3b226f92010-08-26 18:24:14 -04002574 print_output=False, env=os.environ):
2575 data, retcode = RunShellWithReturnCode(command, print_output, universal_newlines, env)
2576 if retcode:
2577 ErrorExit("Got error status from %s:\n%s" % (command, data))
2578 if not silent_ok and not data:
2579 ErrorExit("No output from %s" % command)
2580 return data
Russ Cox79a63722009-10-22 11:12:39 -07002581
2582
2583class VersionControlSystem(object):
Russ Cox3b226f92010-08-26 18:24:14 -04002584 """Abstract base class providing an interface to the VCS."""
Russ Cox79a63722009-10-22 11:12:39 -07002585
Russ Cox3b226f92010-08-26 18:24:14 -04002586 def __init__(self, options):
2587 """Constructor.
Russ Cox79a63722009-10-22 11:12:39 -07002588
Russ Cox3b226f92010-08-26 18:24:14 -04002589 Args:
2590 options: Command line options.
2591 """
2592 self.options = options
Russ Cox79a63722009-10-22 11:12:39 -07002593
Russ Cox3b226f92010-08-26 18:24:14 -04002594 def GenerateDiff(self, args):
2595 """Return the current diff as a string.
Russ Cox79a63722009-10-22 11:12:39 -07002596
Russ Cox3b226f92010-08-26 18:24:14 -04002597 Args:
2598 args: Extra arguments to pass to the diff command.
2599 """
2600 raise NotImplementedError(
2601 "abstract method -- subclass %s must override" % self.__class__)
Russ Cox79a63722009-10-22 11:12:39 -07002602
Russ Cox3b226f92010-08-26 18:24:14 -04002603 def GetUnknownFiles(self):
2604 """Return a list of files unknown to the VCS."""
2605 raise NotImplementedError(
2606 "abstract method -- subclass %s must override" % self.__class__)
Russ Cox79a63722009-10-22 11:12:39 -07002607
Russ Cox3b226f92010-08-26 18:24:14 -04002608 def CheckForUnknownFiles(self):
2609 """Show an "are you sure?" prompt if there are unknown files."""
2610 unknown_files = self.GetUnknownFiles()
2611 if unknown_files:
2612 print "The following files are not added to version control:"
2613 for line in unknown_files:
2614 print line
2615 prompt = "Are you sure to continue?(y/N) "
2616 answer = raw_input(prompt).strip()
2617 if answer != "y":
2618 ErrorExit("User aborted")
Russ Cox79a63722009-10-22 11:12:39 -07002619
Russ Cox3b226f92010-08-26 18:24:14 -04002620 def GetBaseFile(self, filename):
2621 """Get the content of the upstream version of a file.
Russ Cox79a63722009-10-22 11:12:39 -07002622
Russ Cox3b226f92010-08-26 18:24:14 -04002623 Returns:
2624 A tuple (base_content, new_content, is_binary, status)
2625 base_content: The contents of the base file.
2626 new_content: For text files, this is empty. For binary files, this is
2627 the contents of the new file, since the diff output won't contain
2628 information to reconstruct the current file.
2629 is_binary: True iff the file is binary.
2630 status: The status of the file.
2631 """
Russ Cox79a63722009-10-22 11:12:39 -07002632
Russ Cox3b226f92010-08-26 18:24:14 -04002633 raise NotImplementedError(
2634 "abstract method -- subclass %s must override" % self.__class__)
Russ Cox79a63722009-10-22 11:12:39 -07002635
2636
Russ Cox3b226f92010-08-26 18:24:14 -04002637 def GetBaseFiles(self, diff):
2638 """Helper that calls GetBase file for each file in the patch.
Russ Cox79a63722009-10-22 11:12:39 -07002639
Russ Cox3b226f92010-08-26 18:24:14 -04002640 Returns:
2641 A dictionary that maps from filename to GetBaseFile's tuple. Filenames
2642 are retrieved based on lines that start with "Index:" or
2643 "Property changes on:".
2644 """
2645 files = {}
2646 for line in diff.splitlines(True):
2647 if line.startswith('Index:') or line.startswith('Property changes on:'):
2648 unused, filename = line.split(':', 1)
2649 # On Windows if a file has property changes its filename uses '\'
2650 # instead of '/'.
2651 filename = filename.strip().replace('\\', '/')
2652 files[filename] = self.GetBaseFile(filename)
2653 return files
Russ Cox79a63722009-10-22 11:12:39 -07002654
2655
Russ Cox3b226f92010-08-26 18:24:14 -04002656 def UploadBaseFiles(self, issue, rpc_server, patch_list, patchset, options,
2657 files):
2658 """Uploads the base files (and if necessary, the current ones as well)."""
Russ Cox79a63722009-10-22 11:12:39 -07002659
Russ Cox3b226f92010-08-26 18:24:14 -04002660 def UploadFile(filename, file_id, content, is_binary, status, is_base):
2661 """Uploads a file to the server."""
2662 set_status("uploading " + filename)
2663 file_too_large = False
2664 if is_base:
2665 type = "base"
2666 else:
2667 type = "current"
2668 if len(content) > MAX_UPLOAD_SIZE:
2669 print ("Not uploading the %s file for %s because it's too large." %
2670 (type, filename))
2671 file_too_large = True
2672 content = ""
2673 checksum = md5(content).hexdigest()
2674 if options.verbose > 0 and not file_too_large:
2675 print "Uploading %s file for %s" % (type, filename)
2676 url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
2677 form_fields = [
2678 ("filename", filename),
2679 ("status", status),
2680 ("checksum", checksum),
2681 ("is_binary", str(is_binary)),
2682 ("is_current", str(not is_base)),
2683 ]
2684 if file_too_large:
2685 form_fields.append(("file_too_large", "1"))
2686 if options.email:
2687 form_fields.append(("user", options.email))
2688 ctype, body = EncodeMultipartFormData(form_fields, [("data", filename, content)])
2689 response_body = rpc_server.Send(url, body, content_type=ctype)
2690 if not response_body.startswith("OK"):
2691 StatusUpdate(" --> %s" % response_body)
2692 sys.exit(1)
Russ Cox79a63722009-10-22 11:12:39 -07002693
Russ Coxf7d87f32010-08-26 18:56:29 -04002694 # Don't want to spawn too many threads, nor do we want to
2695 # hit Rietveld too hard, or it will start serving 500 errors.
2696 # When 8 works, it's no better than 4, and sometimes 8 is
2697 # too many for Rietveld to handle.
2698 MAX_PARALLEL_UPLOADS = 4
2699
2700 sema = threading.BoundedSemaphore(MAX_PARALLEL_UPLOADS)
2701 upload_threads = []
2702 finished_upload_threads = []
2703
2704 class UploadFileThread(threading.Thread):
2705 def __init__(self, args):
2706 threading.Thread.__init__(self)
2707 self.args = args
2708 def run(self):
2709 UploadFile(*self.args)
2710 finished_upload_threads.append(self)
2711 sema.release()
2712
2713 def StartUploadFile(*args):
2714 sema.acquire()
2715 while len(finished_upload_threads) > 0:
2716 t = finished_upload_threads.pop()
2717 upload_threads.remove(t)
2718 t.join()
2719 t = UploadFileThread(args)
2720 upload_threads.append(t)
2721 t.start()
2722
2723 def WaitForUploads():
2724 for t in upload_threads:
2725 t.join()
2726
Russ Cox3b226f92010-08-26 18:24:14 -04002727 patches = dict()
2728 [patches.setdefault(v, k) for k, v in patch_list]
2729 for filename in patches.keys():
2730 base_content, new_content, is_binary, status = files[filename]
2731 file_id_str = patches.get(filename)
2732 if file_id_str.find("nobase") != -1:
2733 base_content = None
2734 file_id_str = file_id_str[file_id_str.rfind("_") + 1:]
2735 file_id = int(file_id_str)
2736 if base_content != None:
Russ Coxf7d87f32010-08-26 18:56:29 -04002737 StartUploadFile(filename, file_id, base_content, is_binary, status, True)
Russ Cox3b226f92010-08-26 18:24:14 -04002738 if new_content != None:
Russ Coxf7d87f32010-08-26 18:56:29 -04002739 StartUploadFile(filename, file_id, new_content, is_binary, status, False)
2740 WaitForUploads()
Russ Cox79a63722009-10-22 11:12:39 -07002741
Russ Cox3b226f92010-08-26 18:24:14 -04002742 def IsImage(self, filename):
2743 """Returns true if the filename has an image extension."""
2744 mimetype = mimetypes.guess_type(filename)[0]
2745 if not mimetype:
2746 return False
2747 return mimetype.startswith("image/")
Russ Cox79a63722009-10-22 11:12:39 -07002748
Russ Cox3b226f92010-08-26 18:24:14 -04002749 def IsBinary(self, filename):
2750 """Returns true if the guessed mimetyped isnt't in text group."""
2751 mimetype = mimetypes.guess_type(filename)[0]
2752 if not mimetype:
2753 return False # e.g. README, "real" binaries usually have an extension
2754 # special case for text files which don't start with text/
2755 if mimetype in TEXT_MIMETYPES:
2756 return False
2757 return not mimetype.startswith("text/")
Russ Cox79a63722009-10-22 11:12:39 -07002758
Russ Coxf7d87f32010-08-26 18:56:29 -04002759class FakeMercurialUI(object):
2760 def __init__(self):
2761 self.quiet = True
2762 self.output = ''
2763
Evan Shaw9e162aa2010-08-29 23:04:05 -04002764 def write(self, *args, **opts):
2765 self.output += ' '.join(args)
Russ Coxf7d87f32010-08-26 18:56:29 -04002766
2767use_hg_shell = False # set to True to shell out to hg always; slower
Russ Cox79a63722009-10-22 11:12:39 -07002768
2769class MercurialVCS(VersionControlSystem):
Russ Cox3b226f92010-08-26 18:24:14 -04002770 """Implementation of the VersionControlSystem interface for Mercurial."""
Russ Cox79a63722009-10-22 11:12:39 -07002771
Russ Coxf7d87f32010-08-26 18:56:29 -04002772 def __init__(self, options, ui, repo):
Russ Cox3b226f92010-08-26 18:24:14 -04002773 super(MercurialVCS, self).__init__(options)
Russ Coxf7d87f32010-08-26 18:56:29 -04002774 self.ui = ui
2775 self.repo = repo
Russ Cox3b226f92010-08-26 18:24:14 -04002776 # Absolute path to repository (we can be in a subdir)
Russ Coxf7d87f32010-08-26 18:56:29 -04002777 self.repo_dir = os.path.normpath(repo.root)
Russ Cox3b226f92010-08-26 18:24:14 -04002778 # Compute the subdir
2779 cwd = os.path.normpath(os.getcwd())
2780 assert cwd.startswith(self.repo_dir)
2781 self.subdir = cwd[len(self.repo_dir):].lstrip(r"\/")
2782 if self.options.revision:
2783 self.base_rev = self.options.revision
2784 else:
2785 mqparent, err = RunShellWithReturnCode(['hg', 'log', '--rev', 'qparent', '--template={node}'])
Hector Chu1e0efcd2011-01-19 19:02:47 -05002786 if not err and mqparent != "":
Russ Cox3b226f92010-08-26 18:24:14 -04002787 self.base_rev = mqparent
2788 else:
2789 self.base_rev = RunShell(["hg", "parents", "-q"]).split(':')[1].strip()
2790 def _GetRelPath(self, filename):
2791 """Get relative path of a file according to the current directory,
2792 given its logical path in the repo."""
2793 assert filename.startswith(self.subdir), (filename, self.subdir)
2794 return filename[len(self.subdir):].lstrip(r"\/")
Russ Cox79a63722009-10-22 11:12:39 -07002795
Russ Cox3b226f92010-08-26 18:24:14 -04002796 def GenerateDiff(self, extra_args):
2797 # If no file specified, restrict to the current subdir
2798 extra_args = extra_args or ["."]
2799 cmd = ["hg", "diff", "--git", "-r", self.base_rev] + extra_args
2800 data = RunShell(cmd, silent_ok=True)
2801 svndiff = []
2802 filecount = 0
2803 for line in data.splitlines():
2804 m = re.match("diff --git a/(\S+) b/(\S+)", line)
2805 if m:
2806 # Modify line to make it look like as it comes from svn diff.
2807 # With this modification no changes on the server side are required
2808 # to make upload.py work with Mercurial repos.
2809 # NOTE: for proper handling of moved/copied files, we have to use
2810 # the second filename.
2811 filename = m.group(2)
2812 svndiff.append("Index: %s" % filename)
2813 svndiff.append("=" * 67)
2814 filecount += 1
2815 logging.info(line)
2816 else:
2817 svndiff.append(line)
2818 if not filecount:
2819 ErrorExit("No valid patches found in output from hg diff")
2820 return "\n".join(svndiff) + "\n"
Russ Cox79a63722009-10-22 11:12:39 -07002821
Russ Cox3b226f92010-08-26 18:24:14 -04002822 def GetUnknownFiles(self):
2823 """Return a list of files unknown to the VCS."""
2824 args = []
2825 status = RunShell(["hg", "status", "--rev", self.base_rev, "-u", "."],
2826 silent_ok=True)
2827 unknown_files = []
2828 for line in status.splitlines():
2829 st, fn = line.split(" ", 1)
2830 if st == "?":
2831 unknown_files.append(fn)
2832 return unknown_files
Russ Cox79a63722009-10-22 11:12:39 -07002833
Russ Cox3b226f92010-08-26 18:24:14 -04002834 def GetBaseFile(self, filename):
2835 set_status("inspecting " + filename)
2836 # "hg status" and "hg cat" both take a path relative to the current subdir
2837 # rather than to the repo root, but "hg diff" has given us the full path
2838 # to the repo root.
2839 base_content = ""
2840 new_content = None
2841 is_binary = False
2842 oldrelpath = relpath = self._GetRelPath(filename)
2843 # "hg status -C" returns two lines for moved/copied files, one otherwise
Russ Coxf7d87f32010-08-26 18:56:29 -04002844 if use_hg_shell:
2845 out = RunShell(["hg", "status", "-C", "--rev", self.base_rev, relpath])
2846 else:
2847 fui = FakeMercurialUI()
2848 ret = commands.status(fui, self.repo, *[relpath], **{'rev': [self.base_rev], 'copies': True})
2849 if ret:
2850 raise util.Abort(ret)
2851 out = fui.output
Russ Cox3b226f92010-08-26 18:24:14 -04002852 out = out.splitlines()
2853 # HACK: strip error message about missing file/directory if it isn't in
2854 # the working copy
2855 if out[0].startswith('%s: ' % relpath):
2856 out = out[1:]
2857 status, what = out[0].split(' ', 1)
2858 if len(out) > 1 and status == "A" and what == relpath:
2859 oldrelpath = out[1].strip()
2860 status = "M"
2861 if ":" in self.base_rev:
2862 base_rev = self.base_rev.split(":", 1)[0]
2863 else:
2864 base_rev = self.base_rev
2865 if status != "A":
Russ Coxf7d87f32010-08-26 18:56:29 -04002866 if use_hg_shell:
2867 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath], silent_ok=True)
2868 else:
Russ Cox94998562010-09-28 20:29:20 -04002869 base_content = str(self.repo[base_rev][oldrelpath].data())
Russ Cox3b226f92010-08-26 18:24:14 -04002870 is_binary = "\0" in base_content # Mercurial's heuristic
2871 if status != "R":
2872 new_content = open(relpath, "rb").read()
2873 is_binary = is_binary or "\0" in new_content
Russ Coxf7d87f32010-08-26 18:56:29 -04002874 if is_binary and base_content and use_hg_shell:
Russ Cox3b226f92010-08-26 18:24:14 -04002875 # Fetch again without converting newlines
2876 base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
2877 silent_ok=True, universal_newlines=False)
2878 if not is_binary or not self.IsImage(relpath):
2879 new_content = None
2880 return base_content, new_content, is_binary, status
Russ Cox79a63722009-10-22 11:12:39 -07002881
2882
2883# NOTE: The SplitPatch function is duplicated in engine.py, keep them in sync.
2884def SplitPatch(data):
Russ Cox3b226f92010-08-26 18:24:14 -04002885 """Splits a patch into separate pieces for each file.
Russ Cox79a63722009-10-22 11:12:39 -07002886
Russ Cox3b226f92010-08-26 18:24:14 -04002887 Args:
2888 data: A string containing the output of svn diff.
Russ Cox79a63722009-10-22 11:12:39 -07002889
Russ Cox3b226f92010-08-26 18:24:14 -04002890 Returns:
2891 A list of 2-tuple (filename, text) where text is the svn diff output
2892 pertaining to filename.
2893 """
2894 patches = []
2895 filename = None
2896 diff = []
2897 for line in data.splitlines(True):
2898 new_filename = None
2899 if line.startswith('Index:'):
2900 unused, new_filename = line.split(':', 1)
2901 new_filename = new_filename.strip()
2902 elif line.startswith('Property changes on:'):
2903 unused, temp_filename = line.split(':', 1)
2904 # When a file is modified, paths use '/' between directories, however
2905 # when a property is modified '\' is used on Windows. Make them the same
2906 # otherwise the file shows up twice.
2907 temp_filename = temp_filename.strip().replace('\\', '/')
2908 if temp_filename != filename:
2909 # File has property changes but no modifications, create a new diff.
2910 new_filename = temp_filename
2911 if new_filename:
2912 if filename and diff:
2913 patches.append((filename, ''.join(diff)))
2914 filename = new_filename
2915 diff = [line]
2916 continue
2917 if diff is not None:
2918 diff.append(line)
2919 if filename and diff:
2920 patches.append((filename, ''.join(diff)))
2921 return patches
Russ Cox79a63722009-10-22 11:12:39 -07002922
2923
2924def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
Russ Cox3b226f92010-08-26 18:24:14 -04002925 """Uploads a separate patch for each file in the diff output.
Russ Cox79a63722009-10-22 11:12:39 -07002926
Russ Cox3b226f92010-08-26 18:24:14 -04002927 Returns a list of [patch_key, filename] for each file.
2928 """
2929 patches = SplitPatch(data)
2930 rv = []
2931 for patch in patches:
2932 set_status("uploading patch for " + patch[0])
2933 if len(patch[1]) > MAX_UPLOAD_SIZE:
2934 print ("Not uploading the patch for " + patch[0] +
2935 " because the file is too large.")
2936 continue
2937 form_fields = [("filename", patch[0])]
2938 if not options.download_base:
2939 form_fields.append(("content_upload", "1"))
2940 files = [("data", "data.diff", patch[1])]
2941 ctype, body = EncodeMultipartFormData(form_fields, files)
2942 url = "/%d/upload_patch/%d" % (int(issue), int(patchset))
2943 print "Uploading patch for " + patch[0]
2944 response_body = rpc_server.Send(url, body, content_type=ctype)
2945 lines = response_body.splitlines()
2946 if not lines or lines[0] != "OK":
2947 StatusUpdate(" --> %s" % response_body)
2948 sys.exit(1)
2949 rv.append([lines[1], patch[0]])
2950 return rv