blob: 5a426113ab2770af63cc53d54a40afd072b7f909 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001#
2# Copyright (C) 2008 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
Ben Komalo08a3f682010-07-15 16:03:02 -070016import copy
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070017import re
18import sys
19
20from command import InteractiveCommand
21from editor import Editor
22from error import UploadError
23
Dan Morrillf0a9a1a2010-05-05 08:18:35 -070024UNUSUAL_COMMIT_THRESHOLD = 5
Dan Morrill879a9a52010-05-04 16:56:07 -070025
26def _ConfirmManyUploads(multiple_branches=False):
27 if multiple_branches:
28 print "ATTENTION: One or more branches has an unusually high number of commits."
29 else:
30 print "ATTENTION: You are uploading an unusually high number of commits."
31 print "YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across branches?)"
32 answer = raw_input("If you are sure you intend to do this, type 'yes': ").strip()
33 return answer == "yes"
34
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070035def _die(fmt, *args):
36 msg = fmt % args
37 print >>sys.stderr, 'error: %s' % msg
38 sys.exit(1)
39
Joe Onorato2896a792008-11-17 16:56:36 -050040def _SplitEmails(values):
41 result = []
42 for str in values:
43 result.extend([s.strip() for s in str.split(',')])
44 return result
45
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070046class Upload(InteractiveCommand):
47 common = True
48 helpSummary = "Upload changes for code review"
49 helpUsage="""
Joe Onorato2896a792008-11-17 16:56:36 -050050%prog [--re --cc] {[<project>]... | --replace <project>}
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070051"""
52 helpDescription = """
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070053The '%prog' command is used to send changes to the Gerrit Code
54Review system. It searches for topic branches in local projects
55that have not yet been published for review. If multiple topic
56branches are found, '%prog' opens an editor to allow the user to
57select which branches to upload.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070058
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070059'%prog' searches for uploadable changes in all projects listed at
60the command line. Projects can be specified either by name, or by
61a relative or absolute path to the project's local directory. If no
62projects are specified, '%prog' will search for uploadable changes
63in all projects listed in the manifest.
Joe Onorato2896a792008-11-17 16:56:36 -050064
65If the --reviewers or --cc options are passed, those emails are
66added to the respective list of users, and emails are sent to any
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070067new users. Users passed as --reviewers must already be registered
Joe Onorato2896a792008-11-17 16:56:36 -050068with the code review system, or the upload will fail.
Shawn O. Pearcea6df7d22008-12-12 08:04:07 -080069
70If the --replace option is passed the user can designate which
71existing change(s) in Gerrit match up to the commits in the branch
72being uploaded. For each matched pair of change,commit the commit
73will be added as a new patch set, completely replacing the set of
74files and description associated with the change in Gerrit.
Shawn O. Pearcea608fb02009-04-17 12:11:24 -070075
76Configuration
77-------------
78
79review.URL.autoupload:
80
81To disable the "Upload ... (y/n)?" prompt, you can set a per-project
82or global Git configuration option. If review.URL.autoupload is set
83to "true" then repo will assume you always answer "y" at the prompt,
84and will not prompt you further. If it is set to "false" then repo
85will assume you always answer "n", and will abort.
86
Ben Komalo08a3f682010-07-15 16:03:02 -070087review.URL.autocopy:
88
89To automatically copy a user or mailing list to all uploaded reviews,
90you can set a per-project or global Git option to do so. Specifically,
91review.URL.autocopy can be set to a comma separated list of reviewers
92who you always want copied on all uploads with a non-empty --re
93argument.
94
Shawn O. Pearcea608fb02009-04-17 12:11:24 -070095The URL must match the review URL listed in the manifest XML file,
96or in the .git/config within the project. For example:
97
98 [remote "origin"]
99 url = git://git.example.com/project.git
100 review = http://review.example.com/
101
102 [review "http://review.example.com/"]
103 autoupload = true
Ben Komalo08a3f682010-07-15 16:03:02 -0700104 autocopy = johndoe@company.com,my-team-alias@company.com
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700105
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -0700106References
107----------
108
109Gerrit Code Review: http://code.google.com/p/gerrit/
110
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700111"""
112
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800113 def _Options(self, p):
114 p.add_option('--replace',
115 dest='replace', action='store_true',
116 help='Upload replacement patchesets from this branch')
Joe Onorato2896a792008-11-17 16:56:36 -0500117 p.add_option('--re', '--reviewers',
118 type='string', action='append', dest='reviewers',
119 help='Request reviews from these people.')
120 p.add_option('--cc',
121 type='string', action='append', dest='cc',
122 help='Also send email to these email addresses.')
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800123
Joe Onorato2896a792008-11-17 16:56:36 -0500124 def _SingleBranch(self, branch, people):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700125 project = branch.project
126 name = branch.name
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700127 remote = project.GetBranch(name).remote
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700128
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700129 key = 'review.%s.autoupload' % remote.review
130 answer = project.config.GetBoolean(key)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700131
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700132 if answer is False:
133 _die("upload blocked by %s = false" % key)
134
135 if answer is None:
Shawn O. Pearce66bdd462009-04-17 18:47:22 -0700136 date = branch.date
137 list = branch.commits
138
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700139 print 'Upload project %s/:' % project.relpath
140 print ' branch %s (%2d commit%s, %s):' % (
141 name,
142 len(list),
143 len(list) != 1 and 's' or '',
144 date)
145 for commit in list:
146 print ' %s' % commit
147
Shawn O. Pearce8225cdc2009-04-18 11:00:35 -0700148 sys.stdout.write('to %s (y/n)? ' % remote.review)
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700149 answer = sys.stdin.readline().strip()
150 answer = answer in ('y', 'Y', 'yes', '1', 'true', 't')
151
152 if answer:
Dan Morrill879a9a52010-05-04 16:56:07 -0700153 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
154 answer = _ConfirmManyUploads()
155
156 if answer:
Joe Onorato2896a792008-11-17 16:56:36 -0500157 self._UploadAndReport([branch], people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700158 else:
159 _die("upload aborted by user")
160
Joe Onorato2896a792008-11-17 16:56:36 -0500161 def _MultipleBranches(self, pending, people):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700162 projects = {}
163 branches = {}
164
165 script = []
166 script.append('# Uncomment the branches to upload:')
167 for project, avail in pending:
168 script.append('#')
169 script.append('# project %s/:' % project.relpath)
170
171 b = {}
172 for branch in avail:
173 name = branch.name
174 date = branch.date
175 list = branch.commits
176
177 if b:
178 script.append('#')
179 script.append('# branch %s (%2d commit%s, %s):' % (
180 name,
181 len(list),
182 len(list) != 1 and 's' or '',
183 date))
184 for commit in list:
185 script.append('# %s' % commit)
186 b[name] = branch
187
188 projects[project.relpath] = project
189 branches[project.name] = b
190 script.append('')
191
192 script = Editor.EditString("\n".join(script)).split("\n")
193
194 project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$')
195 branch_re = re.compile(r'^\s*branch\s*([^\s(]+)\s*\(.*')
196
197 project = None
198 todo = []
199
200 for line in script:
201 m = project_re.match(line)
202 if m:
203 name = m.group(1)
204 project = projects.get(name)
205 if not project:
206 _die('project %s not available for upload', name)
207 continue
208
209 m = branch_re.match(line)
210 if m:
211 name = m.group(1)
212 if not project:
213 _die('project for branch %s not in script', name)
214 branch = branches[project.name].get(name)
215 if not branch:
216 _die('branch %s not in %s', name, project.relpath)
217 todo.append(branch)
218 if not todo:
219 _die("nothing uncommented for upload")
Dan Morrill879a9a52010-05-04 16:56:07 -0700220
221 many_commits = False
222 for branch in todo:
223 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
224 many_commits = True
225 break
226 if many_commits:
227 if not _ConfirmManyUploads(multiple_branches=True):
228 _die("upload aborted by user")
229
Joe Onorato2896a792008-11-17 16:56:36 -0500230 self._UploadAndReport(todo, people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700231
Ben Komalo08a3f682010-07-15 16:03:02 -0700232 def _AppendAutoCcList(self, branch, people):
233 """
234 Appends the list of users in the CC list in the git project's config if a
235 non-empty reviewer list was found.
236 """
237
238 name = branch.name
239 project = branch.project
240 key = 'review.%s.autocopy' % project.GetBranch(name).remote.review
241 raw_list = project.config.GetString(key)
242 if not raw_list is None and len(people[0]) > 0:
243 people[1].extend([entry.strip() for entry in raw_list.split(',')])
244
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700245 def _FindGerritChange(self, branch):
246 last_pub = branch.project.WasPublished(branch.name)
247 if last_pub is None:
248 return ""
249
250 refs = branch.GetPublishedRefs()
251 try:
252 # refs/changes/XYZ/N --> XYZ
253 return refs.get(last_pub).split('/')[-2]
254 except:
255 return ""
256
Shawn O. Pearcee92ceeb2008-11-24 15:51:25 -0800257 def _ReplaceBranch(self, project, people):
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800258 branch = project.CurrentBranch
259 if not branch:
260 print >>sys.stdout, "no branches ready for upload"
261 return
262 branch = project.GetUploadableBranch(branch)
263 if not branch:
264 print >>sys.stdout, "no branches ready for upload"
265 return
266
267 script = []
268 script.append('# Replacing from branch %s' % branch.name)
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700269
270 if len(branch.commits) == 1:
271 change = self._FindGerritChange(branch)
272 script.append('[%-6s] %s' % (change, branch.commits[0]))
273 else:
274 for commit in branch.commits:
275 script.append('[ ] %s' % commit)
276
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800277 script.append('')
278 script.append('# Insert change numbers in the brackets to add a new patch set.')
279 script.append('# To create a new change record, leave the brackets empty.')
280
281 script = Editor.EditString("\n".join(script)).split("\n")
282
283 change_re = re.compile(r'^\[\s*(\d{1,})\s*\]\s*([0-9a-f]{1,}) .*$')
284 to_replace = dict()
285 full_hashes = branch.unabbrev_commits
286
287 for line in script:
288 m = change_re.match(line)
289 if m:
Shawn O. Pearce67092442008-12-12 08:01:12 -0800290 c = m.group(1)
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800291 f = m.group(2)
292 try:
293 f = full_hashes[f]
294 except KeyError:
295 print 'fh = %s' % full_hashes
296 print >>sys.stderr, "error: commit %s not found" % f
297 sys.exit(1)
Shawn O. Pearce67092442008-12-12 08:01:12 -0800298 if c in to_replace:
299 print >>sys.stderr,\
300 "error: change %s cannot accept multiple commits" % c
301 sys.exit(1)
302 to_replace[c] = f
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800303
304 if not to_replace:
305 print >>sys.stderr, "error: no replacements specified"
306 print >>sys.stderr, " use 'repo upload' without --replace"
307 sys.exit(1)
308
Dan Morrill879a9a52010-05-04 16:56:07 -0700309 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
310 if not _ConfirmManyUploads(multiple_branches=True):
311 _die("upload aborted by user")
312
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800313 branch.replace_changes = to_replace
Joe Onorato2896a792008-11-17 16:56:36 -0500314 self._UploadAndReport([branch], people)
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800315
Ben Komalo08a3f682010-07-15 16:03:02 -0700316 def _UploadAndReport(self, todo, original_people):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700317 have_errors = False
318 for branch in todo:
319 try:
Ben Komalo08a3f682010-07-15 16:03:02 -0700320 people = copy.deepcopy(original_people)
321 self._AppendAutoCcList(branch, people)
322
Anthony Newnamcc50bac2010-04-08 10:28:59 -0500323 # Check if there are local changes that may have been forgotten
324 if branch.project.HasChanges():
325 key = 'review.%s.autoupload' % branch.project.remote.review
326 answer = branch.project.config.GetBoolean(key)
327
328 # if they want to auto upload, let's not ask because it could be automated
329 if answer is None:
330 sys.stdout.write('Uncommitted changes in ' + branch.project.name + ' (did you forget to amend?). Continue uploading? (y/n) ')
331 a = sys.stdin.readline().strip().lower()
332 if a not in ('y', 'yes', 't', 'true', 'on'):
333 print >>sys.stderr, "skipping upload"
334 branch.uploaded = False
335 branch.error = 'User aborted'
336 continue
337
Joe Onorato2896a792008-11-17 16:56:36 -0500338 branch.UploadForReview(people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700339 branch.uploaded = True
340 except UploadError, e:
341 branch.error = e
342 branch.uploaded = False
343 have_errors = True
344
345 print >>sys.stderr, ''
346 print >>sys.stderr, '--------------------------------------------'
347
348 if have_errors:
349 for branch in todo:
350 if not branch.uploaded:
351 print >>sys.stderr, '[FAILED] %-15s %-15s (%s)' % (
352 branch.project.relpath + '/', \
353 branch.name, \
354 branch.error)
355 print >>sys.stderr, ''
356
357 for branch in todo:
358 if branch.uploaded:
359 print >>sys.stderr, '[OK ] %-15s %s' % (
360 branch.project.relpath + '/',
361 branch.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700362
363 if have_errors:
364 sys.exit(1)
365
366 def Execute(self, opt, args):
367 project_list = self.GetProjects(args)
368 pending = []
Joe Onorato2896a792008-11-17 16:56:36 -0500369 reviewers = []
370 cc = []
371
372 if opt.reviewers:
373 reviewers = _SplitEmails(opt.reviewers)
374 if opt.cc:
375 cc = _SplitEmails(opt.cc)
376 people = (reviewers,cc)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700377
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800378 if opt.replace:
379 if len(project_list) != 1:
380 print >>sys.stderr, \
381 'error: --replace requires exactly one project'
382 sys.exit(1)
Shawn O. Pearcee92ceeb2008-11-24 15:51:25 -0800383 self._ReplaceBranch(project_list[0], people)
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800384 return
385
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700386 for project in project_list:
387 avail = project.GetUploadableBranches()
388 if avail:
389 pending.append((project, avail))
390
391 if not pending:
392 print >>sys.stdout, "no branches ready for upload"
393 elif len(pending) == 1 and len(pending[0][1]) == 1:
Joe Onorato2896a792008-11-17 16:56:36 -0500394 self._SingleBranch(pending[0][1][0], people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700395 else:
Joe Onorato2896a792008-11-17 16:56:36 -0500396 self._MultipleBranches(pending, people)