diff options
-rw-r--r-- | project.py | 10 | ||||
-rw-r--r-- | subcmds/upload.py | 250 | ||||
-rw-r--r-- | tests/test_subcmds_upload.py | 69 |
3 files changed, 200 insertions, 129 deletions
@@ -1116,7 +1116,8 @@ class Project(object): | |||
1116 | if not re.match(r"^.+[+-][0-9]+$", label): | 1116 | if not re.match(r"^.+[+-][0-9]+$", label): |
1117 | raise UploadError( | 1117 | raise UploadError( |
1118 | f'invalid label syntax "{label}": labels use forms like ' | 1118 | f'invalid label syntax "{label}": labels use forms like ' |
1119 | "CodeReview+1 or Verified-1" | 1119 | "CodeReview+1 or Verified-1", |
1120 | project=self.name, | ||
1120 | ) | 1121 | ) |
1121 | 1122 | ||
1122 | if dest_branch is None: | 1123 | if dest_branch is None: |
@@ -1132,7 +1133,7 @@ class Project(object): | |||
1132 | 1133 | ||
1133 | url = branch.remote.ReviewUrl(self.UserEmail, validate_certs) | 1134 | url = branch.remote.ReviewUrl(self.UserEmail, validate_certs) |
1134 | if url is None: | 1135 | if url is None: |
1135 | raise UploadError("review not configured") | 1136 | raise UploadError("review not configured", project=self.name) |
1136 | cmd = ["push"] | 1137 | cmd = ["push"] |
1137 | if dryrun: | 1138 | if dryrun: |
1138 | cmd.append("-n") | 1139 | cmd.append("-n") |
@@ -1177,8 +1178,9 @@ class Project(object): | |||
1177 | ref_spec = ref_spec + "%" + ",".join(opts) | 1178 | ref_spec = ref_spec + "%" + ",".join(opts) |
1178 | cmd.append(ref_spec) | 1179 | cmd.append(ref_spec) |
1179 | 1180 | ||
1180 | if GitCommand(self, cmd, bare=True).Wait() != 0: | 1181 | GitCommand( |
1181 | raise UploadError("Upload failed") | 1182 | self, cmd, bare=True, capture_stderr=True, verify_command=True |
1183 | ).Wait() | ||
1182 | 1184 | ||
1183 | if not dryrun: | 1185 | if not dryrun: |
1184 | msg = "posted to %s for %s" % (branch.remote.review, dest_branch) | 1186 | msg = "posted to %s for %s" % (branch.remote.review, dest_branch) |
diff --git a/subcmds/upload.py b/subcmds/upload.py index d0c028b9..040eaeb5 100644 --- a/subcmds/upload.py +++ b/subcmds/upload.py | |||
@@ -21,7 +21,7 @@ from typing import List | |||
21 | 21 | ||
22 | from command import DEFAULT_LOCAL_JOBS, InteractiveCommand | 22 | from command import DEFAULT_LOCAL_JOBS, InteractiveCommand |
23 | from editor import Editor | 23 | from editor import Editor |
24 | from error import UploadError, RepoExitError | 24 | from error import UploadError, SilentRepoExitError, GitError |
25 | from git_command import GitCommand | 25 | from git_command import GitCommand |
26 | from git_refs import R_HEADS | 26 | from git_refs import R_HEADS |
27 | from hooks import RepoHook | 27 | from hooks import RepoHook |
@@ -31,7 +31,7 @@ from project import ReviewableBranch | |||
31 | _DEFAULT_UNUSUAL_COMMIT_THRESHOLD = 5 | 31 | _DEFAULT_UNUSUAL_COMMIT_THRESHOLD = 5 |
32 | 32 | ||
33 | 33 | ||
34 | class UploadExitError(RepoExitError): | 34 | class UploadExitError(SilentRepoExitError): |
35 | """Indicates that there is an upload command error requiring a sys exit.""" | 35 | """Indicates that there is an upload command error requiring a sys exit.""" |
36 | 36 | ||
37 | 37 | ||
@@ -532,137 +532,137 @@ Gerrit Code Review: https://www.gerritcodereview.com/ | |||
532 | except (AttributeError, IndexError): | 532 | except (AttributeError, IndexError): |
533 | return "" | 533 | return "" |
534 | 534 | ||
535 | def _UploadAndReport(self, opt, todo, original_people): | 535 | def _UploadBranch(self, opt, branch, original_people): |
536 | have_errors = False | 536 | """Upload Branch.""" |
537 | for branch in todo: | 537 | people = copy.deepcopy(original_people) |
538 | try: | 538 | self._AppendAutoList(branch, people) |
539 | people = copy.deepcopy(original_people) | 539 | |
540 | self._AppendAutoList(branch, people) | 540 | # Check if there are local changes that may have been forgotten. |
541 | 541 | changes = branch.project.UncommitedFiles() | |
542 | # Check if there are local changes that may have been forgotten. | 542 | if opt.ignore_untracked_files: |
543 | changes = branch.project.UncommitedFiles() | 543 | untracked = set(branch.project.UntrackedFiles()) |
544 | if opt.ignore_untracked_files: | 544 | changes = [x for x in changes if x not in untracked] |
545 | untracked = set(branch.project.UntrackedFiles()) | 545 | |
546 | changes = [x for x in changes if x not in untracked] | 546 | if changes: |
547 | 547 | key = "review.%s.autoupload" % branch.project.remote.review | |
548 | if changes: | 548 | answer = branch.project.config.GetBoolean(key) |
549 | key = "review.%s.autoupload" % branch.project.remote.review | 549 | |
550 | answer = branch.project.config.GetBoolean(key) | 550 | # If they want to auto upload, let's not ask because it |
551 | 551 | # could be automated. | |
552 | # If they want to auto upload, let's not ask because it | 552 | if answer is None: |
553 | # could be automated. | 553 | print() |
554 | if answer is None: | 554 | print( |
555 | print() | 555 | "Uncommitted changes in %s (did you forget to " |
556 | print( | 556 | "amend?):" % branch.project.name |
557 | "Uncommitted changes in %s (did you forget to " | ||
558 | "amend?):" % branch.project.name | ||
559 | ) | ||
560 | print("\n".join(changes)) | ||
561 | print("Continue uploading? (y/N) ", end="", flush=True) | ||
562 | if opt.yes: | ||
563 | print("<--yes>") | ||
564 | a = "yes" | ||
565 | else: | ||
566 | a = sys.stdin.readline().strip().lower() | ||
567 | if a not in ("y", "yes", "t", "true", "on"): | ||
568 | print("skipping upload", file=sys.stderr) | ||
569 | branch.uploaded = False | ||
570 | branch.error = "User aborted" | ||
571 | continue | ||
572 | |||
573 | # Check if topic branches should be sent to the server during | ||
574 | # upload. | ||
575 | if opt.auto_topic is not True: | ||
576 | key = "review.%s.uploadtopic" % branch.project.remote.review | ||
577 | opt.auto_topic = branch.project.config.GetBoolean(key) | ||
578 | |||
579 | def _ExpandCommaList(value): | ||
580 | """Split |value| up into comma delimited entries.""" | ||
581 | if not value: | ||
582 | return | ||
583 | for ret in value.split(","): | ||
584 | ret = ret.strip() | ||
585 | if ret: | ||
586 | yield ret | ||
587 | |||
588 | # Check if hashtags should be included. | ||
589 | key = "review.%s.uploadhashtags" % branch.project.remote.review | ||
590 | hashtags = set( | ||
591 | _ExpandCommaList(branch.project.config.GetString(key)) | ||
592 | ) | ||
593 | for tag in opt.hashtags: | ||
594 | hashtags.update(_ExpandCommaList(tag)) | ||
595 | if opt.hashtag_branch: | ||
596 | hashtags.add(branch.name) | ||
597 | |||
598 | # Check if labels should be included. | ||
599 | key = "review.%s.uploadlabels" % branch.project.remote.review | ||
600 | labels = set( | ||
601 | _ExpandCommaList(branch.project.config.GetString(key)) | ||
602 | ) | 557 | ) |
603 | for label in opt.labels: | 558 | print("\n".join(changes)) |
604 | labels.update(_ExpandCommaList(label)) | 559 | print("Continue uploading? (y/N) ", end="", flush=True) |
605 | 560 | if opt.yes: | |
606 | # Handle e-mail notifications. | 561 | print("<--yes>") |
607 | if opt.notify is False: | 562 | a = "yes" |
608 | notify = "NONE" | ||
609 | else: | 563 | else: |
610 | key = ( | 564 | a = sys.stdin.readline().strip().lower() |
611 | "review.%s.uploadnotify" % branch.project.remote.review | 565 | if a not in ("y", "yes", "t", "true", "on"): |
612 | ) | 566 | print("skipping upload", file=sys.stderr) |
613 | notify = branch.project.config.GetString(key) | 567 | branch.uploaded = False |
568 | branch.error = "User aborted" | ||
569 | return | ||
570 | |||
571 | # Check if topic branches should be sent to the server during | ||
572 | # upload. | ||
573 | if opt.auto_topic is not True: | ||
574 | key = "review.%s.uploadtopic" % branch.project.remote.review | ||
575 | opt.auto_topic = branch.project.config.GetBoolean(key) | ||
576 | |||
577 | def _ExpandCommaList(value): | ||
578 | """Split |value| up into comma delimited entries.""" | ||
579 | if not value: | ||
580 | return | ||
581 | for ret in value.split(","): | ||
582 | ret = ret.strip() | ||
583 | if ret: | ||
584 | yield ret | ||
585 | |||
586 | # Check if hashtags should be included. | ||
587 | key = "review.%s.uploadhashtags" % branch.project.remote.review | ||
588 | hashtags = set(_ExpandCommaList(branch.project.config.GetString(key))) | ||
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(_ExpandCommaList(branch.project.config.GetString(key))) | ||
597 | for label in opt.labels: | ||
598 | labels.update(_ExpandCommaList(label)) | ||
599 | |||
600 | # Handle e-mail notifications. | ||
601 | if opt.notify is False: | ||
602 | notify = "NONE" | ||
603 | else: | ||
604 | key = "review.%s.uploadnotify" % branch.project.remote.review | ||
605 | notify = branch.project.config.GetString(key) | ||
614 | 606 | ||
615 | destination = opt.dest_branch or branch.project.dest_branch | 607 | destination = opt.dest_branch or branch.project.dest_branch |
616 | 608 | ||
617 | if branch.project.dest_branch and not opt.dest_branch: | 609 | if branch.project.dest_branch and not opt.dest_branch: |
618 | merge_branch = self._GetMergeBranch( | 610 | merge_branch = self._GetMergeBranch( |
619 | branch.project, local_branch=branch.name | 611 | branch.project, local_branch=branch.name |
620 | ) | 612 | ) |
621 | 613 | ||
622 | full_dest = destination | 614 | full_dest = destination |
623 | if not full_dest.startswith(R_HEADS): | 615 | if not full_dest.startswith(R_HEADS): |
624 | full_dest = R_HEADS + full_dest | 616 | full_dest = R_HEADS + full_dest |
625 | 617 | ||
626 | # If the merge branch of the local branch is different from | 618 | # If the merge branch of the local branch is different from |
627 | # the project's revision AND destination, this might not be | 619 | # the project's revision AND destination, this might not be |
628 | # intentional. | 620 | # intentional. |
629 | if ( | 621 | if ( |
630 | merge_branch | 622 | merge_branch |
631 | and merge_branch != branch.project.revisionExpr | 623 | and merge_branch != branch.project.revisionExpr |
632 | and merge_branch != full_dest | 624 | and merge_branch != full_dest |
633 | ): | 625 | ): |
634 | print( | 626 | print( |
635 | f"For local branch {branch.name}: merge branch " | 627 | f"For local branch {branch.name}: merge branch " |
636 | f"{merge_branch} does not match destination branch " | 628 | f"{merge_branch} does not match destination branch " |
637 | f"{destination}" | 629 | f"{destination}" |
638 | ) | ||
639 | print("skipping upload.") | ||
640 | print( | ||
641 | f"Please use `--destination {destination}` if this " | ||
642 | "is intentional" | ||
643 | ) | ||
644 | branch.uploaded = False | ||
645 | continue | ||
646 | |||
647 | branch.UploadForReview( | ||
648 | people, | ||
649 | dryrun=opt.dryrun, | ||
650 | auto_topic=opt.auto_topic, | ||
651 | hashtags=hashtags, | ||
652 | labels=labels, | ||
653 | private=opt.private, | ||
654 | notify=notify, | ||
655 | wip=opt.wip, | ||
656 | ready=opt.ready, | ||
657 | dest_branch=destination, | ||
658 | validate_certs=opt.validate_certs, | ||
659 | push_options=opt.push_options, | ||
660 | ) | 630 | ) |
631 | print("skipping upload.") | ||
632 | print( | ||
633 | f"Please use `--destination {destination}` if this " | ||
634 | "is intentional" | ||
635 | ) | ||
636 | branch.uploaded = False | ||
637 | return | ||
638 | |||
639 | branch.UploadForReview( | ||
640 | people, | ||
641 | dryrun=opt.dryrun, | ||
642 | auto_topic=opt.auto_topic, | ||
643 | hashtags=hashtags, | ||
644 | labels=labels, | ||
645 | private=opt.private, | ||
646 | notify=notify, | ||
647 | wip=opt.wip, | ||
648 | ready=opt.ready, | ||
649 | dest_branch=destination, | ||
650 | validate_certs=opt.validate_certs, | ||
651 | push_options=opt.push_options, | ||
652 | ) | ||
653 | |||
654 | branch.uploaded = True | ||
661 | 655 | ||
662 | branch.uploaded = True | 656 | def _UploadAndReport(self, opt, todo, people): |
663 | except UploadError as e: | 657 | have_errors = False |
658 | aggregate_errors = [] | ||
659 | for branch in todo: | ||
660 | try: | ||
661 | self._UploadBranch(opt, branch, people) | ||
662 | except (UploadError, GitError) as e: | ||
664 | self.git_event_log.ErrorEvent(f"upload error: {e}") | 663 | self.git_event_log.ErrorEvent(f"upload error: {e}") |
665 | branch.error = e | 664 | branch.error = e |
665 | aggregate_errors.append(e) | ||
666 | branch.uploaded = False | 666 | branch.uploaded = False |
667 | have_errors = True | 667 | have_errors = True |
668 | 668 | ||
@@ -701,7 +701,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/ | |||
701 | ) | 701 | ) |
702 | 702 | ||
703 | if have_errors: | 703 | if have_errors: |
704 | raise branch.error | 704 | raise UploadExitError(aggregate_errors=aggregate_errors) |
705 | 705 | ||
706 | def _GetMergeBranch(self, project, local_branch=None): | 706 | def _GetMergeBranch(self, project, local_branch=None): |
707 | if local_branch is None: | 707 | if local_branch is None: |
diff --git a/tests/test_subcmds_upload.py b/tests/test_subcmds_upload.py new file mode 100644 index 00000000..75811996 --- /dev/null +++ b/tests/test_subcmds_upload.py | |||
@@ -0,0 +1,69 @@ | |||
1 | # Copyright (C) 2023 The Android Open Source Project | ||
2 | # | ||
3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
4 | # you may not use this file except in compliance with the License. | ||
5 | # You may obtain a copy of the License at | ||
6 | # | ||
7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
8 | # | ||
9 | # Unless required by applicable law or agreed to in writing, software | ||
10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
12 | # See the License for the specific language governing permissions and | ||
13 | # limitations under the License. | ||
14 | |||
15 | """Unittests for the subcmds/upload.py module.""" | ||
16 | |||
17 | import unittest | ||
18 | from unittest import mock | ||
19 | |||
20 | from subcmds import upload | ||
21 | from error import UploadError, GitError | ||
22 | |||
23 | |||
24 | class UnexpectedError(Exception): | ||
25 | """An exception not expected by upload command.""" | ||
26 | |||
27 | |||
28 | class UploadCommand(unittest.TestCase): | ||
29 | """Check registered all_commands.""" | ||
30 | |||
31 | def setUp(self): | ||
32 | self.cmd = upload.Upload() | ||
33 | self.branch = mock.MagicMock() | ||
34 | self.people = mock.MagicMock() | ||
35 | self.opt, _ = self.cmd.OptionParser.parse_args([]) | ||
36 | mock.patch.object( | ||
37 | self.cmd, "_AppendAutoList", return_value=None | ||
38 | ).start() | ||
39 | mock.patch.object(self.cmd, "git_event_log").start() | ||
40 | |||
41 | def tearDown(self): | ||
42 | mock.patch.stopall() | ||
43 | |||
44 | def test_UploadAndReport_UploadError(self): | ||
45 | """Check UploadExitError raised when UploadError encountered.""" | ||
46 | side_effect = UploadError("upload error") | ||
47 | with mock.patch.object( | ||
48 | self.cmd, "_UploadBranch", side_effect=side_effect | ||
49 | ): | ||
50 | with self.assertRaises(upload.UploadExitError): | ||
51 | self.cmd._UploadAndReport(self.opt, [self.branch], self.people) | ||
52 | |||
53 | def test_UploadAndReport_GitError(self): | ||
54 | """Check UploadExitError raised when GitError encountered.""" | ||
55 | side_effect = GitError("some git error") | ||
56 | with mock.patch.object( | ||
57 | self.cmd, "_UploadBranch", side_effect=side_effect | ||
58 | ): | ||
59 | with self.assertRaises(upload.UploadExitError): | ||
60 | self.cmd._UploadAndReport(self.opt, [self.branch], self.people) | ||
61 | |||
62 | def test_UploadAndReport_UnhandledError(self): | ||
63 | """Check UnexpectedError passed through.""" | ||
64 | side_effect = UnexpectedError("some os error") | ||
65 | with mock.patch.object( | ||
66 | self.cmd, "_UploadBranch", side_effect=side_effect | ||
67 | ): | ||
68 | with self.assertRaises(type(side_effect)): | ||
69 | self.cmd._UploadAndReport(self.opt, [self.branch], self.people) | ||