summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Chang <jasonnc@google.com>2023-08-03 14:38:00 -0700
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-08-07 23:56:07 +0000
commitf9aacd4087b02948da9a7878da48ea186ab99d5a (patch)
treeb683190635cd6fcb7cf817837ad0c4259b53078f
parentb8a7b4a629c3435d77a3266a4e6dce51dc342bd9 (diff)
downloadgit-repo-f9aacd4087b02948da9a7878da48ea186ab99d5a.tar.gz
Raise repo exit errors in place of sys.exit
Bug: b/293344017 Change-Id: I92d81c78eba8ff31b5252415f4c9a515a6c76411 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/381774 Tested-by: Jason Chang <jasonnc@google.com> Reviewed-by: Joanna Wang <jojwang@google.com> Commit-Queue: Jason Chang <jasonnc@google.com>
-rw-r--r--command.py8
-rw-r--r--fetch.py9
-rw-r--r--git_command.py21
-rw-r--r--project.py22
-rw-r--r--subcmds/abandon.py24
-rw-r--r--subcmds/init.py12
-rw-r--r--subcmds/upload.py10
-rw-r--r--tests/test_error.py4
-rw-r--r--tests/test_git_command.py4
9 files changed, 80 insertions, 34 deletions
diff --git a/command.py b/command.py
index 939a4630..d2129381 100644
--- a/command.py
+++ b/command.py
@@ -16,11 +16,11 @@ import multiprocessing
16import os 16import os
17import optparse 17import optparse
18import re 18import re
19import sys
20 19
21from event_log import EventLog 20from event_log import EventLog
22from error import NoSuchProjectError 21from error import NoSuchProjectError
23from error import InvalidProjectGroupsError 22from error import InvalidProjectGroupsError
23from error import RepoExitError
24import progress 24import progress
25 25
26 26
@@ -42,6 +42,10 @@ WORKER_BATCH_SIZE = 32
42DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8) 42DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8)
43 43
44 44
45class UsageError(RepoExitError):
46 """Exception thrown with invalid command usage."""
47
48
45class Command(object): 49class Command(object):
46 """Base class for any command line action in repo.""" 50 """Base class for any command line action in repo."""
47 51
@@ -215,7 +219,7 @@ class Command(object):
215 def Usage(self): 219 def Usage(self):
216 """Display usage and terminate.""" 220 """Display usage and terminate."""
217 self.OptionParser.print_usage() 221 self.OptionParser.print_usage()
218 sys.exit(1) 222 raise UsageError()
219 223
220 def CommonValidateOptions(self, opt, args): 224 def CommonValidateOptions(self, opt, args):
221 """Validate common options.""" 225 """Validate common options."""
diff --git a/fetch.py b/fetch.py
index 31f8152f..cf8d1d7d 100644
--- a/fetch.py
+++ b/fetch.py
@@ -18,6 +18,11 @@ import subprocess
18import sys 18import sys
19from urllib.parse import urlparse 19from urllib.parse import urlparse
20from urllib.request import urlopen 20from urllib.request import urlopen
21from error import RepoExitError
22
23
24class FetchFileError(RepoExitError):
25 """Exit error when fetch_file fails."""
21 26
22 27
23def fetch_file(url, verbose=False): 28def fetch_file(url, verbose=False):
@@ -29,6 +34,7 @@ def fetch_file(url, verbose=False):
29 scheme = urlparse(url).scheme 34 scheme = urlparse(url).scheme
30 if scheme == "gs": 35 if scheme == "gs":
31 cmd = ["gsutil", "cat", url] 36 cmd = ["gsutil", "cat", url]
37 errors = []
32 try: 38 try:
33 result = subprocess.run( 39 result = subprocess.run(
34 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True 40 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True
@@ -41,9 +47,10 @@ def fetch_file(url, verbose=False):
41 ) 47 )
42 return result.stdout 48 return result.stdout
43 except subprocess.CalledProcessError as e: 49 except subprocess.CalledProcessError as e:
50 errors.append(e)
44 print( 51 print(
45 'fatal: error running "gsutil": %s' % e.stderr, file=sys.stderr 52 'fatal: error running "gsutil": %s' % e.stderr, file=sys.stderr
46 ) 53 )
47 sys.exit(1) 54 raise FetchFileError(aggregate_errors=errors)
48 with urlopen(url) as f: 55 with urlopen(url) as f:
49 return f.read() 56 return f.read()
diff --git a/git_command.py b/git_command.py
index 588a64fd..36fcfe7c 100644
--- a/git_command.py
+++ b/git_command.py
@@ -19,6 +19,7 @@ import subprocess
19from typing import Any, Optional 19from typing import Any, Optional
20 20
21from error import GitError 21from error import GitError
22from error import RepoExitError
22from git_refs import HEAD 23from git_refs import HEAD
23import platform_utils 24import platform_utils
24from repo_trace import REPO_TRACE, IsTrace, Trace 25from repo_trace import REPO_TRACE, IsTrace, Trace
@@ -44,6 +45,7 @@ DEFAULT_GIT_FAIL_MESSAGE = "git command failure"
44# Common line length limit 45# Common line length limit
45GIT_ERROR_STDOUT_LINES = 1 46GIT_ERROR_STDOUT_LINES = 1
46GIT_ERROR_STDERR_LINES = 1 47GIT_ERROR_STDERR_LINES = 1
48INVALID_GIT_EXIT_CODE = 126
47 49
48 50
49class _GitCall(object): 51class _GitCall(object):
@@ -51,8 +53,9 @@ class _GitCall(object):
51 def version_tuple(self): 53 def version_tuple(self):
52 ret = Wrapper().ParseGitVersion() 54 ret = Wrapper().ParseGitVersion()
53 if ret is None: 55 if ret is None:
54 print("fatal: unable to detect git version", file=sys.stderr) 56 msg = "fatal: unable to detect git version"
55 sys.exit(1) 57 print(msg, file=sys.stderr)
58 raise GitRequireError(msg)
56 return ret 59 return ret
57 60
58 def __getattr__(self, name): 61 def __getattr__(self, name):
@@ -167,10 +170,9 @@ def git_require(min_version, fail=False, msg=""):
167 need = ".".join(map(str, min_version)) 170 need = ".".join(map(str, min_version))
168 if msg: 171 if msg:
169 msg = " for " + msg 172 msg = " for " + msg
170 print( 173 error_msg = "fatal: git %s or later required%s" % (need, msg)
171 "fatal: git %s or later required%s" % (need, msg), file=sys.stderr 174 print(error_msg, file=sys.stderr)
172 ) 175 raise GitRequireError(error_msg)
173 sys.exit(1)
174 return False 176 return False
175 177
176 178
@@ -403,6 +405,13 @@ class GitCommand(object):
403 ) 405 )
404 406
405 407
408class GitRequireError(RepoExitError):
409 """Error raised when git version is unavailable or invalid."""
410
411 def __init__(self, message, exit_code: int = INVALID_GIT_EXIT_CODE):
412 super().__init__(message, exit_code=exit_code)
413
414
406class GitCommandError(GitError): 415class GitCommandError(GitError):
407 """ 416 """
408 Error raised from a failed git command. 417 Error raised from a failed git command.
diff --git a/project.py b/project.py
index b268007d..c3eb09c9 100644
--- a/project.py
+++ b/project.py
@@ -2003,8 +2003,8 @@ class Project(object):
2003 name: The name of the branch to abandon. 2003 name: The name of the branch to abandon.
2004 2004
2005 Returns: 2005 Returns:
2006 True if the abandon succeeded; False if it didn't; None if the 2006 True if the abandon succeeded; Raises GitCommandError if it didn't;
2007 branch didn't exist. 2007 None if the branch didn't exist.
2008 """ 2008 """
2009 rev = R_HEADS + name 2009 rev = R_HEADS + name
2010 all_refs = self.bare_ref.all 2010 all_refs = self.bare_ref.all
@@ -2025,16 +2025,14 @@ class Project(object):
2025 ) 2025 )
2026 else: 2026 else:
2027 self._Checkout(revid, quiet=True) 2027 self._Checkout(revid, quiet=True)
2028 2028 GitCommand(
2029 return ( 2029 self,
2030 GitCommand( 2030 ["branch", "-D", name],
2031 self, 2031 capture_stdout=True,
2032 ["branch", "-D", name], 2032 capture_stderr=True,
2033 capture_stdout=True, 2033 verify_command=True,
2034 capture_stderr=True, 2034 ).Wait()
2035 ).Wait() 2035 return True
2036 == 0
2037 )
2038 2036
2039 def PruneHeads(self): 2037 def PruneHeads(self):
2040 """Prune any topic branches already merged into upstream.""" 2038 """Prune any topic branches already merged into upstream."""
diff --git a/subcmds/abandon.py b/subcmds/abandon.py
index ded287f6..896b348f 100644
--- a/subcmds/abandon.py
+++ b/subcmds/abandon.py
@@ -20,6 +20,11 @@ import sys
20from command import Command, DEFAULT_LOCAL_JOBS 20from command import Command, DEFAULT_LOCAL_JOBS
21from git_command import git 21from git_command import git
22from progress import Progress 22from progress import Progress
23from error import RepoError, RepoExitError
24
25
26class AbandonError(RepoExitError):
27 """Exit error when abandon command fails."""
23 28
24 29
25class Abandon(Command): 30class Abandon(Command):
@@ -68,28 +73,37 @@ It is equivalent to "git branch -D <branchname>".
68 branches = nb 73 branches = nb
69 74
70 ret = {} 75 ret = {}
76 errors = []
71 for name in branches: 77 for name in branches:
72 status = project.AbandonBranch(name) 78 status = None
79 try:
80 status = project.AbandonBranch(name)
81 except RepoError as e:
82 status = False
83 errors.append(e)
73 if status is not None: 84 if status is not None:
74 ret[name] = status 85 ret[name] = status
75 return (ret, project) 86
87 return (ret, project, errors)
76 88
77 def Execute(self, opt, args): 89 def Execute(self, opt, args):
78 nb = args[0].split() 90 nb = args[0].split()
79 err = defaultdict(list) 91 err = defaultdict(list)
80 success = defaultdict(list) 92 success = defaultdict(list)
93 aggregate_errors = []
81 all_projects = self.GetProjects( 94 all_projects = self.GetProjects(
82 args[1:], all_manifests=not opt.this_manifest_only 95 args[1:], all_manifests=not opt.this_manifest_only
83 ) 96 )
84 _RelPath = lambda p: p.RelPath(local=opt.this_manifest_only) 97 _RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
85 98
86 def _ProcessResults(_pool, pm, states): 99 def _ProcessResults(_pool, pm, states):
87 for results, project in states: 100 for results, project, errors in states:
88 for branch, status in results.items(): 101 for branch, status in results.items():
89 if status: 102 if status:
90 success[branch].append(project) 103 success[branch].append(project)
91 else: 104 else:
92 err[branch].append(project) 105 err[branch].append(project)
106 aggregate_errors.extend(errors)
93 pm.update(msg="") 107 pm.update(msg="")
94 108
95 self.ExecuteInParallel( 109 self.ExecuteInParallel(
@@ -116,13 +130,13 @@ It is equivalent to "git branch -D <branchname>".
116 " " * len(err_msg) + " | %s" % _RelPath(proj), 130 " " * len(err_msg) + " | %s" % _RelPath(proj),
117 file=sys.stderr, 131 file=sys.stderr,
118 ) 132 )
119 sys.exit(1) 133 raise AbandonError(aggregate_errors=aggregate_errors)
120 elif not success: 134 elif not success:
121 print( 135 print(
122 "error: no project has local branch(es) : %s" % nb, 136 "error: no project has local branch(es) : %s" % nb,
123 file=sys.stderr, 137 file=sys.stderr,
124 ) 138 )
125 sys.exit(1) 139 raise AbandonError(aggregate_errors=aggregate_errors)
126 else: 140 else:
127 # Everything below here is displaying status. 141 # Everything below here is displaying status.
128 if opt.quiet: 142 if opt.quiet:
diff --git a/subcmds/init.py b/subcmds/init.py
index 6d7fd857..868d339e 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -19,6 +19,8 @@ from color import Coloring
19from command import InteractiveCommand, MirrorSafeCommand 19from command import InteractiveCommand, MirrorSafeCommand
20from git_command import git_require, MIN_GIT_VERSION_SOFT, MIN_GIT_VERSION_HARD 20from git_command import git_require, MIN_GIT_VERSION_SOFT, MIN_GIT_VERSION_HARD
21from wrapper import Wrapper 21from wrapper import Wrapper
22from error import UpdateManifestError
23from error import RepoUnhandledExceptionError
22 24
23_REPO_ALLOW_SHALLOW = os.environ.get("REPO_ALLOW_SHALLOW") 25_REPO_ALLOW_SHALLOW = os.environ.get("REPO_ALLOW_SHALLOW")
24 26
@@ -156,7 +158,10 @@ to update the working directory files.
156 git_event_log=self.git_event_log, 158 git_event_log=self.git_event_log,
157 manifest_name=opt.manifest_name, 159 manifest_name=opt.manifest_name,
158 ): 160 ):
159 sys.exit(1) 161 manifest_name = opt.manifest_name
162 raise UpdateManifestError(
163 f"Unable to sync manifest {manifest_name}"
164 )
160 165
161 def _Prompt(self, prompt, value): 166 def _Prompt(self, prompt, value):
162 print("%-10s [%s]: " % (prompt, value), end="", flush=True) 167 print("%-10s [%s]: " % (prompt, value), end="", flush=True)
@@ -346,14 +351,15 @@ to update the working directory files.
346 repo_verify=opt.repo_verify, 351 repo_verify=opt.repo_verify,
347 quiet=opt.quiet, 352 quiet=opt.quiet,
348 ) 353 )
349 except wrapper.CloneFailure: 354 except wrapper.CloneFailure as e:
350 err_msg = "fatal: double check your --repo-rev setting." 355 err_msg = "fatal: double check your --repo-rev setting."
351 print( 356 print(
352 err_msg, 357 err_msg,
353 file=sys.stderr, 358 file=sys.stderr,
354 ) 359 )
355 self.git_event_log.ErrorEvent(err_msg) 360 self.git_event_log.ErrorEvent(err_msg)
356 sys.exit(1) 361 raise RepoUnhandledExceptionError(e)
362
357 branch = rp.GetBranch("default") 363 branch = rp.GetBranch("default")
358 branch.merge = remote_ref 364 branch.merge = remote_ref
359 rp.work_git.reset("--hard", rev) 365 rp.work_git.reset("--hard", rev)
diff --git a/subcmds/upload.py b/subcmds/upload.py
index 8d949bef..d0c028b9 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 24from error import UploadError, RepoExitError
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,6 +31,10 @@ from project import ReviewableBranch
31_DEFAULT_UNUSUAL_COMMIT_THRESHOLD = 5 31_DEFAULT_UNUSUAL_COMMIT_THRESHOLD = 5
32 32
33 33
34class UploadExitError(RepoExitError):
35 """Indicates that there is an upload command error requiring a sys exit."""
36
37
34def _VerifyPendingCommits(branches: List[ReviewableBranch]) -> bool: 38def _VerifyPendingCommits(branches: List[ReviewableBranch]) -> bool:
35 """Perform basic safety checks on the given set of branches. 39 """Perform basic safety checks on the given set of branches.
36 40
@@ -86,7 +90,7 @@ def _VerifyPendingCommits(branches: List[ReviewableBranch]) -> bool:
86def _die(fmt, *args): 90def _die(fmt, *args):
87 msg = fmt % args 91 msg = fmt % args
88 print("error: %s" % msg, file=sys.stderr) 92 print("error: %s" % msg, file=sys.stderr)
89 sys.exit(1) 93 raise UploadExitError(msg)
90 94
91 95
92def _SplitEmails(values): 96def _SplitEmails(values):
@@ -697,7 +701,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
697 ) 701 )
698 702
699 if have_errors: 703 if have_errors:
700 sys.exit(1) 704 raise branch.error
701 705
702 def _GetMergeBranch(self, project, local_branch=None): 706 def _GetMergeBranch(self, project, local_branch=None):
703 if local_branch is None: 707 if local_branch is None:
diff --git a/tests/test_error.py b/tests/test_error.py
index 2733ab8c..2b28f5c2 100644
--- a/tests/test_error.py
+++ b/tests/test_error.py
@@ -21,12 +21,16 @@ import unittest
21import error 21import error
22import project 22import project
23import git_command 23import git_command
24import fetch
25import command
24from subcmds import all_modules 26from subcmds import all_modules
25 27
26imports = all_modules + [ 28imports = all_modules + [
27 error, 29 error,
28 project, 30 project,
29 git_command, 31 git_command,
32 fetch,
33 command,
30] 34]
31 35
32 36
diff --git a/tests/test_git_command.py b/tests/test_git_command.py
index 1e8beabc..3dd31b29 100644
--- a/tests/test_git_command.py
+++ b/tests/test_git_command.py
@@ -204,12 +204,12 @@ class GitRequireTests(unittest.TestCase):
204 204
205 def test_older_fatal(self): 205 def test_older_fatal(self):
206 """Test fatal require calls with old versions.""" 206 """Test fatal require calls with old versions."""
207 with self.assertRaises(SystemExit) as e: 207 with self.assertRaises(git_command.GitRequireError) as e:
208 git_command.git_require((2,), fail=True) 208 git_command.git_require((2,), fail=True)
209 self.assertNotEqual(0, e.code) 209 self.assertNotEqual(0, e.code)
210 210
211 def test_older_fatal_msg(self): 211 def test_older_fatal_msg(self):
212 """Test fatal require calls with old versions and message.""" 212 """Test fatal require calls with old versions and message."""
213 with self.assertRaises(SystemExit) as e: 213 with self.assertRaises(git_command.GitRequireError) as e:
214 git_command.git_require((2,), fail=True, msg="so sad") 214 git_command.git_require((2,), fail=True, msg="so sad")
215 self.assertNotEqual(0, e.code) 215 self.assertNotEqual(0, e.code)