diff options
author | Gavin Mak <gavinmak@google.com> | 2023-03-11 06:46:20 +0000 |
---|---|---|
committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2023-03-22 17:46:28 +0000 |
commit | ea2e330e43c182dc16b0111ebc69ee5a71ee4ce1 (patch) | |
tree | dc33ba0e56825b3e007d0589891756724725a465 /subcmds/upload.py | |
parent | 1604cf255f8c1786a23388db6d5277ac7949a24a (diff) | |
download | git-repo-ea2e330e43c182dc16b0111ebc69ee5a71ee4ce1.tar.gz |
Format codebase with black and check formatting in CQ
Apply rules set by https://gerrit-review.googlesource.com/c/git-repo/+/362954/ across the codebase and fix any lingering errors caught
by flake8. Also check black formatting in run_tests (and CQ).
Bug: b/267675342
Change-Id: I972d77649dac351150dcfeb1cd1ad0ea2efc1956
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/363474
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
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) | ||