diff options
| author | Jason Chang <jasonnc@google.com> | 2023-08-17 11:36:41 -0700 |
|---|---|---|
| committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2023-08-21 16:52:48 +0000 |
| commit | 5a3a5f7cec40c70d8c5ceb473f828e1149724962 (patch) | |
| tree | ec64b0ed6afde9632ec93f4215ce5dfad54383f1 | |
| parent | 11cb96030ed5a0c322b5262c10a6af1a70486981 (diff) | |
| download | git-repo-5a3a5f7cec40c70d8c5ceb473f828e1149724962.tar.gz | |
upload: fix error handling
There was a bug in error handeling code that caused an uncaught
exception to be raised.
Bug: b/296316540
Change-Id: I49c72f29c00f26ba60de552f958bc6eddf841162
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/383254
Reviewed-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Jason Chang <jasonnc@google.com>
Tested-by: Jason Chang <jasonnc@google.com>
| -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) | ||
