blob: c1958373f043bb299e2378435c51b6840666783a [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
Doug Anderson37282b42011-03-04 11:54:18 -080022from error import HookError, UploadError
23from project import RepoHook
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070024
Dan Morrillf0a9a1a2010-05-05 08:18:35 -070025UNUSUAL_COMMIT_THRESHOLD = 5
Dan Morrill879a9a52010-05-04 16:56:07 -070026
27def _ConfirmManyUploads(multiple_branches=False):
28 if multiple_branches:
29 print "ATTENTION: One or more branches has an unusually high number of commits."
30 else:
31 print "ATTENTION: You are uploading an unusually high number of commits."
32 print "YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across branches?)"
33 answer = raw_input("If you are sure you intend to do this, type 'yes': ").strip()
34 return answer == "yes"
35
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070036def _die(fmt, *args):
37 msg = fmt % args
38 print >>sys.stderr, 'error: %s' % msg
39 sys.exit(1)
40
Joe Onorato2896a792008-11-17 16:56:36 -050041def _SplitEmails(values):
42 result = []
43 for str in values:
44 result.extend([s.strip() for s in str.split(',')])
45 return result
46
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070047class Upload(InteractiveCommand):
48 common = True
49 helpSummary = "Upload changes for code review"
50 helpUsage="""
Ficus Kirkpatricka0de6e82010-10-22 13:06:47 -070051%prog [--re --cc] [<project>]...
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070052"""
53 helpDescription = """
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070054The '%prog' command is used to send changes to the Gerrit Code
55Review system. It searches for topic branches in local projects
56that have not yet been published for review. If multiple topic
57branches are found, '%prog' opens an editor to allow the user to
58select which branches to upload.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070059
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070060'%prog' searches for uploadable changes in all projects listed at
61the command line. Projects can be specified either by name, or by
62a relative or absolute path to the project's local directory. If no
63projects are specified, '%prog' will search for uploadable changes
64in all projects listed in the manifest.
Joe Onorato2896a792008-11-17 16:56:36 -050065
66If the --reviewers or --cc options are passed, those emails are
67added to the respective list of users, and emails are sent to any
Shawn O. Pearce337fb9c2009-04-18 10:59:33 -070068new users. Users passed as --reviewers must already be registered
Joe Onorato2896a792008-11-17 16:56:36 -050069with the code review system, or the upload will fail.
Shawn O. Pearcea6df7d22008-12-12 08:04:07 -080070
Shawn O. Pearcea608fb02009-04-17 12:11:24 -070071Configuration
72-------------
73
74review.URL.autoupload:
75
76To disable the "Upload ... (y/n)?" prompt, you can set a per-project
77or global Git configuration option. If review.URL.autoupload is set
78to "true" then repo will assume you always answer "y" at the prompt,
79and will not prompt you further. If it is set to "false" then repo
80will assume you always answer "n", and will abort.
81
Ben Komalo08a3f682010-07-15 16:03:02 -070082review.URL.autocopy:
83
84To automatically copy a user or mailing list to all uploaded reviews,
85you can set a per-project or global Git option to do so. Specifically,
86review.URL.autocopy can be set to a comma separated list of reviewers
87who you always want copied on all uploads with a non-empty --re
88argument.
89
Shawn O. Pearce3575b8f2010-07-15 17:00:14 -070090review.URL.username:
91
92Override the username used to connect to Gerrit Code Review.
93By default the local part of the email address is used.
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):
Shawn O. Pearcea5ece0e2010-07-15 16:52:42 -0700114 p.add_option('-t',
115 dest='auto_topic', action='store_true',
116 help='Send local branch name to Gerrit Code Review')
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.')
Mandeep Singh Bainesd6c93a22011-05-26 10:34:11 -0700123 p.add_option('--br',
124 type='string', action='store', dest='branch',
125 help='Branch to upload.')
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800126
Doug Anderson37282b42011-03-04 11:54:18 -0800127 # Options relating to upload hook. Note that verify and no-verify are NOT
128 # opposites of each other, which is why they store to different locations.
129 # We are using them to match 'git commit' syntax.
130 #
131 # Combinations:
132 # - no-verify=False, verify=False (DEFAULT):
133 # If stdout is a tty, can prompt about running upload hooks if needed.
134 # If user denies running hooks, the upload is cancelled. If stdout is
135 # not a tty and we would need to prompt about upload hooks, upload is
136 # cancelled.
137 # - no-verify=False, verify=True:
138 # Always run upload hooks with no prompt.
139 # - no-verify=True, verify=False:
140 # Never run upload hooks, but upload anyway (AKA bypass hooks).
141 # - no-verify=True, verify=True:
142 # Invalid
143 p.add_option('--no-verify',
144 dest='bypass_hooks', action='store_true',
145 help='Do not run the upload hook.')
146 p.add_option('--verify',
147 dest='allow_all_hooks', action='store_true',
148 help='Run the upload hook without prompting.')
149
Shawn O. Pearcea5ece0e2010-07-15 16:52:42 -0700150 def _SingleBranch(self, opt, branch, people):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700151 project = branch.project
152 name = branch.name
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700153 remote = project.GetBranch(name).remote
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700154
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700155 key = 'review.%s.autoupload' % remote.review
156 answer = project.config.GetBoolean(key)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700157
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700158 if answer is False:
159 _die("upload blocked by %s = false" % key)
160
161 if answer is None:
Shawn O. Pearce66bdd462009-04-17 18:47:22 -0700162 date = branch.date
163 list = branch.commits
164
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700165 print 'Upload project %s/:' % project.relpath
166 print ' branch %s (%2d commit%s, %s):' % (
167 name,
168 len(list),
169 len(list) != 1 and 's' or '',
170 date)
171 for commit in list:
172 print ' %s' % commit
173
Shawn O. Pearce8225cdc2009-04-18 11:00:35 -0700174 sys.stdout.write('to %s (y/n)? ' % remote.review)
Shawn O. Pearcea608fb02009-04-17 12:11:24 -0700175 answer = sys.stdin.readline().strip()
176 answer = answer in ('y', 'Y', 'yes', '1', 'true', 't')
177
178 if answer:
Dan Morrill879a9a52010-05-04 16:56:07 -0700179 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
180 answer = _ConfirmManyUploads()
181
182 if answer:
Shawn O. Pearcea5ece0e2010-07-15 16:52:42 -0700183 self._UploadAndReport(opt, [branch], people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700184 else:
185 _die("upload aborted by user")
186
Shawn O. Pearcea5ece0e2010-07-15 16:52:42 -0700187 def _MultipleBranches(self, opt, pending, people):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700188 projects = {}
189 branches = {}
190
191 script = []
192 script.append('# Uncomment the branches to upload:')
193 for project, avail in pending:
194 script.append('#')
195 script.append('# project %s/:' % project.relpath)
196
197 b = {}
198 for branch in avail:
199 name = branch.name
200 date = branch.date
201 list = branch.commits
202
203 if b:
204 script.append('#')
205 script.append('# branch %s (%2d commit%s, %s):' % (
206 name,
207 len(list),
208 len(list) != 1 and 's' or '',
209 date))
210 for commit in list:
211 script.append('# %s' % commit)
212 b[name] = branch
213
214 projects[project.relpath] = project
215 branches[project.name] = b
216 script.append('')
217
218 script = Editor.EditString("\n".join(script)).split("\n")
219
220 project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$')
221 branch_re = re.compile(r'^\s*branch\s*([^\s(]+)\s*\(.*')
222
223 project = None
224 todo = []
225
226 for line in script:
227 m = project_re.match(line)
228 if m:
229 name = m.group(1)
230 project = projects.get(name)
231 if not project:
232 _die('project %s not available for upload', name)
233 continue
234
235 m = branch_re.match(line)
236 if m:
237 name = m.group(1)
238 if not project:
239 _die('project for branch %s not in script', name)
240 branch = branches[project.name].get(name)
241 if not branch:
242 _die('branch %s not in %s', name, project.relpath)
243 todo.append(branch)
244 if not todo:
245 _die("nothing uncommented for upload")
Dan Morrill879a9a52010-05-04 16:56:07 -0700246
247 many_commits = False
248 for branch in todo:
249 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
250 many_commits = True
251 break
252 if many_commits:
253 if not _ConfirmManyUploads(multiple_branches=True):
254 _die("upload aborted by user")
255
Shawn O. Pearcea5ece0e2010-07-15 16:52:42 -0700256 self._UploadAndReport(opt, todo, people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700257
Ben Komalo08a3f682010-07-15 16:03:02 -0700258 def _AppendAutoCcList(self, branch, people):
259 """
260 Appends the list of users in the CC list in the git project's config if a
261 non-empty reviewer list was found.
262 """
263
264 name = branch.name
265 project = branch.project
266 key = 'review.%s.autocopy' % project.GetBranch(name).remote.review
267 raw_list = project.config.GetString(key)
268 if not raw_list is None and len(people[0]) > 0:
269 people[1].extend([entry.strip() for entry in raw_list.split(',')])
270
Ficus Kirkpatrickbc7ef672009-05-04 12:45:11 -0700271 def _FindGerritChange(self, branch):
272 last_pub = branch.project.WasPublished(branch.name)
273 if last_pub is None:
274 return ""
275
276 refs = branch.GetPublishedRefs()
277 try:
278 # refs/changes/XYZ/N --> XYZ
279 return refs.get(last_pub).split('/')[-2]
280 except:
281 return ""
282
Shawn O. Pearcea5ece0e2010-07-15 16:52:42 -0700283 def _UploadAndReport(self, opt, todo, original_people):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700284 have_errors = False
285 for branch in todo:
286 try:
Ben Komalo08a3f682010-07-15 16:03:02 -0700287 people = copy.deepcopy(original_people)
288 self._AppendAutoCcList(branch, people)
289
Anthony Newnamcc50bac2010-04-08 10:28:59 -0500290 # Check if there are local changes that may have been forgotten
291 if branch.project.HasChanges():
292 key = 'review.%s.autoupload' % branch.project.remote.review
293 answer = branch.project.config.GetBoolean(key)
294
295 # if they want to auto upload, let's not ask because it could be automated
296 if answer is None:
297 sys.stdout.write('Uncommitted changes in ' + branch.project.name + ' (did you forget to amend?). Continue uploading? (y/n) ')
298 a = sys.stdin.readline().strip().lower()
299 if a not in ('y', 'yes', 't', 'true', 'on'):
300 print >>sys.stderr, "skipping upload"
301 branch.uploaded = False
302 branch.error = 'User aborted'
303 continue
304
Shawn O. Pearcea5ece0e2010-07-15 16:52:42 -0700305 branch.UploadForReview(people, auto_topic=opt.auto_topic)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700306 branch.uploaded = True
307 except UploadError, e:
308 branch.error = e
309 branch.uploaded = False
310 have_errors = True
311
312 print >>sys.stderr, ''
Shawn O. Pearcef00e0ce2009-08-22 18:39:49 -0700313 print >>sys.stderr, '----------------------------------------------------------------------'
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700314
315 if have_errors:
316 for branch in todo:
317 if not branch.uploaded:
Shawn O. Pearcef00e0ce2009-08-22 18:39:49 -0700318 if len(str(branch.error)) <= 30:
319 fmt = ' (%s)'
320 else:
321 fmt = '\n (%s)'
322 print >>sys.stderr, ('[FAILED] %-15s %-15s' + fmt) % (
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700323 branch.project.relpath + '/', \
324 branch.name, \
Shawn O. Pearcef00e0ce2009-08-22 18:39:49 -0700325 str(branch.error))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700326 print >>sys.stderr, ''
327
328 for branch in todo:
329 if branch.uploaded:
330 print >>sys.stderr, '[OK ] %-15s %s' % (
331 branch.project.relpath + '/',
332 branch.name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700333
334 if have_errors:
335 sys.exit(1)
336
337 def Execute(self, opt, args):
338 project_list = self.GetProjects(args)
339 pending = []
Joe Onorato2896a792008-11-17 16:56:36 -0500340 reviewers = []
341 cc = []
Mandeep Singh Bainesd6c93a22011-05-26 10:34:11 -0700342 branch = None
343
344 if opt.branch:
345 branch = opt.branch
Joe Onorato2896a792008-11-17 16:56:36 -0500346
Doug Anderson37282b42011-03-04 11:54:18 -0800347 for project in project_list:
Mandeep Singh Bainesd6c93a22011-05-26 10:34:11 -0700348 avail = project.GetUploadableBranches(branch)
Doug Anderson37282b42011-03-04 11:54:18 -0800349 if avail:
350 pending.append((project, avail))
351
352 if pending and (not opt.bypass_hooks):
353 hook = RepoHook('pre-upload', self.manifest.repo_hooks_project,
354 self.manifest.topdir, abort_if_user_denies=True)
355 pending_proj_names = [project.name for (project, avail) in pending]
356 try:
357 hook.Run(opt.allow_all_hooks, project_list=pending_proj_names)
358 except HookError, e:
359 print >>sys.stderr, "ERROR: %s" % str(e)
360 return
361
Joe Onorato2896a792008-11-17 16:56:36 -0500362 if opt.reviewers:
363 reviewers = _SplitEmails(opt.reviewers)
364 if opt.cc:
365 cc = _SplitEmails(opt.cc)
366 people = (reviewers,cc)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700367
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700368 if not pending:
369 print >>sys.stdout, "no branches ready for upload"
370 elif len(pending) == 1 and len(pending[0][1]) == 1:
Shawn O. Pearcea5ece0e2010-07-15 16:52:42 -0700371 self._SingleBranch(opt, pending[0][1][0], people)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700372 else:
Shawn O. Pearcea5ece0e2010-07-15 16:52:42 -0700373 self._MultipleBranches(opt, pending, people)