blob: 4fa5b432c282dfe371743eb17614118d6565f0e1 [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
16import re
17import sys
18
19from command import InteractiveCommand
20from editor import Editor
21from error import UploadError
22
Dan Morrillf0a9a1a2010-05-05 08:18:35 -070023UNUSUAL_COMMIT_THRESHOLD = 5
Dan Morrill879a9a52010-05-04 16:56:07 -070024
25def _ConfirmManyUploads(multiple_branches=False):
26 if multiple_branches:
27 print "ATTENTION: One or more branches has an unusually high number of commits."
28 else:
29 print "ATTENTION: You are uploading an unusually high number of commits."
30 print "YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across branches?)"
31 answer = raw_input("If you are sure you intend to do this, type 'yes': ").strip()
32 return answer == "yes"
33
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070034def _die(fmt, *args):
35 msg = fmt % args
36 print >>sys.stderr, 'error: %s' % msg
37 sys.exit(1)
38
Joe Onorato2896a792008-11-17 16:56:36 -050039def _SplitEmails(values):
40 result = []
41 for str in values:
42 result.extend([s.strip() for s in str.split(',')])
43 return result
44
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070045class Upload(InteractiveCommand):
46 common = True
47 helpSummary = "Upload changes for code review"
48 helpUsage="""
Joe Onorato2896a792008-11-17 16:56:36 -050049%prog [--re --cc] {[<project>]... | --replace <project>}
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070050"""
51 helpDescription = """
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070052The '%prog' command is used to send changes to the Gerrit Code
53Review system. It searches for topic branches in local projects
54that have not yet been published for review. If multiple topic
55branches are found, '%prog' opens an editor to allow the user to
56select which branches to upload.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070057
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070058'%prog' searches for uploadable changes in all projects listed at
59the command line. Projects can be specified either by name, or by
60a relative or absolute path to the project's local directory. If no
61projects are specified, '%prog' will search for uploadable changes
62in all projects listed in the manifest.
Joe Onorato2896a792008-11-17 16:56:36 -050063
64If the --reviewers or --cc options are passed, those emails are
65added to the respective list of users, and emails are sent to any
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070066new users. Users passed as --reviewers must already be registered
Joe Onorato2896a792008-11-17 16:56:36 -050067with the code review system, or the upload will fail.
Shawn O. Pearcea6df7d22008-12-12 08:04:07 -080068
69If the --replace option is passed the user can designate which
70existing change(s) in Gerrit match up to the commits in the branch
71being uploaded. For each matched pair of change,commit the commit
72will be added as a new patch set, completely replacing the set of
73files and description associated with the change in Gerrit.
Shawn O. Pearcea608fb02009-04-17 12:11:24 -070074
75Configuration
76-------------
77
78review.URL.autoupload:
79
80To disable the "Upload ... (y/n)?" prompt, you can set a per-project
81or global Git configuration option. If review.URL.autoupload is set
82to "true" then repo will assume you always answer "y" at the prompt,
83and will not prompt you further. If it is set to "false" then repo
84will assume you always answer "n", and will abort.
85
86The URL must match the review URL listed in the manifest XML file,
87or in the .git/config within the project. For example:
88
89 [remote "origin"]
90 url = git://git.example.com/project.git
91 review = http://review.example.com/
92
93 [review "http://review.example.com/"]
94 autoupload = true
95
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070096References
97----------
98
99Gerrit Code Review: http://code.google.com/p/gerrit/
100
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700101"""
102
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800103 def _Options(self, p):
104 p.add_option('--replace',
105 dest='replace', action='store_true',
106 help='Upload replacement patchesets from this branch')
Joe Onorato2896a792008-11-17 16:56:36 -0500107 p.add_option('--re', '--reviewers',
108 type='string', action='append', dest='reviewers',
109 help='Request reviews from these people.')
110 p.add_option('--cc',
111 type='string', action='append', dest='cc',
112 help='Also send email to these email addresses.')
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800113
Joe Onorato2896a792008-11-17 16:56:36 -0500114 def _SingleBranch(self, branch, people):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700115 project = branch.project
116 name = branch.name
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700117 remote = project.GetBranch(name).remote
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700118
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700119 key = 'review.%s.autoupload' % remote.review
120 answer = project.config.GetBoolean(key)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700121
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700122 if answer is False:
123 _die("upload blocked by %s = false" % key)
124
125 if answer is None:
Shawn O. Pearce66bdd462009-04-17 18:47:22 -0700126 date = branch.date
127 list = branch.commits
128
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700129 print 'Upload project %s/:' % project.relpath
130 print ' branch %s (%2d commit%s, %s):' % (
131 name,
132 len(list),
133 len(list) != 1 and 's' or '',
134 date)
135 for commit in list:
136 print ' %s' % commit
137
Shawn O. Pearce8225cdc2009-04-18 11:00:35 -0700138 sys.stdout.write('to %s (y/n)? ' % remote.review)
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700139 answer = sys.stdin.readline().strip()
140 answer = answer in ('y', 'Y', 'yes', '1', 'true', 't')
141
142 if answer:
Dan Morrill879a9a52010-05-04 16:56:07 -0700143 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
144 answer = _ConfirmManyUploads()
145
146 if answer:
Joe Onorato2896a792008-11-17 16:56:36 -0500147 self._UploadAndReport([branch], people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700148 else:
149 _die("upload aborted by user")
150
Joe Onorato2896a792008-11-17 16:56:36 -0500151 def _MultipleBranches(self, pending, people):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700152 projects = {}
153 branches = {}
154
155 script = []
156 script.append('# Uncomment the branches to upload:')
157 for project, avail in pending:
158 script.append('#')
159 script.append('# project %s/:' % project.relpath)
160
161 b = {}
162 for branch in avail:
163 name = branch.name
164 date = branch.date
165 list = branch.commits
166
167 if b:
168 script.append('#')
169 script.append('# branch %s (%2d commit%s, %s):' % (
170 name,
171 len(list),
172 len(list) != 1 and 's' or '',
173 date))
174 for commit in list:
175 script.append('# %s' % commit)
176 b[name] = branch
177
178 projects[project.relpath] = project
179 branches[project.name] = b
180 script.append('')
181
182 script = Editor.EditString("\n".join(script)).split("\n")
183
184 project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$')
185 branch_re = re.compile(r'^\s*branch\s*([^\s(]+)\s*\(.*')
186
187 project = None
188 todo = []
189
190 for line in script:
191 m = project_re.match(line)
192 if m:
193 name = m.group(1)
194 project = projects.get(name)
195 if not project:
196 _die('project %s not available for upload', name)
197 continue
198
199 m = branch_re.match(line)
200 if m:
201 name = m.group(1)
202 if not project:
203 _die('project for branch %s not in script', name)
204 branch = branches[project.name].get(name)
205 if not branch:
206 _die('branch %s not in %s', name, project.relpath)
207 todo.append(branch)
208 if not todo:
209 _die("nothing uncommented for upload")
Dan Morrill879a9a52010-05-04 16:56:07 -0700210
211 many_commits = False
212 for branch in todo:
213 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
214 many_commits = True
215 break
216 if many_commits:
217 if not _ConfirmManyUploads(multiple_branches=True):
218 _die("upload aborted by user")
219
Joe Onorato2896a792008-11-17 16:56:36 -0500220 self._UploadAndReport(todo, people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700221
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700222 def _FindGerritChange(self, branch):
223 last_pub = branch.project.WasPublished(branch.name)
224 if last_pub is None:
225 return ""
226
227 refs = branch.GetPublishedRefs()
228 try:
229 # refs/changes/XYZ/N --> XYZ
230 return refs.get(last_pub).split('/')[-2]
231 except:
232 return ""
233
Shawn O. Pearcee92ceeb2008-11-24 15:51:25 -0800234 def _ReplaceBranch(self, project, people):
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800235 branch = project.CurrentBranch
236 if not branch:
237 print >>sys.stdout, "no branches ready for upload"
238 return
239 branch = project.GetUploadableBranch(branch)
240 if not branch:
241 print >>sys.stdout, "no branches ready for upload"
242 return
243
244 script = []
245 script.append('# Replacing from branch %s' % branch.name)
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700246
247 if len(branch.commits) == 1:
248 change = self._FindGerritChange(branch)
249 script.append('[%-6s] %s' % (change, branch.commits[0]))
250 else:
251 for commit in branch.commits:
252 script.append('[ ] %s' % commit)
253
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800254 script.append('')
255 script.append('# Insert change numbers in the brackets to add a new patch set.')
256 script.append('# To create a new change record, leave the brackets empty.')
257
258 script = Editor.EditString("\n".join(script)).split("\n")
259
260 change_re = re.compile(r'^\[\s*(\d{1,})\s*\]\s*([0-9a-f]{1,}) .*$')
261 to_replace = dict()
262 full_hashes = branch.unabbrev_commits
263
264 for line in script:
265 m = change_re.match(line)
266 if m:
Shawn O. Pearce67092442008-12-12 08:01:12 -0800267 c = m.group(1)
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800268 f = m.group(2)
269 try:
270 f = full_hashes[f]
271 except KeyError:
272 print 'fh = %s' % full_hashes
273 print >>sys.stderr, "error: commit %s not found" % f
274 sys.exit(1)
Shawn O. Pearce67092442008-12-12 08:01:12 -0800275 if c in to_replace:
276 print >>sys.stderr,\
277 "error: change %s cannot accept multiple commits" % c
278 sys.exit(1)
279 to_replace[c] = f
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800280
281 if not to_replace:
282 print >>sys.stderr, "error: no replacements specified"
283 print >>sys.stderr, " use 'repo upload' without --replace"
284 sys.exit(1)
285
Dan Morrill879a9a52010-05-04 16:56:07 -0700286 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
287 if not _ConfirmManyUploads(multiple_branches=True):
288 _die("upload aborted by user")
289
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800290 branch.replace_changes = to_replace
Joe Onorato2896a792008-11-17 16:56:36 -0500291 self._UploadAndReport([branch], people)
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800292
Joe Onorato2896a792008-11-17 16:56:36 -0500293 def _UploadAndReport(self, todo, people):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700294 have_errors = False
295 for branch in todo:
296 try:
Joe Onorato2896a792008-11-17 16:56:36 -0500297 branch.UploadForReview(people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700298 branch.uploaded = True
299 except UploadError, e:
300 branch.error = e
301 branch.uploaded = False
302 have_errors = True
303
304 print >>sys.stderr, ''
305 print >>sys.stderr, '--------------------------------------------'
306
307 if have_errors:
308 for branch in todo:
309 if not branch.uploaded:
310 print >>sys.stderr, '[FAILED] %-15s %-15s (%s)' % (
311 branch.project.relpath + '/', \
312 branch.name, \
313 branch.error)
314 print >>sys.stderr, ''
315
316 for branch in todo:
317 if branch.uploaded:
318 print >>sys.stderr, '[OK ] %-15s %s' % (
319 branch.project.relpath + '/',
320 branch.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700321
322 if have_errors:
323 sys.exit(1)
324
325 def Execute(self, opt, args):
326 project_list = self.GetProjects(args)
327 pending = []
Joe Onorato2896a792008-11-17 16:56:36 -0500328 reviewers = []
329 cc = []
330
331 if opt.reviewers:
332 reviewers = _SplitEmails(opt.reviewers)
333 if opt.cc:
334 cc = _SplitEmails(opt.cc)
335 people = (reviewers,cc)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700336
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800337 if opt.replace:
338 if len(project_list) != 1:
339 print >>sys.stderr, \
340 'error: --replace requires exactly one project'
341 sys.exit(1)
Shawn O. Pearcee92ceeb2008-11-24 15:51:25 -0800342 self._ReplaceBranch(project_list[0], people)
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800343 return
344
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700345 for project in project_list:
346 avail = project.GetUploadableBranches()
347 if avail:
348 pending.append((project, avail))
349
350 if not pending:
351 print >>sys.stdout, "no branches ready for upload"
352 elif len(pending) == 1 and len(pending[0][1]) == 1:
Joe Onorato2896a792008-11-17 16:56:36 -0500353 self._SingleBranch(pending[0][1][0], people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700354 else:
Joe Onorato2896a792008-11-17 16:56:36 -0500355 self._MultipleBranches(pending, people)