summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Chang <jasonnc@google.com>2023-08-17 11:36:41 -0700
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-08-21 16:52:48 +0000
commit5a3a5f7cec40c70d8c5ceb473f828e1149724962 (patch)
treeec64b0ed6afde9632ec93f4215ce5dfad54383f1
parent11cb96030ed5a0c322b5262c10a6af1a70486981 (diff)
downloadgit-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.py10
-rw-r--r--subcmds/upload.py250
-rw-r--r--tests/test_subcmds_upload.py69
3 files changed, 200 insertions, 129 deletions
diff --git a/project.py b/project.py
index 6e6a605e..a3b6312e 100644
--- a/project.py
+++ b/project.py
@@ -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
22from command import DEFAULT_LOCAL_JOBS, InteractiveCommand 22from command import DEFAULT_LOCAL_JOBS, InteractiveCommand
23from editor import Editor 23from editor import Editor
24from error import UploadError, RepoExitError 24from error import UploadError, SilentRepoExitError, GitError
25from git_command import GitCommand 25from git_command import GitCommand
26from git_refs import R_HEADS 26from git_refs import R_HEADS
27from hooks import RepoHook 27from 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
34class UploadExitError(RepoExitError): 34class 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
17import unittest
18from unittest import mock
19
20from subcmds import upload
21from error import UploadError, GitError
22
23
24class UnexpectedError(Exception):
25 """An exception not expected by upload command."""
26
27
28class 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)