diff options
Diffstat (limited to 'subcmds/upload.py')
| -rw-r--r-- | subcmds/upload.py | 1152 |
1 files changed, 664 insertions, 488 deletions
diff --git a/subcmds/upload.py b/subcmds/upload.py index 9c279230..63216afb 100644 --- a/subcmds/upload.py +++ b/subcmds/upload.py | |||
| @@ -32,69 +32,77 @@ _DEFAULT_UNUSUAL_COMMIT_THRESHOLD = 5 | |||
| 32 | 32 | ||
| 33 | 33 | ||
| 34 | def _VerifyPendingCommits(branches: List[ReviewableBranch]) -> bool: | 34 | def _VerifyPendingCommits(branches: List[ReviewableBranch]) -> bool: |
| 35 | """Perform basic safety checks on the given set of branches. | 35 | """Perform basic safety checks on the given set of branches. |
| 36 | 36 | ||
| 37 | Ensures that each branch does not have a "large" number of commits | 37 | Ensures that each branch does not have a "large" number of commits |
| 38 | and, if so, prompts the user to confirm they want to proceed with | 38 | and, if so, prompts the user to confirm they want to proceed with |
| 39 | the upload. | 39 | the upload. |
| 40 | 40 | ||
| 41 | Returns true if all branches pass the safety check or the user | 41 | Returns true if all branches pass the safety check or the user |
| 42 | confirmed. Returns false if the upload should be aborted. | 42 | confirmed. Returns false if the upload should be aborted. |
| 43 | """ | 43 | """ |
| 44 | 44 | ||
| 45 | # Determine if any branch has a suspicious number of commits. | 45 | # Determine if any branch has a suspicious number of commits. |
| 46 | many_commits = False | 46 | many_commits = False |
| 47 | for branch in branches: | 47 | for branch in branches: |
| 48 | # Get the user's unusual threshold for the branch. | 48 | # Get the user's unusual threshold for the branch. |
| 49 | # | 49 | # |
| 50 | # Each branch may be configured to have a different threshold. | 50 | # Each branch may be configured to have a different threshold. |
| 51 | remote = branch.project.GetBranch(branch.name).remote | 51 | remote = branch.project.GetBranch(branch.name).remote |
| 52 | key = f'review.{remote.review}.uploadwarningthreshold' | 52 | key = f"review.{remote.review}.uploadwarningthreshold" |
| 53 | threshold = branch.project.config.GetInt(key) | 53 | threshold = branch.project.config.GetInt(key) |
| 54 | if threshold is None: | 54 | if threshold is None: |
| 55 | threshold = _DEFAULT_UNUSUAL_COMMIT_THRESHOLD | 55 | threshold = _DEFAULT_UNUSUAL_COMMIT_THRESHOLD |
| 56 | 56 | ||
| 57 | # If the branch has more commits than the threshold, show a warning. | 57 | # If the branch has more commits than the threshold, show a warning. |
| 58 | if len(branch.commits) > threshold: | 58 | if len(branch.commits) > threshold: |
| 59 | many_commits = True | 59 | many_commits = True |
| 60 | break | 60 | break |
| 61 | 61 | ||
| 62 | # If any branch has many commits, prompt the user. | 62 | # If any branch has many commits, prompt the user. |
| 63 | if many_commits: | 63 | if many_commits: |
| 64 | if len(branches) > 1: | 64 | if len(branches) > 1: |
| 65 | print('ATTENTION: One or more branches has an unusually high number ' | 65 | print( |
| 66 | 'of commits.') | 66 | "ATTENTION: One or more branches has an unusually high number " |
| 67 | else: | 67 | "of commits." |
| 68 | print('ATTENTION: You are uploading an unusually high number of commits.') | 68 | ) |
| 69 | print('YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across ' | 69 | else: |
| 70 | 'branches?)') | 70 | print( |
| 71 | answer = input( | 71 | "ATTENTION: You are uploading an unusually high number of " |
| 72 | "If you are sure you intend to do this, type 'yes': ").strip() | 72 | "commits." |
| 73 | return answer == 'yes' | 73 | ) |
| 74 | 74 | print( | |
| 75 | return True | 75 | "YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across " |
| 76 | "branches?)" | ||
| 77 | ) | ||
| 78 | answer = input( | ||
| 79 | "If you are sure you intend to do this, type 'yes': " | ||
| 80 | ).strip() | ||
| 81 | return answer == "yes" | ||
| 82 | |||
| 83 | return True | ||
| 76 | 84 | ||
| 77 | 85 | ||
| 78 | def _die(fmt, *args): | 86 | def _die(fmt, *args): |
| 79 | msg = fmt % args | 87 | msg = fmt % args |
| 80 | print('error: %s' % msg, file=sys.stderr) | 88 | print("error: %s" % msg, file=sys.stderr) |
| 81 | sys.exit(1) | 89 | sys.exit(1) |
| 82 | 90 | ||
| 83 | 91 | ||
| 84 | def _SplitEmails(values): | 92 | def _SplitEmails(values): |
| 85 | result = [] | 93 | result = [] |
| 86 | for value in values: | 94 | for value in values: |
| 87 | result.extend([s.strip() for s in value.split(',')]) | 95 | result.extend([s.strip() for s in value.split(",")]) |
| 88 | return result | 96 | return result |
| 89 | 97 | ||
| 90 | 98 | ||
| 91 | class Upload(InteractiveCommand): | 99 | class Upload(InteractiveCommand): |
| 92 | COMMON = True | 100 | COMMON = True |
| 93 | helpSummary = "Upload changes for code review" | 101 | helpSummary = "Upload changes for code review" |
| 94 | helpUsage = """ | 102 | helpUsage = """ |
| 95 | %prog [--re --cc] [<project>]... | 103 | %prog [--re --cc] [<project>]... |
| 96 | """ | 104 | """ |
| 97 | helpDescription = """ | 105 | helpDescription = """ |
| 98 | The '%prog' command is used to send changes to the Gerrit Code | 106 | The '%prog' command is used to send changes to the Gerrit Code |
| 99 | Review system. It searches for topic branches in local projects | 107 | Review system. It searches for topic branches in local projects |
| 100 | that have not yet been published for review. If multiple topic | 108 | that have not yet been published for review. If multiple topic |
| @@ -195,443 +203,611 @@ threshold to a different value. | |||
| 195 | Gerrit Code Review: https://www.gerritcodereview.com/ | 203 | Gerrit Code Review: https://www.gerritcodereview.com/ |
| 196 | 204 | ||
| 197 | """ | 205 | """ |
| 198 | PARALLEL_JOBS = DEFAULT_LOCAL_JOBS | 206 | PARALLEL_JOBS = DEFAULT_LOCAL_JOBS |
| 199 | 207 | ||
| 200 | def _Options(self, p): | 208 | def _Options(self, p): |
| 201 | p.add_option('-t', | 209 | p.add_option( |
| 202 | dest='auto_topic', action='store_true', | 210 | "-t", |
| 203 | help='send local branch name to Gerrit Code Review') | 211 | dest="auto_topic", |
| 204 | p.add_option('--hashtag', '--ht', | 212 | action="store_true", |
| 205 | dest='hashtags', action='append', default=[], | 213 | help="send local branch name to Gerrit Code Review", |
| 206 | help='add hashtags (comma delimited) to the review') | 214 | ) |
| 207 | p.add_option('--hashtag-branch', '--htb', | 215 | p.add_option( |
| 208 | action='store_true', | 216 | "--hashtag", |
| 209 | help='add local branch name as a hashtag') | 217 | "--ht", |
| 210 | p.add_option('-l', '--label', | 218 | dest="hashtags", |
| 211 | dest='labels', action='append', default=[], | 219 | action="append", |
| 212 | help='add a label when uploading') | 220 | default=[], |
| 213 | p.add_option('--re', '--reviewers', | 221 | help="add hashtags (comma delimited) to the review", |
| 214 | type='string', action='append', dest='reviewers', | 222 | ) |
| 215 | help='request reviews from these people') | 223 | p.add_option( |
| 216 | p.add_option('--cc', | 224 | "--hashtag-branch", |
| 217 | type='string', action='append', dest='cc', | 225 | "--htb", |
| 218 | help='also send email to these email addresses') | 226 | action="store_true", |
| 219 | p.add_option('--br', '--branch', | 227 | help="add local branch name as a hashtag", |
| 220 | type='string', action='store', dest='branch', | 228 | ) |
| 221 | help='(local) branch to upload') | 229 | p.add_option( |
| 222 | p.add_option('-c', '--current-branch', | 230 | "-l", |
| 223 | dest='current_branch', action='store_true', | 231 | "--label", |
| 224 | help='upload current git branch') | 232 | dest="labels", |
| 225 | p.add_option('--no-current-branch', | 233 | action="append", |
| 226 | dest='current_branch', action='store_false', | 234 | default=[], |
| 227 | help='upload all git branches') | 235 | help="add a label when uploading", |
| 228 | # Turn this into a warning & remove this someday. | 236 | ) |
| 229 | p.add_option('--cbr', | 237 | p.add_option( |
| 230 | dest='current_branch', action='store_true', | 238 | "--re", |
| 231 | help=optparse.SUPPRESS_HELP) | 239 | "--reviewers", |
| 232 | p.add_option('--ne', '--no-emails', | 240 | type="string", |
| 233 | action='store_false', dest='notify', default=True, | 241 | action="append", |
| 234 | help='do not send e-mails on upload') | 242 | dest="reviewers", |
| 235 | p.add_option('-p', '--private', | 243 | help="request reviews from these people", |
| 236 | action='store_true', dest='private', default=False, | 244 | ) |
| 237 | help='upload as a private change (deprecated; use --wip)') | 245 | p.add_option( |
| 238 | p.add_option('-w', '--wip', | 246 | "--cc", |
| 239 | action='store_true', dest='wip', default=False, | 247 | type="string", |
| 240 | help='upload as a work-in-progress change') | 248 | action="append", |
| 241 | p.add_option('-r', '--ready', | 249 | dest="cc", |
| 242 | action='store_true', default=False, | 250 | help="also send email to these email addresses", |
| 243 | help='mark change as ready (clears work-in-progress setting)') | 251 | ) |
| 244 | p.add_option('-o', '--push-option', | 252 | p.add_option( |
| 245 | type='string', action='append', dest='push_options', | 253 | "--br", |
| 246 | default=[], | 254 | "--branch", |
| 247 | help='additional push options to transmit') | 255 | type="string", |
| 248 | p.add_option('-D', '--destination', '--dest', | 256 | action="store", |
| 249 | type='string', action='store', dest='dest_branch', | 257 | dest="branch", |
| 250 | metavar='BRANCH', | 258 | help="(local) branch to upload", |
| 251 | help='submit for review on this target branch') | 259 | ) |
| 252 | p.add_option('-n', '--dry-run', | 260 | p.add_option( |
| 253 | dest='dryrun', default=False, action='store_true', | 261 | "-c", |
| 254 | help='do everything except actually upload the CL') | 262 | "--current-branch", |
| 255 | p.add_option('-y', '--yes', | 263 | dest="current_branch", |
| 256 | default=False, action='store_true', | 264 | action="store_true", |
| 257 | help='answer yes to all safe prompts') | 265 | help="upload current git branch", |
| 258 | p.add_option('--ignore-untracked-files', | 266 | ) |
| 259 | action='store_true', default=False, | 267 | p.add_option( |
| 260 | help='ignore untracked files in the working copy') | 268 | "--no-current-branch", |
| 261 | p.add_option('--no-ignore-untracked-files', | 269 | dest="current_branch", |
| 262 | dest='ignore_untracked_files', action='store_false', | 270 | action="store_false", |
| 263 | help='always ask about untracked files in the working copy') | 271 | help="upload all git branches", |
| 264 | p.add_option('--no-cert-checks', | 272 | ) |
| 265 | dest='validate_certs', action='store_false', default=True, | 273 | # Turn this into a warning & remove this someday. |
| 266 | help='disable verifying ssl certs (unsafe)') | 274 | p.add_option( |
| 267 | RepoHook.AddOptionGroup(p, 'pre-upload') | 275 | "--cbr", |
| 268 | 276 | dest="current_branch", | |
| 269 | def _SingleBranch(self, opt, branch, people): | 277 | action="store_true", |
| 270 | project = branch.project | 278 | help=optparse.SUPPRESS_HELP, |
| 271 | name = branch.name | 279 | ) |
| 272 | remote = project.GetBranch(name).remote | 280 | p.add_option( |
| 273 | 281 | "--ne", | |
| 274 | key = 'review.%s.autoupload' % remote.review | 282 | "--no-emails", |
| 275 | answer = project.config.GetBoolean(key) | 283 | action="store_false", |
| 276 | 284 | dest="notify", | |
| 277 | if answer is False: | 285 | default=True, |
| 278 | _die("upload blocked by %s = false" % key) | 286 | help="do not send e-mails on upload", |
| 279 | 287 | ) | |
| 280 | if answer is None: | 288 | p.add_option( |
| 281 | date = branch.date | 289 | "-p", |
| 282 | commit_list = branch.commits | 290 | "--private", |
| 283 | 291 | action="store_true", | |
| 284 | destination = opt.dest_branch or project.dest_branch or project.revisionExpr | 292 | dest="private", |
| 285 | print('Upload project %s/ to remote branch %s%s:' % | 293 | default=False, |
| 286 | (project.RelPath(local=opt.this_manifest_only), destination, | 294 | help="upload as a private change (deprecated; use --wip)", |
| 287 | ' (private)' if opt.private else '')) | 295 | ) |
| 288 | print(' branch %s (%2d commit%s, %s):' % ( | 296 | p.add_option( |
| 289 | name, | 297 | "-w", |
| 290 | len(commit_list), | 298 | "--wip", |
| 291 | len(commit_list) != 1 and 's' or '', | 299 | action="store_true", |
| 292 | date)) | 300 | dest="wip", |
| 293 | for commit in commit_list: | 301 | default=False, |
| 294 | print(' %s' % commit) | 302 | help="upload as a work-in-progress change", |
| 295 | 303 | ) | |
| 296 | print('to %s (y/N)? ' % remote.review, end='', flush=True) | 304 | p.add_option( |
| 297 | if opt.yes: | 305 | "-r", |
| 298 | print('<--yes>') | 306 | "--ready", |
| 299 | answer = True | 307 | action="store_true", |
| 300 | else: | 308 | default=False, |
| 301 | answer = sys.stdin.readline().strip().lower() | 309 | help="mark change as ready (clears work-in-progress setting)", |
| 302 | answer = answer in ('y', 'yes', '1', 'true', 't') | 310 | ) |
| 303 | if not answer: | 311 | p.add_option( |
| 304 | _die("upload aborted by user") | 312 | "-o", |
| 305 | 313 | "--push-option", | |
| 306 | # Perform some basic safety checks prior to uploading. | 314 | type="string", |
| 307 | if not opt.yes and not _VerifyPendingCommits([branch]): | 315 | action="append", |
| 308 | _die("upload aborted by user") | 316 | dest="push_options", |
| 309 | 317 | default=[], | |
| 310 | self._UploadAndReport(opt, [branch], people) | 318 | help="additional push options to transmit", |
| 311 | 319 | ) | |
| 312 | def _MultipleBranches(self, opt, pending, people): | 320 | p.add_option( |
| 313 | projects = {} | 321 | "-D", |
| 314 | branches = {} | 322 | "--destination", |
| 315 | 323 | "--dest", | |
| 316 | script = [] | 324 | type="string", |
| 317 | script.append('# Uncomment the branches to upload:') | 325 | action="store", |
| 318 | for project, avail in pending: | 326 | dest="dest_branch", |
| 319 | project_path = project.RelPath(local=opt.this_manifest_only) | 327 | metavar="BRANCH", |
| 320 | script.append('#') | 328 | help="submit for review on this target branch", |
| 321 | script.append(f'# project {project_path}/:') | 329 | ) |
| 322 | 330 | p.add_option( | |
| 323 | b = {} | 331 | "-n", |
| 324 | for branch in avail: | 332 | "--dry-run", |
| 325 | if branch is None: | 333 | dest="dryrun", |
| 326 | continue | 334 | default=False, |
| 335 | action="store_true", | ||
| 336 | help="do everything except actually upload the CL", | ||
| 337 | ) | ||
| 338 | p.add_option( | ||
| 339 | "-y", | ||
| 340 | "--yes", | ||
| 341 | default=False, | ||
| 342 | action="store_true", | ||
| 343 | help="answer yes to all safe prompts", | ||
| 344 | ) | ||
| 345 | p.add_option( | ||
| 346 | "--ignore-untracked-files", | ||
| 347 | action="store_true", | ||
| 348 | default=False, | ||
| 349 | help="ignore untracked files in the working copy", | ||
| 350 | ) | ||
| 351 | p.add_option( | ||
| 352 | "--no-ignore-untracked-files", | ||
| 353 | dest="ignore_untracked_files", | ||
| 354 | action="store_false", | ||
| 355 | help="always ask about untracked files in the working copy", | ||
| 356 | ) | ||
| 357 | p.add_option( | ||
| 358 | "--no-cert-checks", | ||
| 359 | dest="validate_certs", | ||
| 360 | action="store_false", | ||
| 361 | default=True, | ||
| 362 | help="disable verifying ssl certs (unsafe)", | ||
| 363 | ) | ||
| 364 | RepoHook.AddOptionGroup(p, "pre-upload") | ||
| 365 | |||
| 366 | def _SingleBranch(self, opt, branch, people): | ||
| 367 | project = branch.project | ||
| 327 | name = branch.name | 368 | name = branch.name |
| 328 | date = branch.date | 369 | remote = project.GetBranch(name).remote |
| 329 | commit_list = branch.commits | 370 | |
| 330 | 371 | key = "review.%s.autoupload" % remote.review | |
| 331 | if b: | 372 | answer = project.config.GetBoolean(key) |
| 332 | script.append('#') | 373 | |
| 333 | destination = opt.dest_branch or project.dest_branch or project.revisionExpr | 374 | if answer is False: |
| 334 | script.append('# branch %s (%2d commit%s, %s) to remote branch %s:' % ( | 375 | _die("upload blocked by %s = false" % key) |
| 335 | name, | 376 | |
| 336 | len(commit_list), | 377 | if answer is None: |
| 337 | len(commit_list) != 1 and 's' or '', | 378 | date = branch.date |
| 338 | date, | 379 | commit_list = branch.commits |
| 339 | destination)) | 380 | |
| 340 | for commit in commit_list: | 381 | destination = ( |
| 341 | script.append('# %s' % commit) | 382 | opt.dest_branch or project.dest_branch or project.revisionExpr |
| 342 | b[name] = branch | 383 | ) |
| 343 | 384 | print( | |
| 344 | projects[project_path] = project | 385 | "Upload project %s/ to remote branch %s%s:" |
| 345 | branches[project_path] = b | 386 | % ( |
| 346 | script.append('') | 387 | project.RelPath(local=opt.this_manifest_only), |
| 347 | 388 | destination, | |
| 348 | script = Editor.EditString("\n".join(script)).split("\n") | 389 | " (private)" if opt.private else "", |
| 349 | 390 | ) | |
| 350 | project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$') | 391 | ) |
| 351 | branch_re = re.compile(r'^\s*branch\s*([^\s(]+)\s*\(.*') | 392 | print( |
| 352 | 393 | " branch %s (%2d commit%s, %s):" | |
| 353 | project = None | 394 | % ( |
| 354 | todo = [] | 395 | name, |
| 355 | 396 | len(commit_list), | |
| 356 | for line in script: | 397 | len(commit_list) != 1 and "s" or "", |
| 357 | m = project_re.match(line) | 398 | date, |
| 358 | if m: | 399 | ) |
| 359 | name = m.group(1) | 400 | ) |
| 360 | project = projects.get(name) | 401 | for commit in commit_list: |
| 361 | if not project: | 402 | print(" %s" % commit) |
| 362 | _die('project %s not available for upload', name) | 403 | |
| 363 | continue | 404 | print("to %s (y/N)? " % remote.review, end="", flush=True) |
| 364 | |||
| 365 | m = branch_re.match(line) | ||
| 366 | if m: | ||
| 367 | name = m.group(1) | ||
| 368 | if not project: | ||
| 369 | _die('project for branch %s not in script', name) | ||
| 370 | project_path = project.RelPath(local=opt.this_manifest_only) | ||
| 371 | branch = branches[project_path].get(name) | ||
| 372 | if not branch: | ||
| 373 | _die('branch %s not in %s', name, project_path) | ||
| 374 | todo.append(branch) | ||
| 375 | if not todo: | ||
| 376 | _die("nothing uncommented for upload") | ||
| 377 | |||
| 378 | # Perform some basic safety checks prior to uploading. | ||
| 379 | if not opt.yes and not _VerifyPendingCommits(todo): | ||
| 380 | _die("upload aborted by user") | ||
| 381 | |||
| 382 | self._UploadAndReport(opt, todo, people) | ||
| 383 | |||
| 384 | def _AppendAutoList(self, branch, people): | ||
| 385 | """ | ||
| 386 | Appends the list of reviewers in the git project's config. | ||
| 387 | Appends the list of users in the CC list in the git project's config if a | ||
| 388 | non-empty reviewer list was found. | ||
| 389 | """ | ||
| 390 | name = branch.name | ||
| 391 | project = branch.project | ||
| 392 | |||
| 393 | key = 'review.%s.autoreviewer' % project.GetBranch(name).remote.review | ||
| 394 | raw_list = project.config.GetString(key) | ||
| 395 | if raw_list is not None: | ||
| 396 | people[0].extend([entry.strip() for entry in raw_list.split(',')]) | ||
| 397 | |||
| 398 | key = 'review.%s.autocopy' % project.GetBranch(name).remote.review | ||
| 399 | raw_list = project.config.GetString(key) | ||
| 400 | if raw_list is not None and len(people[0]) > 0: | ||
| 401 | people[1].extend([entry.strip() for entry in raw_list.split(',')]) | ||
| 402 | |||
| 403 | def _FindGerritChange(self, branch): | ||
| 404 | last_pub = branch.project.WasPublished(branch.name) | ||
| 405 | if last_pub is None: | ||
| 406 | return "" | ||
| 407 | |||
| 408 | refs = branch.GetPublishedRefs() | ||
| 409 | try: | ||
| 410 | # refs/changes/XYZ/N --> XYZ | ||
| 411 | return refs.get(last_pub).split('/')[-2] | ||
| 412 | except (AttributeError, IndexError): | ||
| 413 | return "" | ||
| 414 | |||
| 415 | def _UploadAndReport(self, opt, todo, original_people): | ||
| 416 | have_errors = False | ||
| 417 | for branch in todo: | ||
| 418 | try: | ||
| 419 | people = copy.deepcopy(original_people) | ||
| 420 | self._AppendAutoList(branch, people) | ||
| 421 | |||
| 422 | # Check if there are local changes that may have been forgotten | ||
| 423 | changes = branch.project.UncommitedFiles() | ||
| 424 | if opt.ignore_untracked_files: | ||
| 425 | untracked = set(branch.project.UntrackedFiles()) | ||
| 426 | changes = [x for x in changes if x not in untracked] | ||
| 427 | |||
| 428 | if changes: | ||
| 429 | key = 'review.%s.autoupload' % branch.project.remote.review | ||
| 430 | answer = branch.project.config.GetBoolean(key) | ||
| 431 | |||
| 432 | # if they want to auto upload, let's not ask because it could be automated | ||
| 433 | if answer is None: | ||
| 434 | print() | ||
| 435 | print('Uncommitted changes in %s (did you forget to amend?):' | ||
| 436 | % branch.project.name) | ||
| 437 | print('\n'.join(changes)) | ||
| 438 | print('Continue uploading? (y/N) ', end='', flush=True) | ||
| 439 | if opt.yes: | 405 | if opt.yes: |
| 440 | print('<--yes>') | 406 | print("<--yes>") |
| 441 | a = 'yes' | 407 | answer = True |
| 408 | else: | ||
| 409 | answer = sys.stdin.readline().strip().lower() | ||
| 410 | answer = answer in ("y", "yes", "1", "true", "t") | ||
| 411 | if not answer: | ||
| 412 | _die("upload aborted by user") | ||
| 413 | |||
| 414 | # Perform some basic safety checks prior to uploading. | ||
| 415 | if not opt.yes and not _VerifyPendingCommits([branch]): | ||
| 416 | _die("upload aborted by user") | ||
| 417 | |||
| 418 | self._UploadAndReport(opt, [branch], people) | ||
| 419 | |||
| 420 | def _MultipleBranches(self, opt, pending, people): | ||
| 421 | projects = {} | ||
| 422 | branches = {} | ||
| 423 | |||
| 424 | script = [] | ||
| 425 | script.append("# Uncomment the branches to upload:") | ||
| 426 | for project, avail in pending: | ||
| 427 | project_path = project.RelPath(local=opt.this_manifest_only) | ||
| 428 | script.append("#") | ||
| 429 | script.append(f"# project {project_path}/:") | ||
| 430 | |||
| 431 | b = {} | ||
| 432 | for branch in avail: | ||
| 433 | if branch is None: | ||
| 434 | continue | ||
| 435 | name = branch.name | ||
| 436 | date = branch.date | ||
| 437 | commit_list = branch.commits | ||
| 438 | |||
| 439 | if b: | ||
| 440 | script.append("#") | ||
| 441 | destination = ( | ||
| 442 | opt.dest_branch | ||
| 443 | or project.dest_branch | ||
| 444 | or project.revisionExpr | ||
| 445 | ) | ||
| 446 | script.append( | ||
| 447 | "# branch %s (%2d commit%s, %s) to remote branch %s:" | ||
| 448 | % ( | ||
| 449 | name, | ||
| 450 | len(commit_list), | ||
| 451 | len(commit_list) != 1 and "s" or "", | ||
| 452 | date, | ||
| 453 | destination, | ||
| 454 | ) | ||
| 455 | ) | ||
| 456 | for commit in commit_list: | ||
| 457 | script.append("# %s" % commit) | ||
| 458 | b[name] = branch | ||
| 459 | |||
| 460 | projects[project_path] = project | ||
| 461 | branches[project_path] = b | ||
| 462 | script.append("") | ||
| 463 | |||
| 464 | script = Editor.EditString("\n".join(script)).split("\n") | ||
| 465 | |||
| 466 | project_re = re.compile(r"^#?\s*project\s*([^\s]+)/:$") | ||
| 467 | branch_re = re.compile(r"^\s*branch\s*([^\s(]+)\s*\(.*") | ||
| 468 | |||
| 469 | project = None | ||
| 470 | todo = [] | ||
| 471 | |||
| 472 | for line in script: | ||
| 473 | m = project_re.match(line) | ||
| 474 | if m: | ||
| 475 | name = m.group(1) | ||
| 476 | project = projects.get(name) | ||
| 477 | if not project: | ||
| 478 | _die("project %s not available for upload", name) | ||
| 479 | continue | ||
| 480 | |||
| 481 | m = branch_re.match(line) | ||
| 482 | if m: | ||
| 483 | name = m.group(1) | ||
| 484 | if not project: | ||
| 485 | _die("project for branch %s not in script", name) | ||
| 486 | project_path = project.RelPath(local=opt.this_manifest_only) | ||
| 487 | branch = branches[project_path].get(name) | ||
| 488 | if not branch: | ||
| 489 | _die("branch %s not in %s", name, project_path) | ||
| 490 | todo.append(branch) | ||
| 491 | if not todo: | ||
| 492 | _die("nothing uncommented for upload") | ||
| 493 | |||
| 494 | # Perform some basic safety checks prior to uploading. | ||
| 495 | if not opt.yes and not _VerifyPendingCommits(todo): | ||
| 496 | _die("upload aborted by user") | ||
| 497 | |||
| 498 | self._UploadAndReport(opt, todo, people) | ||
| 499 | |||
| 500 | def _AppendAutoList(self, branch, people): | ||
| 501 | """ | ||
| 502 | Appends the list of reviewers in the git project's config. | ||
| 503 | Appends the list of users in the CC list in the git project's config if | ||
| 504 | a non-empty reviewer list was found. | ||
| 505 | """ | ||
| 506 | name = branch.name | ||
| 507 | project = branch.project | ||
| 508 | |||
| 509 | key = "review.%s.autoreviewer" % project.GetBranch(name).remote.review | ||
| 510 | raw_list = project.config.GetString(key) | ||
| 511 | if raw_list is not None: | ||
| 512 | people[0].extend([entry.strip() for entry in raw_list.split(",")]) | ||
| 513 | |||
| 514 | key = "review.%s.autocopy" % project.GetBranch(name).remote.review | ||
| 515 | raw_list = project.config.GetString(key) | ||
| 516 | if raw_list is not None and len(people[0]) > 0: | ||
| 517 | people[1].extend([entry.strip() for entry in raw_list.split(",")]) | ||
| 518 | |||
| 519 | def _FindGerritChange(self, branch): | ||
| 520 | last_pub = branch.project.WasPublished(branch.name) | ||
| 521 | if last_pub is None: | ||
| 522 | return "" | ||
| 523 | |||
| 524 | refs = branch.GetPublishedRefs() | ||
| 525 | try: | ||
| 526 | # refs/changes/XYZ/N --> XYZ | ||
| 527 | return refs.get(last_pub).split("/")[-2] | ||
| 528 | except (AttributeError, IndexError): | ||
| 529 | return "" | ||
| 530 | |||
| 531 | def _UploadAndReport(self, opt, todo, original_people): | ||
| 532 | have_errors = False | ||
| 533 | for branch in todo: | ||
| 534 | try: | ||
| 535 | people = copy.deepcopy(original_people) | ||
| 536 | self._AppendAutoList(branch, people) | ||
| 537 | |||
| 538 | # Check if there are local changes that may have been forgotten. | ||
| 539 | changes = branch.project.UncommitedFiles() | ||
| 540 | if opt.ignore_untracked_files: | ||
| 541 | untracked = set(branch.project.UntrackedFiles()) | ||
| 542 | changes = [x for x in changes if x not in untracked] | ||
| 543 | |||
| 544 | if changes: | ||
| 545 | key = "review.%s.autoupload" % branch.project.remote.review | ||
| 546 | answer = branch.project.config.GetBoolean(key) | ||
| 547 | |||
| 548 | # If they want to auto upload, let's not ask because it | ||
| 549 | # could be automated. | ||
| 550 | if answer is None: | ||
| 551 | print() | ||
| 552 | print( | ||
| 553 | "Uncommitted changes in %s (did you forget to " | ||
| 554 | "amend?):" % branch.project.name | ||
| 555 | ) | ||
| 556 | print("\n".join(changes)) | ||
| 557 | print("Continue uploading? (y/N) ", end="", flush=True) | ||
| 558 | if opt.yes: | ||
| 559 | print("<--yes>") | ||
| 560 | a = "yes" | ||
| 561 | else: | ||
| 562 | a = sys.stdin.readline().strip().lower() | ||
| 563 | if a not in ("y", "yes", "t", "true", "on"): | ||
| 564 | print("skipping upload", file=sys.stderr) | ||
| 565 | branch.uploaded = False | ||
| 566 | branch.error = "User aborted" | ||
| 567 | continue | ||
| 568 | |||
| 569 | # Check if topic branches should be sent to the server during | ||
| 570 | # upload. | ||
| 571 | if opt.auto_topic is not True: | ||
| 572 | key = "review.%s.uploadtopic" % branch.project.remote.review | ||
| 573 | opt.auto_topic = branch.project.config.GetBoolean(key) | ||
| 574 | |||
| 575 | def _ExpandCommaList(value): | ||
| 576 | """Split |value| up into comma delimited entries.""" | ||
| 577 | if not value: | ||
| 578 | return | ||
| 579 | for ret in value.split(","): | ||
| 580 | ret = ret.strip() | ||
| 581 | if ret: | ||
| 582 | yield ret | ||
| 583 | |||
| 584 | # Check if hashtags should be included. | ||
| 585 | key = "review.%s.uploadhashtags" % branch.project.remote.review | ||
| 586 | hashtags = set( | ||
| 587 | _ExpandCommaList(branch.project.config.GetString(key)) | ||
| 588 | ) | ||
| 589 | for tag in opt.hashtags: | ||
| 590 | hashtags.update(_ExpandCommaList(tag)) | ||
| 591 | if opt.hashtag_branch: | ||
| 592 | hashtags.add(branch.name) | ||
| 593 | |||
| 594 | # Check if labels should be included. | ||
| 595 | key = "review.%s.uploadlabels" % branch.project.remote.review | ||
| 596 | labels = set( | ||
| 597 | _ExpandCommaList(branch.project.config.GetString(key)) | ||
| 598 | ) | ||
| 599 | for label in opt.labels: | ||
| 600 | labels.update(_ExpandCommaList(label)) | ||
| 601 | |||
| 602 | # Handle e-mail notifications. | ||
| 603 | if opt.notify is False: | ||
| 604 | notify = "NONE" | ||
| 605 | else: | ||
| 606 | key = ( | ||
| 607 | "review.%s.uploadnotify" % branch.project.remote.review | ||
| 608 | ) | ||
| 609 | notify = branch.project.config.GetString(key) | ||
| 610 | |||
| 611 | destination = opt.dest_branch or branch.project.dest_branch | ||
| 612 | |||
| 613 | if branch.project.dest_branch and not opt.dest_branch: | ||
| 614 | merge_branch = self._GetMergeBranch( | ||
| 615 | branch.project, local_branch=branch.name | ||
| 616 | ) | ||
| 617 | |||
| 618 | full_dest = destination | ||
| 619 | if not full_dest.startswith(R_HEADS): | ||
| 620 | full_dest = R_HEADS + full_dest | ||
| 621 | |||
| 622 | # If the merge branch of the local branch is different from | ||
| 623 | # the project's revision AND destination, this might not be | ||
| 624 | # intentional. | ||
| 625 | if ( | ||
| 626 | merge_branch | ||
| 627 | and merge_branch != branch.project.revisionExpr | ||
| 628 | and merge_branch != full_dest | ||
| 629 | ): | ||
| 630 | print( | ||
| 631 | f"For local branch {branch.name}: merge branch " | ||
| 632 | f"{merge_branch} does not match destination branch " | ||
| 633 | f"{destination}" | ||
| 634 | ) | ||
| 635 | print("skipping upload.") | ||
| 636 | print( | ||
| 637 | f"Please use `--destination {destination}` if this " | ||
| 638 | "is intentional" | ||
| 639 | ) | ||
| 640 | branch.uploaded = False | ||
| 641 | continue | ||
| 642 | |||
| 643 | branch.UploadForReview( | ||
| 644 | people, | ||
| 645 | dryrun=opt.dryrun, | ||
| 646 | auto_topic=opt.auto_topic, | ||
| 647 | hashtags=hashtags, | ||
| 648 | labels=labels, | ||
| 649 | private=opt.private, | ||
| 650 | notify=notify, | ||
| 651 | wip=opt.wip, | ||
| 652 | ready=opt.ready, | ||
| 653 | dest_branch=destination, | ||
| 654 | validate_certs=opt.validate_certs, | ||
| 655 | push_options=opt.push_options, | ||
| 656 | ) | ||
| 657 | |||
| 658 | branch.uploaded = True | ||
| 659 | except UploadError as e: | ||
| 660 | branch.error = e | ||
| 661 | branch.uploaded = False | ||
| 662 | have_errors = True | ||
| 663 | |||
| 664 | print(file=sys.stderr) | ||
| 665 | print("-" * 70, file=sys.stderr) | ||
| 666 | |||
| 667 | if have_errors: | ||
| 668 | for branch in todo: | ||
| 669 | if not branch.uploaded: | ||
| 670 | if len(str(branch.error)) <= 30: | ||
| 671 | fmt = " (%s)" | ||
| 672 | else: | ||
| 673 | fmt = "\n (%s)" | ||
| 674 | print( | ||
| 675 | ("[FAILED] %-15s %-15s" + fmt) | ||
| 676 | % ( | ||
| 677 | branch.project.RelPath(local=opt.this_manifest_only) | ||
| 678 | + "/", | ||
| 679 | branch.name, | ||
| 680 | str(branch.error), | ||
| 681 | ), | ||
| 682 | file=sys.stderr, | ||
| 683 | ) | ||
| 684 | print() | ||
| 685 | |||
| 686 | for branch in todo: | ||
| 687 | if branch.uploaded: | ||
| 688 | print( | ||
| 689 | "[OK ] %-15s %s" | ||
| 690 | % ( | ||
| 691 | branch.project.RelPath(local=opt.this_manifest_only) | ||
| 692 | + "/", | ||
| 693 | branch.name, | ||
| 694 | ), | ||
| 695 | file=sys.stderr, | ||
| 696 | ) | ||
| 697 | |||
| 698 | if have_errors: | ||
| 699 | sys.exit(1) | ||
| 700 | |||
| 701 | def _GetMergeBranch(self, project, local_branch=None): | ||
| 702 | if local_branch is None: | ||
| 703 | p = GitCommand( | ||
| 704 | project, | ||
| 705 | ["rev-parse", "--abbrev-ref", "HEAD"], | ||
| 706 | capture_stdout=True, | ||
| 707 | capture_stderr=True, | ||
| 708 | ) | ||
| 709 | p.Wait() | ||
| 710 | local_branch = p.stdout.strip() | ||
| 711 | p = GitCommand( | ||
| 712 | project, | ||
| 713 | ["config", "--get", "branch.%s.merge" % local_branch], | ||
| 714 | capture_stdout=True, | ||
| 715 | capture_stderr=True, | ||
| 716 | ) | ||
| 717 | p.Wait() | ||
| 718 | merge_branch = p.stdout.strip() | ||
| 719 | return merge_branch | ||
| 720 | |||
| 721 | @staticmethod | ||
| 722 | def _GatherOne(opt, project): | ||
| 723 | """Figure out the upload status for |project|.""" | ||
| 724 | if opt.current_branch: | ||
| 725 | cbr = project.CurrentBranch | ||
| 726 | up_branch = project.GetUploadableBranch(cbr) | ||
| 727 | avail = [up_branch] if up_branch else None | ||
| 728 | else: | ||
| 729 | avail = project.GetUploadableBranches(opt.branch) | ||
| 730 | return (project, avail) | ||
| 731 | |||
| 732 | def Execute(self, opt, args): | ||
| 733 | projects = self.GetProjects( | ||
| 734 | args, all_manifests=not opt.this_manifest_only | ||
| 735 | ) | ||
| 736 | |||
| 737 | def _ProcessResults(_pool, _out, results): | ||
| 738 | pending = [] | ||
| 739 | for result in results: | ||
| 740 | project, avail = result | ||
| 741 | if avail is None: | ||
| 742 | print( | ||
| 743 | 'repo: error: %s: Unable to upload branch "%s". ' | ||
| 744 | "You might be able to fix the branch by running:\n" | ||
| 745 | " git branch --set-upstream-to m/%s" | ||
| 746 | % ( | ||
| 747 | project.RelPath(local=opt.this_manifest_only), | ||
| 748 | project.CurrentBranch, | ||
| 749 | project.manifest.branch, | ||
| 750 | ), | ||
| 751 | file=sys.stderr, | ||
| 752 | ) | ||
| 753 | elif avail: | ||
| 754 | pending.append(result) | ||
| 755 | return pending | ||
| 756 | |||
| 757 | pending = self.ExecuteInParallel( | ||
| 758 | opt.jobs, | ||
| 759 | functools.partial(self._GatherOne, opt), | ||
| 760 | projects, | ||
| 761 | callback=_ProcessResults, | ||
| 762 | ) | ||
| 763 | |||
| 764 | if not pending: | ||
| 765 | if opt.branch is None: | ||
| 766 | print( | ||
| 767 | "repo: error: no branches ready for upload", file=sys.stderr | ||
| 768 | ) | ||
| 442 | else: | 769 | else: |
| 443 | a = sys.stdin.readline().strip().lower() | 770 | print( |
| 444 | if a not in ('y', 'yes', 't', 'true', 'on'): | 771 | 'repo: error: no branches named "%s" ready for upload' |
| 445 | print("skipping upload", file=sys.stderr) | 772 | % (opt.branch,), |
| 446 | branch.uploaded = False | 773 | file=sys.stderr, |
| 447 | branch.error = 'User aborted' | 774 | ) |
| 448 | continue | 775 | return 1 |
| 449 | 776 | ||
| 450 | # Check if topic branches should be sent to the server during upload | 777 | manifests = { |
| 451 | if opt.auto_topic is not True: | 778 | project.manifest.topdir: project.manifest |
| 452 | key = 'review.%s.uploadtopic' % branch.project.remote.review | 779 | for (project, available) in pending |
| 453 | opt.auto_topic = branch.project.config.GetBoolean(key) | 780 | } |
| 454 | 781 | ret = 0 | |
| 455 | def _ExpandCommaList(value): | 782 | for manifest in manifests.values(): |
| 456 | """Split |value| up into comma delimited entries.""" | 783 | pending_proj_names = [ |
| 457 | if not value: | 784 | project.name |
| 458 | return | 785 | for (project, available) in pending |
| 459 | for ret in value.split(','): | 786 | if project.manifest.topdir == manifest.topdir |
| 460 | ret = ret.strip() | 787 | ] |
| 461 | if ret: | 788 | pending_worktrees = [ |
| 462 | yield ret | 789 | project.worktree |
| 463 | 790 | for (project, available) in pending | |
| 464 | # Check if hashtags should be included. | 791 | if project.manifest.topdir == manifest.topdir |
| 465 | key = 'review.%s.uploadhashtags' % branch.project.remote.review | 792 | ] |
| 466 | hashtags = set(_ExpandCommaList(branch.project.config.GetString(key))) | 793 | hook = RepoHook.FromSubcmd( |
| 467 | for tag in opt.hashtags: | 794 | hook_type="pre-upload", |
| 468 | hashtags.update(_ExpandCommaList(tag)) | 795 | manifest=manifest, |
| 469 | if opt.hashtag_branch: | 796 | opt=opt, |
| 470 | hashtags.add(branch.name) | 797 | abort_if_user_denies=True, |
| 471 | 798 | ) | |
| 472 | # Check if labels should be included. | 799 | if not hook.Run( |
| 473 | key = 'review.%s.uploadlabels' % branch.project.remote.review | 800 | project_list=pending_proj_names, worktree_list=pending_worktrees |
| 474 | labels = set(_ExpandCommaList(branch.project.config.GetString(key))) | 801 | ): |
| 475 | for label in opt.labels: | 802 | ret = 1 |
| 476 | labels.update(_ExpandCommaList(label)) | 803 | if ret: |
| 477 | 804 | return ret | |
| 478 | # Handle e-mail notifications. | 805 | |
| 479 | if opt.notify is False: | 806 | reviewers = _SplitEmails(opt.reviewers) if opt.reviewers else [] |
| 480 | notify = 'NONE' | 807 | cc = _SplitEmails(opt.cc) if opt.cc else [] |
| 808 | people = (reviewers, cc) | ||
| 809 | |||
| 810 | if len(pending) == 1 and len(pending[0][1]) == 1: | ||
| 811 | self._SingleBranch(opt, pending[0][1][0], people) | ||
| 481 | else: | 812 | else: |
| 482 | key = 'review.%s.uploadnotify' % branch.project.remote.review | 813 | self._MultipleBranches(opt, pending, people) |
| 483 | notify = branch.project.config.GetString(key) | ||
| 484 | |||
| 485 | destination = opt.dest_branch or branch.project.dest_branch | ||
| 486 | |||
| 487 | if branch.project.dest_branch and not opt.dest_branch: | ||
| 488 | |||
| 489 | merge_branch = self._GetMergeBranch( | ||
| 490 | branch.project, local_branch=branch.name) | ||
| 491 | |||
| 492 | full_dest = destination | ||
| 493 | if not full_dest.startswith(R_HEADS): | ||
| 494 | full_dest = R_HEADS + full_dest | ||
| 495 | |||
| 496 | # If the merge branch of the local branch is different from the | ||
| 497 | # project's revision AND destination, this might not be intentional. | ||
| 498 | if (merge_branch and merge_branch != branch.project.revisionExpr | ||
| 499 | and merge_branch != full_dest): | ||
| 500 | print(f'For local branch {branch.name}: merge branch ' | ||
| 501 | f'{merge_branch} does not match destination branch ' | ||
| 502 | f'{destination}') | ||
| 503 | print('skipping upload.') | ||
| 504 | print(f'Please use `--destination {destination}` if this is intentional') | ||
| 505 | branch.uploaded = False | ||
| 506 | continue | ||
| 507 | |||
| 508 | branch.UploadForReview(people, | ||
| 509 | dryrun=opt.dryrun, | ||
| 510 | auto_topic=opt.auto_topic, | ||
| 511 | hashtags=hashtags, | ||
| 512 | labels=labels, | ||
| 513 | private=opt.private, | ||
| 514 | notify=notify, | ||
| 515 | wip=opt.wip, | ||
| 516 | ready=opt.ready, | ||
| 517 | dest_branch=destination, | ||
| 518 | validate_certs=opt.validate_certs, | ||
| 519 | push_options=opt.push_options) | ||
| 520 | |||
| 521 | branch.uploaded = True | ||
| 522 | except UploadError as e: | ||
| 523 | branch.error = e | ||
| 524 | branch.uploaded = False | ||
| 525 | have_errors = True | ||
| 526 | |||
| 527 | print(file=sys.stderr) | ||
| 528 | print('----------------------------------------------------------------------', file=sys.stderr) | ||
| 529 | |||
| 530 | if have_errors: | ||
| 531 | for branch in todo: | ||
| 532 | if not branch.uploaded: | ||
| 533 | if len(str(branch.error)) <= 30: | ||
| 534 | fmt = ' (%s)' | ||
| 535 | else: | ||
| 536 | fmt = '\n (%s)' | ||
| 537 | print(('[FAILED] %-15s %-15s' + fmt) % ( | ||
| 538 | branch.project.RelPath(local=opt.this_manifest_only) + '/', | ||
| 539 | branch.name, | ||
| 540 | str(branch.error)), | ||
| 541 | file=sys.stderr) | ||
| 542 | print() | ||
| 543 | |||
| 544 | for branch in todo: | ||
| 545 | if branch.uploaded: | ||
| 546 | print('[OK ] %-15s %s' % ( | ||
| 547 | branch.project.RelPath(local=opt.this_manifest_only) + '/', | ||
| 548 | branch.name), | ||
| 549 | file=sys.stderr) | ||
| 550 | |||
| 551 | if have_errors: | ||
| 552 | sys.exit(1) | ||
| 553 | |||
| 554 | def _GetMergeBranch(self, project, local_branch=None): | ||
| 555 | if local_branch is None: | ||
| 556 | p = GitCommand(project, | ||
| 557 | ['rev-parse', '--abbrev-ref', 'HEAD'], | ||
| 558 | capture_stdout=True, | ||
| 559 | capture_stderr=True) | ||
| 560 | p.Wait() | ||
| 561 | local_branch = p.stdout.strip() | ||
| 562 | p = GitCommand(project, | ||
| 563 | ['config', '--get', 'branch.%s.merge' % local_branch], | ||
| 564 | capture_stdout=True, | ||
| 565 | capture_stderr=True) | ||
| 566 | p.Wait() | ||
| 567 | merge_branch = p.stdout.strip() | ||
| 568 | return merge_branch | ||
| 569 | |||
| 570 | @staticmethod | ||
| 571 | def _GatherOne(opt, project): | ||
| 572 | """Figure out the upload status for |project|.""" | ||
| 573 | if opt.current_branch: | ||
| 574 | cbr = project.CurrentBranch | ||
| 575 | up_branch = project.GetUploadableBranch(cbr) | ||
| 576 | avail = [up_branch] if up_branch else None | ||
| 577 | else: | ||
| 578 | avail = project.GetUploadableBranches(opt.branch) | ||
| 579 | return (project, avail) | ||
| 580 | |||
| 581 | def Execute(self, opt, args): | ||
| 582 | projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only) | ||
| 583 | |||
| 584 | def _ProcessResults(_pool, _out, results): | ||
| 585 | pending = [] | ||
| 586 | for result in results: | ||
| 587 | project, avail = result | ||
| 588 | if avail is None: | ||
| 589 | print('repo: error: %s: Unable to upload branch "%s". ' | ||
| 590 | 'You might be able to fix the branch by running:\n' | ||
| 591 | ' git branch --set-upstream-to m/%s' % | ||
| 592 | (project.RelPath(local=opt.this_manifest_only), project.CurrentBranch, | ||
| 593 | project.manifest.branch), | ||
| 594 | file=sys.stderr) | ||
| 595 | elif avail: | ||
| 596 | pending.append(result) | ||
| 597 | return pending | ||
| 598 | |||
| 599 | pending = self.ExecuteInParallel( | ||
| 600 | opt.jobs, | ||
| 601 | functools.partial(self._GatherOne, opt), | ||
| 602 | projects, | ||
| 603 | callback=_ProcessResults) | ||
| 604 | |||
| 605 | if not pending: | ||
| 606 | if opt.branch is None: | ||
| 607 | print('repo: error: no branches ready for upload', file=sys.stderr) | ||
| 608 | else: | ||
| 609 | print('repo: error: no branches named "%s" ready for upload' % | ||
| 610 | (opt.branch,), file=sys.stderr) | ||
| 611 | return 1 | ||
| 612 | |||
| 613 | manifests = {project.manifest.topdir: project.manifest | ||
| 614 | for (project, available) in pending} | ||
| 615 | ret = 0 | ||
| 616 | for manifest in manifests.values(): | ||
| 617 | pending_proj_names = [project.name for (project, available) in pending | ||
| 618 | if project.manifest.topdir == manifest.topdir] | ||
| 619 | pending_worktrees = [project.worktree for (project, available) in pending | ||
| 620 | if project.manifest.topdir == manifest.topdir] | ||
| 621 | hook = RepoHook.FromSubcmd( | ||
| 622 | hook_type='pre-upload', manifest=manifest, | ||
| 623 | opt=opt, abort_if_user_denies=True) | ||
| 624 | if not hook.Run(project_list=pending_proj_names, | ||
| 625 | worktree_list=pending_worktrees): | ||
| 626 | ret = 1 | ||
| 627 | if ret: | ||
| 628 | return ret | ||
| 629 | |||
| 630 | reviewers = _SplitEmails(opt.reviewers) if opt.reviewers else [] | ||
| 631 | cc = _SplitEmails(opt.cc) if opt.cc else [] | ||
| 632 | people = (reviewers, cc) | ||
| 633 | |||
| 634 | if len(pending) == 1 and len(pending[0][1]) == 1: | ||
| 635 | self._SingleBranch(opt, pending[0][1][0], people) | ||
| 636 | else: | ||
| 637 | self._MultipleBranches(opt, pending, people) | ||
