summaryrefslogtreecommitdiffstats
path: root/subcmds/upload.py
diff options
context:
space:
mode:
Diffstat (limited to 'subcmds/upload.py')
-rw-r--r--subcmds/upload.py315
1 files changed, 191 insertions, 124 deletions
diff --git a/subcmds/upload.py b/subcmds/upload.py
index 5c12aaee..c48deab6 100644
--- a/subcmds/upload.py
+++ b/subcmds/upload.py
@@ -1,5 +1,3 @@
1# -*- coding:utf-8 -*-
2#
3# Copyright (C) 2008 The Android Open Source Project 1# Copyright (C) 2008 The Android Open Source Project
4# 2#
5# Licensed under the Apache License, Version 2.0 (the "License"); 3# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,25 +12,23 @@
14# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
15# limitations under the License. 13# limitations under the License.
16 14
17from __future__ import print_function
18import copy 15import copy
16import functools
17import optparse
19import re 18import re
20import sys 19import sys
21 20
22from command import InteractiveCommand 21from command import DEFAULT_LOCAL_JOBS, InteractiveCommand
23from editor import Editor 22from editor import Editor
24from error import HookError, UploadError 23from error import UploadError
25from git_command import GitCommand 24from git_command import GitCommand
26from project import RepoHook 25from git_refs import R_HEADS
26from hooks import RepoHook
27 27
28from pyversion import is_python3
29if not is_python3():
30 input = raw_input
31else:
32 unicode = str
33 28
34UNUSUAL_COMMIT_THRESHOLD = 5 29UNUSUAL_COMMIT_THRESHOLD = 5
35 30
31
36def _ConfirmManyUploads(multiple_branches=False): 32def _ConfirmManyUploads(multiple_branches=False):
37 if multiple_branches: 33 if multiple_branches:
38 print('ATTENTION: One or more branches has an unusually high number ' 34 print('ATTENTION: One or more branches has an unusually high number '
@@ -44,19 +40,22 @@ def _ConfirmManyUploads(multiple_branches=False):
44 answer = input("If you are sure you intend to do this, type 'yes': ").strip() 40 answer = input("If you are sure you intend to do this, type 'yes': ").strip()
45 return answer == "yes" 41 return answer == "yes"
46 42
43
47def _die(fmt, *args): 44def _die(fmt, *args):
48 msg = fmt % args 45 msg = fmt % args
49 print('error: %s' % msg, file=sys.stderr) 46 print('error: %s' % msg, file=sys.stderr)
50 sys.exit(1) 47 sys.exit(1)
51 48
49
52def _SplitEmails(values): 50def _SplitEmails(values):
53 result = [] 51 result = []
54 for value in values: 52 for value in values:
55 result.extend([s.strip() for s in value.split(',')]) 53 result.extend([s.strip() for s in value.split(',')])
56 return result 54 return result
57 55
56
58class Upload(InteractiveCommand): 57class Upload(InteractiveCommand):
59 common = True 58 COMMON = True
60 helpSummary = "Upload changes for code review" 59 helpSummary = "Upload changes for code review"
61 helpUsage = """ 60 helpUsage = """
62%prog [--re --cc] [<project>]... 61%prog [--re --cc] [<project>]...
@@ -126,74 +125,89 @@ is set to "true" then repo will assume you always want the equivalent
126of the -t option to the repo command. If unset or set to "false" then 125of the -t option to the repo command. If unset or set to "false" then
127repo will make use of only the command line option. 126repo will make use of only the command line option.
128 127
128review.URL.uploadhashtags:
129
130To add hashtags whenever uploading a commit, you can set a per-project
131or global Git option to do so. The value of review.URL.uploadhashtags
132will be used as comma delimited hashtags like the --hashtag option.
133
134review.URL.uploadlabels:
135
136To add labels whenever uploading a commit, you can set a per-project
137or global Git option to do so. The value of review.URL.uploadlabels
138will be used as comma delimited labels like the --label option.
139
140review.URL.uploadnotify:
141
142Control e-mail notifications when uploading.
143https://gerrit-review.googlesource.com/Documentation/user-upload.html#notify
144
129# References 145# References
130 146
131Gerrit Code Review: https://www.gerritcodereview.com/ 147Gerrit Code Review: https://www.gerritcodereview.com/
132 148
133""" 149"""
150 PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
134 151
135 def _Options(self, p): 152 def _Options(self, p):
136 p.add_option('-t', 153 p.add_option('-t',
137 dest='auto_topic', action='store_true', 154 dest='auto_topic', action='store_true',
138 help='Send local branch name to Gerrit Code Review') 155 help='send local branch name to Gerrit Code Review')
156 p.add_option('--hashtag', '--ht',
157 dest='hashtags', action='append', default=[],
158 help='add hashtags (comma delimited) to the review')
159 p.add_option('--hashtag-branch', '--htb',
160 action='store_true',
161 help='add local branch name as a hashtag')
162 p.add_option('-l', '--label',
163 dest='labels', action='append', default=[],
164 help='add a label when uploading')
139 p.add_option('--re', '--reviewers', 165 p.add_option('--re', '--reviewers',
140 type='string', action='append', dest='reviewers', 166 type='string', action='append', dest='reviewers',
141 help='Request reviews from these people.') 167 help='request reviews from these people')
142 p.add_option('--cc', 168 p.add_option('--cc',
143 type='string', action='append', dest='cc', 169 type='string', action='append', dest='cc',
144 help='Also send email to these email addresses.') 170 help='also send email to these email addresses')
145 p.add_option('--br', 171 p.add_option('--br', '--branch',
146 type='string', action='store', dest='branch', 172 type='string', action='store', dest='branch',
147 help='Branch to upload.') 173 help='(local) branch to upload')
148 p.add_option('--cbr', '--current-branch', 174 p.add_option('-c', '--current-branch',
175 dest='current_branch', action='store_true',
176 help='upload current git branch')
177 p.add_option('--no-current-branch',
178 dest='current_branch', action='store_false',
179 help='upload all git branches')
180 # Turn this into a warning & remove this someday.
181 p.add_option('--cbr',
149 dest='current_branch', action='store_true', 182 dest='current_branch', action='store_true',
150 help='Upload current git branch.') 183 help=optparse.SUPPRESS_HELP)
151 p.add_option('-d', '--draft',
152 action='store_true', dest='draft', default=False,
153 help='If specified, upload as a draft.')
154 p.add_option('--ne', '--no-emails', 184 p.add_option('--ne', '--no-emails',
155 action='store_false', dest='notify', default=True, 185 action='store_false', dest='notify', default=True,
156 help='If specified, do not send emails on upload.') 186 help='do not send e-mails on upload')
157 p.add_option('-p', '--private', 187 p.add_option('-p', '--private',
158 action='store_true', dest='private', default=False, 188 action='store_true', dest='private', default=False,
159 help='If specified, upload as a private change.') 189 help='upload as a private change (deprecated; use --wip)')
160 p.add_option('-w', '--wip', 190 p.add_option('-w', '--wip',
161 action='store_true', dest='wip', default=False, 191 action='store_true', dest='wip', default=False,
162 help='If specified, upload as a work-in-progress change.') 192 help='upload as a work-in-progress change')
163 p.add_option('-o', '--push-option', 193 p.add_option('-o', '--push-option',
164 type='string', action='append', dest='push_options', 194 type='string', action='append', dest='push_options',
165 default=[], 195 default=[],
166 help='Additional push options to transmit') 196 help='additional push options to transmit')
167 p.add_option('-D', '--destination', '--dest', 197 p.add_option('-D', '--destination', '--dest',
168 type='string', action='store', dest='dest_branch', 198 type='string', action='store', dest='dest_branch',
169 metavar='BRANCH', 199 metavar='BRANCH',
170 help='Submit for review on this target branch.') 200 help='submit for review on this target branch')
171 201 p.add_option('-n', '--dry-run',
172 # Options relating to upload hook. Note that verify and no-verify are NOT 202 dest='dryrun', default=False, action='store_true',
173 # opposites of each other, which is why they store to different locations. 203 help='do everything except actually upload the CL')
174 # We are using them to match 'git commit' syntax. 204 p.add_option('-y', '--yes',
175 # 205 default=False, action='store_true',
176 # Combinations: 206 help='answer yes to all safe prompts')
177 # - no-verify=False, verify=False (DEFAULT):
178 # If stdout is a tty, can prompt about running upload hooks if needed.
179 # If user denies running hooks, the upload is cancelled. If stdout is
180 # not a tty and we would need to prompt about upload hooks, upload is
181 # cancelled.
182 # - no-verify=False, verify=True:
183 # Always run upload hooks with no prompt.
184 # - no-verify=True, verify=False:
185 # Never run upload hooks, but upload anyway (AKA bypass hooks).
186 # - no-verify=True, verify=True:
187 # Invalid
188 p.add_option('--no-cert-checks', 207 p.add_option('--no-cert-checks',
189 dest='validate_certs', action='store_false', default=True, 208 dest='validate_certs', action='store_false', default=True,
190 help='Disable verifying ssl certs (unsafe).') 209 help='disable verifying ssl certs (unsafe)')
191 p.add_option('--no-verify', 210 RepoHook.AddOptionGroup(p, 'pre-upload')
192 dest='bypass_hooks', action='store_true',
193 help='Do not run the upload hook.')
194 p.add_option('--verify',
195 dest='allow_all_hooks', action='store_true',
196 help='Run the upload hook without prompting.')
197 211
198 def _SingleBranch(self, opt, branch, people): 212 def _SingleBranch(self, opt, branch, people):
199 project = branch.project 213 project = branch.project
@@ -212,20 +226,24 @@ Gerrit Code Review: https://www.gerritcodereview.com/
212 226
213 destination = opt.dest_branch or project.dest_branch or project.revisionExpr 227 destination = opt.dest_branch or project.dest_branch or project.revisionExpr
214 print('Upload project %s/ to remote branch %s%s:' % 228 print('Upload project %s/ to remote branch %s%s:' %
215 (project.relpath, destination, ' (draft)' if opt.draft else '')) 229 (project.relpath, destination, ' (private)' if opt.private else ''))
216 print(' branch %s (%2d commit%s, %s):' % ( 230 print(' branch %s (%2d commit%s, %s):' % (
217 name, 231 name,
218 len(commit_list), 232 len(commit_list),
219 len(commit_list) != 1 and 's' or '', 233 len(commit_list) != 1 and 's' or '',
220 date)) 234 date))
221 for commit in commit_list: 235 for commit in commit_list:
222 print(' %s' % commit) 236 print(' %s' % commit)
223 237
224 print('to %s (y/N)? ' % remote.review, end='') 238 print('to %s (y/N)? ' % remote.review, end='')
225 # TODO: When we require Python 3, use flush=True w/print above. 239 # TODO: When we require Python 3, use flush=True w/print above.
226 sys.stdout.flush() 240 sys.stdout.flush()
227 answer = sys.stdin.readline().strip().lower() 241 if opt.yes:
228 answer = answer in ('y', 'yes', '1', 'true', 't') 242 print('<--yes>')
243 answer = True
244 else:
245 answer = sys.stdin.readline().strip().lower()
246 answer = answer in ('y', 'yes', '1', 'true', 't')
229 247
230 if answer: 248 if answer:
231 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD: 249 if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD:
@@ -322,12 +340,12 @@ Gerrit Code Review: https://www.gerritcodereview.com/
322 340
323 key = 'review.%s.autoreviewer' % project.GetBranch(name).remote.review 341 key = 'review.%s.autoreviewer' % project.GetBranch(name).remote.review
324 raw_list = project.config.GetString(key) 342 raw_list = project.config.GetString(key)
325 if not raw_list is None: 343 if raw_list is not None:
326 people[0].extend([entry.strip() for entry in raw_list.split(',')]) 344 people[0].extend([entry.strip() for entry in raw_list.split(',')])
327 345
328 key = 'review.%s.autocopy' % project.GetBranch(name).remote.review 346 key = 'review.%s.autocopy' % project.GetBranch(name).remote.review
329 raw_list = project.config.GetString(key) 347 raw_list = project.config.GetString(key)
330 if not raw_list is None and len(people[0]) > 0: 348 if raw_list is not None and len(people[0]) > 0:
331 people[1].extend([entry.strip() for entry in raw_list.split(',')]) 349 people[1].extend([entry.strip() for entry in raw_list.split(',')])
332 350
333 def _FindGerritChange(self, branch): 351 def _FindGerritChange(self, branch):
@@ -364,7 +382,11 @@ Gerrit Code Review: https://www.gerritcodereview.com/
364 print('Continue uploading? (y/N) ', end='') 382 print('Continue uploading? (y/N) ', end='')
365 # TODO: When we require Python 3, use flush=True w/print above. 383 # TODO: When we require Python 3, use flush=True w/print above.
366 sys.stdout.flush() 384 sys.stdout.flush()
367 a = sys.stdin.readline().strip().lower() 385 if opt.yes:
386 print('<--yes>')
387 a = 'yes'
388 else:
389 a = sys.stdin.readline().strip().lower()
368 if a not in ('y', 'yes', 't', 'true', 'on'): 390 if a not in ('y', 'yes', 't', 'true', 'on'):
369 print("skipping upload", file=sys.stderr) 391 print("skipping upload", file=sys.stderr)
370 branch.uploaded = False 392 branch.uploaded = False
@@ -376,12 +398,51 @@ Gerrit Code Review: https://www.gerritcodereview.com/
376 key = 'review.%s.uploadtopic' % branch.project.remote.review 398 key = 'review.%s.uploadtopic' % branch.project.remote.review
377 opt.auto_topic = branch.project.config.GetBoolean(key) 399 opt.auto_topic = branch.project.config.GetBoolean(key)
378 400
401 def _ExpandCommaList(value):
402 """Split |value| up into comma delimited entries."""
403 if not value:
404 return
405 for ret in value.split(','):
406 ret = ret.strip()
407 if ret:
408 yield ret
409
410 # Check if hashtags should be included.
411 key = 'review.%s.uploadhashtags' % branch.project.remote.review
412 hashtags = set(_ExpandCommaList(branch.project.config.GetString(key)))
413 for tag in opt.hashtags:
414 hashtags.update(_ExpandCommaList(tag))
415 if opt.hashtag_branch:
416 hashtags.add(branch.name)
417
418 # Check if labels should be included.
419 key = 'review.%s.uploadlabels' % branch.project.remote.review
420 labels = set(_ExpandCommaList(branch.project.config.GetString(key)))
421 for label in opt.labels:
422 labels.update(_ExpandCommaList(label))
423 # Basic sanity check on label syntax.
424 for label in labels:
425 if not re.match(r'^.+[+-][0-9]+$', label):
426 print('repo: error: invalid label syntax "%s": labels use forms '
427 'like CodeReview+1 or Verified-1' % (label,), file=sys.stderr)
428 sys.exit(1)
429
430 # Handle e-mail notifications.
431 if opt.notify is False:
432 notify = 'NONE'
433 else:
434 key = 'review.%s.uploadnotify' % branch.project.remote.review
435 notify = branch.project.config.GetString(key)
436
379 destination = opt.dest_branch or branch.project.dest_branch 437 destination = opt.dest_branch or branch.project.dest_branch
380 438
381 # Make sure our local branch is not setup to track a different remote branch 439 # Make sure our local branch is not setup to track a different remote branch
382 merge_branch = self._GetMergeBranch(branch.project) 440 merge_branch = self._GetMergeBranch(branch.project)
383 if destination: 441 if destination:
384 full_dest = 'refs/heads/%s' % destination 442 full_dest = destination
443 if not full_dest.startswith(R_HEADS):
444 full_dest = R_HEADS + full_dest
445
385 if not opt.dest_branch and merge_branch and merge_branch != full_dest: 446 if not opt.dest_branch and merge_branch and merge_branch != full_dest:
386 print('merge branch %s does not match destination branch %s' 447 print('merge branch %s does not match destination branch %s'
387 % (merge_branch, full_dest)) 448 % (merge_branch, full_dest))
@@ -392,10 +453,12 @@ Gerrit Code Review: https://www.gerritcodereview.com/
392 continue 453 continue
393 454
394 branch.UploadForReview(people, 455 branch.UploadForReview(people,
456 dryrun=opt.dryrun,
395 auto_topic=opt.auto_topic, 457 auto_topic=opt.auto_topic,
396 draft=opt.draft, 458 hashtags=hashtags,
459 labels=labels,
397 private=opt.private, 460 private=opt.private,
398 notify=None if opt.notify else 'NONE', 461 notify=notify,
399 wip=opt.wip, 462 wip=opt.wip,
400 dest_branch=destination, 463 dest_branch=destination,
401 validate_certs=opt.validate_certs, 464 validate_certs=opt.validate_certs,
@@ -418,18 +481,18 @@ Gerrit Code Review: https://www.gerritcodereview.com/
418 else: 481 else:
419 fmt = '\n (%s)' 482 fmt = '\n (%s)'
420 print(('[FAILED] %-15s %-15s' + fmt) % ( 483 print(('[FAILED] %-15s %-15s' + fmt) % (
421 branch.project.relpath + '/', \ 484 branch.project.relpath + '/',
422 branch.name, \ 485 branch.name,
423 str(branch.error)), 486 str(branch.error)),
424 file=sys.stderr) 487 file=sys.stderr)
425 print() 488 print()
426 489
427 for branch in todo: 490 for branch in todo:
428 if branch.uploaded: 491 if branch.uploaded:
429 print('[OK ] %-15s %s' % ( 492 print('[OK ] %-15s %s' % (
430 branch.project.relpath + '/', 493 branch.project.relpath + '/',
431 branch.name), 494 branch.name),
432 file=sys.stderr) 495 file=sys.stderr)
433 496
434 if have_errors: 497 if have_errors:
435 sys.exit(1) 498 sys.exit(1)
@@ -437,68 +500,72 @@ Gerrit Code Review: https://www.gerritcodereview.com/
437 def _GetMergeBranch(self, project): 500 def _GetMergeBranch(self, project):
438 p = GitCommand(project, 501 p = GitCommand(project,
439 ['rev-parse', '--abbrev-ref', 'HEAD'], 502 ['rev-parse', '--abbrev-ref', 'HEAD'],
440 capture_stdout = True, 503 capture_stdout=True,
441 capture_stderr = True) 504 capture_stderr=True)
442 p.Wait() 505 p.Wait()
443 local_branch = p.stdout.strip() 506 local_branch = p.stdout.strip()
444 p = GitCommand(project, 507 p = GitCommand(project,
445 ['config', '--get', 'branch.%s.merge' % local_branch], 508 ['config', '--get', 'branch.%s.merge' % local_branch],
446 capture_stdout = True, 509 capture_stdout=True,
447 capture_stderr = True) 510 capture_stderr=True)
448 p.Wait() 511 p.Wait()
449 merge_branch = p.stdout.strip() 512 merge_branch = p.stdout.strip()
450 return merge_branch 513 return merge_branch
451 514
515 @staticmethod
516 def _GatherOne(opt, project):
517 """Figure out the upload status for |project|."""
518 if opt.current_branch:
519 cbr = project.CurrentBranch
520 up_branch = project.GetUploadableBranch(cbr)
521 avail = [up_branch] if up_branch else None
522 else:
523 avail = project.GetUploadableBranches(opt.branch)
524 return (project, avail)
525
452 def Execute(self, opt, args): 526 def Execute(self, opt, args):
453 project_list = self.GetProjects(args) 527 projects = self.GetProjects(args)
454 pending = [] 528
455 reviewers = [] 529 def _ProcessResults(_pool, _out, results):
456 cc = [] 530 pending = []
457 branch = None 531 for result in results:
458 532 project, avail = result
459 if opt.branch: 533 if avail is None:
460 branch = opt.branch 534 print('repo: error: %s: Unable to upload branch "%s". '
461 535 'You might be able to fix the branch by running:\n'
462 for project in project_list: 536 ' git branch --set-upstream-to m/%s' %
463 if opt.current_branch: 537 (project.relpath, project.CurrentBranch, self.manifest.branch),
464 cbr = project.CurrentBranch
465 up_branch = project.GetUploadableBranch(cbr)
466 if up_branch:
467 avail = [up_branch]
468 else:
469 avail = None
470 print('ERROR: Current branch (%s) not uploadable. '
471 'You may be able to type '
472 '"git branch --set-upstream-to m/master" to fix '
473 'your branch.' % str(cbr),
474 file=sys.stderr) 538 file=sys.stderr)
475 else: 539 elif avail:
476 avail = project.GetUploadableBranches(branch) 540 pending.append(result)
477 if avail: 541 return pending
478 pending.append((project, avail)) 542
543 pending = self.ExecuteInParallel(
544 opt.jobs,
545 functools.partial(self._GatherOne, opt),
546 projects,
547 callback=_ProcessResults)
479 548
480 if not pending: 549 if not pending:
481 print("no branches ready for upload", file=sys.stderr) 550 if opt.branch is None:
482 return 551 print('repo: error: no branches ready for upload', file=sys.stderr)
483 552 else:
484 if not opt.bypass_hooks: 553 print('repo: error: no branches named "%s" ready for upload' %
485 hook = RepoHook('pre-upload', self.manifest.repo_hooks_project, 554 (opt.branch,), file=sys.stderr)
486 self.manifest.topdir, 555 return 1
487 self.manifest.manifestProject.GetRemote('origin').url, 556
488 abort_if_user_denies=True) 557 pending_proj_names = [project.name for (project, available) in pending]
489 pending_proj_names = [project.name for (project, available) in pending] 558 pending_worktrees = [project.worktree for (project, available) in pending]
490 pending_worktrees = [project.worktree for (project, available) in pending] 559 hook = RepoHook.FromSubcmd(
491 try: 560 hook_type='pre-upload', manifest=self.manifest,
492 hook.Run(opt.allow_all_hooks, project_list=pending_proj_names, 561 opt=opt, abort_if_user_denies=True)
493 worktree_list=pending_worktrees) 562 if not hook.Run(
494 except HookError as e: 563 project_list=pending_proj_names,
495 print("ERROR: %s" % str(e), file=sys.stderr) 564 worktree_list=pending_worktrees):
496 return 565 return 1
497 566
498 if opt.reviewers: 567 reviewers = _SplitEmails(opt.reviewers) if opt.reviewers else []
499 reviewers = _SplitEmails(opt.reviewers) 568 cc = _SplitEmails(opt.cc) if opt.cc else []
500 if opt.cc:
501 cc = _SplitEmails(opt.cc)
502 people = (reviewers, cc) 569 people = (reviewers, cc)
503 570
504 if len(pending) == 1 and len(pending[0][1]) == 1: 571 if len(pending) == 1 and len(pending[0][1]) == 1: