summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Chang <jasonnc@google.com>2023-07-14 16:45:35 -0700
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-08-02 18:29:05 +0000
commit32b59565b7bd41ec1a121869823557f0b2b022d7 (patch)
tree0cd0fe644ecc6e319df96861f26b77a55c9969eb
parenta6413f5d88f12466b3daa833668d0f59fc65ece4 (diff)
downloadgit-repo-32b59565b7bd41ec1a121869823557f0b2b022d7.tar.gz
Refactor errors for sync command
Per discussion in go/repo-error-update updated aggregated and exit errors for sync command. Aggregated errors are errors that result in eventual command failure. Exit errors are errors that result in immediate command failure. Also updated main.py to log aggregated and exit errors to git sessions log Bug: b/293344017 Change-Id: I77a21f14da32fe2e68c16841feb22de72e86a251 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/379614 Reviewed-by: Aravind Vasudevan <aravindvasudev@google.com> Tested-by: Jason Chang <jasonnc@google.com> Commit-Queue: Jason Chang <jasonnc@google.com>
-rwxr-xr-xmain.py56
-rw-r--r--project.py283
-rw-r--r--subcmds/download.py2
-rw-r--r--subcmds/sync.py159
-rw-r--r--tests/test_subcmds_sync.py83
5 files changed, 441 insertions, 142 deletions
diff --git a/main.py b/main.py
index 4c5f1043..57a59acb 100755
--- a/main.py
+++ b/main.py
@@ -30,6 +30,7 @@ import sys
30import textwrap 30import textwrap
31import time 31import time
32import urllib.request 32import urllib.request
33import json
33 34
34try: 35try:
35 import kerberos 36 import kerberos
@@ -50,10 +51,12 @@ from editor import Editor
50from error import DownloadError 51from error import DownloadError
51from error import InvalidProjectGroupsError 52from error import InvalidProjectGroupsError
52from error import ManifestInvalidRevisionError 53from error import ManifestInvalidRevisionError
53from error import ManifestParseError
54from error import NoManifestException 54from error import NoManifestException
55from error import NoSuchProjectError 55from error import NoSuchProjectError
56from error import RepoChangedException 56from error import RepoChangedException
57from error import RepoExitError
58from error import RepoUnhandledExceptionError
59from error import RepoError
57import gitc_utils 60import gitc_utils
58from manifest_xml import GitcClient, RepoClient 61from manifest_xml import GitcClient, RepoClient
59from pager import RunPager, TerminatePager 62from pager import RunPager, TerminatePager
@@ -97,6 +100,7 @@ else:
97 ) 100 )
98 101
99KEYBOARD_INTERRUPT_EXIT = 128 + signal.SIGINT 102KEYBOARD_INTERRUPT_EXIT = 128 + signal.SIGINT
103MAX_PRINT_ERRORS = 5
100 104
101global_options = optparse.OptionParser( 105global_options = optparse.OptionParser(
102 usage="repo [-p|--paginate|--no-pager] COMMAND [ARGS]", 106 usage="repo [-p|--paginate|--no-pager] COMMAND [ARGS]",
@@ -422,10 +426,33 @@ class _Repo(object):
422 """ 426 """
423 try: 427 try:
424 execute_command_helper() 428 execute_command_helper()
425 except (KeyboardInterrupt, SystemExit, Exception) as e: 429 except (
430 KeyboardInterrupt,
431 SystemExit,
432 Exception,
433 RepoExitError,
434 ) as e:
426 ok = isinstance(e, SystemExit) and not e.code 435 ok = isinstance(e, SystemExit) and not e.code
436 exception_name = type(e).__name__
437 if isinstance(e, RepoUnhandledExceptionError):
438 exception_name = type(e.error).__name__
439 if isinstance(e, RepoExitError):
440 aggregated_errors = e.aggregate_errors or []
441 for error in aggregated_errors:
442 project = None
443 if isinstance(error, RepoError):
444 project = error.project
445 error_info = json.dumps(
446 {
447 "ErrorType": type(error).__name__,
448 "Project": project,
449 "Message": str(error),
450 }
451 )
452 git_trace2_event_log.ErrorEvent(
453 f"AggregateExitError:{error_info}"
454 )
427 if not ok: 455 if not ok:
428 exception_name = type(e).__name__
429 git_trace2_event_log.ErrorEvent( 456 git_trace2_event_log.ErrorEvent(
430 f"RepoExitError:{exception_name}" 457 f"RepoExitError:{exception_name}"
431 ) 458 )
@@ -447,13 +474,13 @@ class _Repo(object):
447 "error: manifest missing or unreadable -- please run init", 474 "error: manifest missing or unreadable -- please run init",
448 file=sys.stderr, 475 file=sys.stderr,
449 ) 476 )
450 result = 1 477 result = e.exit_code
451 except NoSuchProjectError as e: 478 except NoSuchProjectError as e:
452 if e.name: 479 if e.name:
453 print("error: project %s not found" % e.name, file=sys.stderr) 480 print("error: project %s not found" % e.name, file=sys.stderr)
454 else: 481 else:
455 print("error: no project in current directory", file=sys.stderr) 482 print("error: no project in current directory", file=sys.stderr)
456 result = 1 483 result = e.exit_code
457 except InvalidProjectGroupsError as e: 484 except InvalidProjectGroupsError as e:
458 if e.name: 485 if e.name:
459 print( 486 print(
@@ -467,7 +494,7 @@ class _Repo(object):
467 "the current directory", 494 "the current directory",
468 file=sys.stderr, 495 file=sys.stderr,
469 ) 496 )
470 result = 1 497 result = e.exit_code
471 except SystemExit as e: 498 except SystemExit as e:
472 if e.code: 499 if e.code:
473 result = e.code 500 result = e.code
@@ -475,6 +502,9 @@ class _Repo(object):
475 except KeyboardInterrupt: 502 except KeyboardInterrupt:
476 result = KEYBOARD_INTERRUPT_EXIT 503 result = KEYBOARD_INTERRUPT_EXIT
477 raise 504 raise
505 except RepoExitError as e:
506 result = e.exit_code
507 raise
478 except Exception: 508 except Exception:
479 result = 1 509 result = 1
480 raise 510 raise
@@ -841,12 +871,20 @@ def _Main(argv):
841 SetTraceToStderr() 871 SetTraceToStderr()
842 872
843 result = repo._Run(name, gopts, argv) or 0 873 result = repo._Run(name, gopts, argv) or 0
874 except RepoExitError as e:
875 exception_name = type(e).__name__
876 result = e.exit_code
877 print("fatal: %s" % e, file=sys.stderr)
878 if e.aggregate_errors:
879 print(f"{exception_name} Aggregate Errors")
880 for err in e.aggregate_errors[:MAX_PRINT_ERRORS]:
881 print(err)
882 if len(e.aggregate_errors) > MAX_PRINT_ERRORS:
883 diff = len(e.aggregate_errors) - MAX_PRINT_ERRORS
884 print(f"+{diff} additional errors ...")
844 except KeyboardInterrupt: 885 except KeyboardInterrupt:
845 print("aborted by user", file=sys.stderr) 886 print("aborted by user", file=sys.stderr)
846 result = KEYBOARD_INTERRUPT_EXIT 887 result = KEYBOARD_INTERRUPT_EXIT
847 except ManifestParseError as mpe:
848 print("fatal: %s" % mpe, file=sys.stderr)
849 result = 1
850 except RepoChangedException as rce: 888 except RepoChangedException as rce:
851 # If repo changed, re-exec ourselves. 889 # If repo changed, re-exec ourselves.
852 # 890 #
diff --git a/project.py b/project.py
index 83f3eff9..b268007d 100644
--- a/project.py
+++ b/project.py
@@ -26,7 +26,7 @@ import sys
26import tarfile 26import tarfile
27import tempfile 27import tempfile
28import time 28import time
29from typing import NamedTuple 29from typing import NamedTuple, List
30import urllib.parse 30import urllib.parse
31 31
32from color import Coloring 32from color import Coloring
@@ -41,7 +41,12 @@ from git_config import (
41) 41)
42import git_superproject 42import git_superproject
43from git_trace2_event_log import EventLog 43from git_trace2_event_log import EventLog
44from error import GitError, UploadError, DownloadError 44from error import (
45 GitError,
46 UploadError,
47 DownloadError,
48 RepoError,
49)
45from error import ManifestInvalidRevisionError, ManifestInvalidPathError 50from error import ManifestInvalidRevisionError, ManifestInvalidPathError
46from error import NoManifestException, ManifestParseError 51from error import NoManifestException, ManifestParseError
47import platform_utils 52import platform_utils
@@ -54,11 +59,33 @@ from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M
54class SyncNetworkHalfResult(NamedTuple): 59class SyncNetworkHalfResult(NamedTuple):
55 """Sync_NetworkHalf return value.""" 60 """Sync_NetworkHalf return value."""
56 61
57 # True if successful.
58 success: bool
59 # Did we query the remote? False when optimized_fetch is True and we have 62 # Did we query the remote? False when optimized_fetch is True and we have
60 # the commit already present. 63 # the commit already present.
61 remote_fetched: bool 64 remote_fetched: bool
65 # Error from SyncNetworkHalf
66 error: Exception = None
67
68 @property
69 def success(self) -> bool:
70 return not self.error
71
72
73class SyncNetworkHalfError(RepoError):
74 """Failure trying to sync."""
75
76
77class DeleteWorktreeError(RepoError):
78 """Failure to delete worktree."""
79
80 def __init__(
81 self, *args, aggregate_errors: List[Exception] = None, **kwargs
82 ) -> None:
83 super().__init__(*args, **kwargs)
84 self.aggregate_errors = aggregate_errors or []
85
86
87class DeleteDirtyWorktreeError(DeleteWorktreeError):
88 """Failure to delete worktree due to uncommitted changes."""
62 89
63 90
64# Maximum sleep time allowed during retries. 91# Maximum sleep time allowed during retries.
@@ -1070,13 +1097,19 @@ class Project(object):
1070 if branch is None: 1097 if branch is None:
1071 branch = self.CurrentBranch 1098 branch = self.CurrentBranch
1072 if branch is None: 1099 if branch is None:
1073 raise GitError("not currently on a branch") 1100 raise GitError("not currently on a branch", project=self.name)
1074 1101
1075 branch = self.GetBranch(branch) 1102 branch = self.GetBranch(branch)
1076 if not branch.LocalMerge: 1103 if not branch.LocalMerge:
1077 raise GitError("branch %s does not track a remote" % branch.name) 1104 raise GitError(
1105 "branch %s does not track a remote" % branch.name,
1106 project=self.name,
1107 )
1078 if not branch.remote.review: 1108 if not branch.remote.review:
1079 raise GitError("remote %s has no review url" % branch.remote.name) 1109 raise GitError(
1110 "remote %s has no review url" % branch.remote.name,
1111 project=self.name,
1112 )
1080 1113
1081 # Basic validity check on label syntax. 1114 # Basic validity check on label syntax.
1082 for label in labels: 1115 for label in labels:
@@ -1193,11 +1226,18 @@ class Project(object):
1193 """ 1226 """
1194 if archive and not isinstance(self, MetaProject): 1227 if archive and not isinstance(self, MetaProject):
1195 if self.remote.url.startswith(("http://", "https://")): 1228 if self.remote.url.startswith(("http://", "https://")):
1229 msg_template = (
1230 "%s: Cannot fetch archives from http/https remotes."
1231 )
1232 msg_args = self.name
1233 msg = msg_template % msg_args
1196 _error( 1234 _error(
1197 "%s: Cannot fetch archives from http/https remotes.", 1235 msg_template,
1198 self.name, 1236 msg_args,
1237 )
1238 return SyncNetworkHalfResult(
1239 False, SyncNetworkHalfError(msg, project=self.name)
1199 ) 1240 )
1200 return SyncNetworkHalfResult(False, False)
1201 1241
1202 name = self.relpath.replace("\\", "/") 1242 name = self.relpath.replace("\\", "/")
1203 name = name.replace("/", "_") 1243 name = name.replace("/", "_")
@@ -1208,19 +1248,25 @@ class Project(object):
1208 self._FetchArchive(tarpath, cwd=topdir) 1248 self._FetchArchive(tarpath, cwd=topdir)
1209 except GitError as e: 1249 except GitError as e:
1210 _error("%s", e) 1250 _error("%s", e)
1211 return SyncNetworkHalfResult(False, False) 1251 return SyncNetworkHalfResult(False, e)
1212 1252
1213 # From now on, we only need absolute tarpath. 1253 # From now on, we only need absolute tarpath.
1214 tarpath = os.path.join(topdir, tarpath) 1254 tarpath = os.path.join(topdir, tarpath)
1215 1255
1216 if not self._ExtractArchive(tarpath, path=topdir): 1256 if not self._ExtractArchive(tarpath, path=topdir):
1217 return SyncNetworkHalfResult(False, True) 1257 return SyncNetworkHalfResult(
1258 True,
1259 SyncNetworkHalfError(
1260 f"Unable to Extract Archive {tarpath}",
1261 project=self.name,
1262 ),
1263 )
1218 try: 1264 try:
1219 platform_utils.remove(tarpath) 1265 platform_utils.remove(tarpath)
1220 except OSError as e: 1266 except OSError as e:
1221 _warn("Cannot remove archive %s: %s", tarpath, str(e)) 1267 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1222 self._CopyAndLinkFiles() 1268 self._CopyAndLinkFiles()
1223 return SyncNetworkHalfResult(True, True) 1269 return SyncNetworkHalfResult(True)
1224 1270
1225 # If the shared object dir already exists, don't try to rebootstrap with 1271 # If the shared object dir already exists, don't try to rebootstrap with
1226 # a clone bundle download. We should have the majority of objects 1272 # a clone bundle download. We should have the majority of objects
@@ -1310,23 +1356,35 @@ class Project(object):
1310 ) 1356 )
1311 ): 1357 ):
1312 remote_fetched = True 1358 remote_fetched = True
1313 if not self._RemoteFetch( 1359 try:
1314 initial=is_new, 1360 if not self._RemoteFetch(
1315 quiet=quiet, 1361 initial=is_new,
1316 verbose=verbose, 1362 quiet=quiet,
1317 output_redir=output_redir, 1363 verbose=verbose,
1318 alt_dir=alt_dir, 1364 output_redir=output_redir,
1319 current_branch_only=current_branch_only, 1365 alt_dir=alt_dir,
1320 tags=tags, 1366 current_branch_only=current_branch_only,
1321 prune=prune, 1367 tags=tags,
1322 depth=depth, 1368 prune=prune,
1323 submodules=submodules, 1369 depth=depth,
1324 force_sync=force_sync, 1370 submodules=submodules,
1325 ssh_proxy=ssh_proxy, 1371 force_sync=force_sync,
1326 clone_filter=clone_filter, 1372 ssh_proxy=ssh_proxy,
1327 retry_fetches=retry_fetches, 1373 clone_filter=clone_filter,
1328 ): 1374 retry_fetches=retry_fetches,
1329 return SyncNetworkHalfResult(False, remote_fetched) 1375 ):
1376 return SyncNetworkHalfResult(
1377 remote_fetched,
1378 SyncNetworkHalfError(
1379 f"Unable to remote fetch project {self.name}",
1380 project=self.name,
1381 ),
1382 )
1383 except RepoError as e:
1384 return SyncNetworkHalfResult(
1385 remote_fetched,
1386 e,
1387 )
1330 1388
1331 mp = self.manifest.manifestProject 1389 mp = self.manifest.manifestProject
1332 dissociate = mp.dissociate 1390 dissociate = mp.dissociate
@@ -1346,7 +1404,12 @@ class Project(object):
1346 if p.stdout and output_redir: 1404 if p.stdout and output_redir:
1347 output_redir.write(p.stdout) 1405 output_redir.write(p.stdout)
1348 if p.Wait() != 0: 1406 if p.Wait() != 0:
1349 return SyncNetworkHalfResult(False, remote_fetched) 1407 return SyncNetworkHalfResult(
1408 remote_fetched,
1409 GitError(
1410 "Unable to repack alternates", project=self.name
1411 ),
1412 )
1350 platform_utils.remove(alternates_file) 1413 platform_utils.remove(alternates_file)
1351 1414
1352 if self.worktree: 1415 if self.worktree:
@@ -1356,7 +1419,7 @@ class Project(object):
1356 platform_utils.remove( 1419 platform_utils.remove(
1357 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True 1420 os.path.join(self.gitdir, "FETCH_HEAD"), missing_ok=True
1358 ) 1421 )
1359 return SyncNetworkHalfResult(True, remote_fetched) 1422 return SyncNetworkHalfResult(remote_fetched)
1360 1423
1361 def PostRepoUpgrade(self): 1424 def PostRepoUpgrade(self):
1362 self._InitHooks() 1425 self._InitHooks()
@@ -1409,16 +1472,27 @@ class Project(object):
1409 1472
1410 self.revisionId = revisionId 1473 self.revisionId = revisionId
1411 1474
1412 def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False): 1475 def Sync_LocalHalf(
1476 self, syncbuf, force_sync=False, submodules=False, errors=None
1477 ):
1413 """Perform only the local IO portion of the sync process. 1478 """Perform only the local IO portion of the sync process.
1414 1479
1415 Network access is not required. 1480 Network access is not required.
1416 """ 1481 """
1482 if errors is None:
1483 errors = []
1484
1485 def fail(error: Exception):
1486 errors.append(error)
1487 syncbuf.fail(self, error)
1488
1417 if not os.path.exists(self.gitdir): 1489 if not os.path.exists(self.gitdir):
1418 syncbuf.fail( 1490 fail(
1419 self, 1491 LocalSyncFail(
1420 "Cannot checkout %s due to missing network sync; Run " 1492 "Cannot checkout %s due to missing network sync; Run "
1421 "`repo sync -n %s` first." % (self.name, self.name), 1493 "`repo sync -n %s` first." % (self.name, self.name),
1494 project=self.name,
1495 )
1422 ) 1496 )
1423 return 1497 return
1424 1498
@@ -1438,10 +1512,12 @@ class Project(object):
1438 ) 1512 )
1439 bad_paths = paths & PROTECTED_PATHS 1513 bad_paths = paths & PROTECTED_PATHS
1440 if bad_paths: 1514 if bad_paths:
1441 syncbuf.fail( 1515 fail(
1442 self, 1516 LocalSyncFail(
1443 "Refusing to checkout project that writes to protected " 1517 "Refusing to checkout project that writes to protected "
1444 "paths: %s" % (", ".join(bad_paths),), 1518 "paths: %s" % (", ".join(bad_paths),),
1519 project=self.name,
1520 )
1445 ) 1521 )
1446 return 1522 return
1447 1523
@@ -1466,7 +1542,7 @@ class Project(object):
1466 # Currently on a detached HEAD. The user is assumed to 1542 # Currently on a detached HEAD. The user is assumed to
1467 # not have any local modifications worth worrying about. 1543 # not have any local modifications worth worrying about.
1468 if self.IsRebaseInProgress(): 1544 if self.IsRebaseInProgress():
1469 syncbuf.fail(self, _PriorSyncFailedError()) 1545 fail(_PriorSyncFailedError(project=self.name))
1470 return 1546 return
1471 1547
1472 if head == revid: 1548 if head == revid:
@@ -1486,7 +1562,7 @@ class Project(object):
1486 if submodules: 1562 if submodules:
1487 self._SyncSubmodules(quiet=True) 1563 self._SyncSubmodules(quiet=True)
1488 except GitError as e: 1564 except GitError as e:
1489 syncbuf.fail(self, e) 1565 fail(e)
1490 return 1566 return
1491 self._CopyAndLinkFiles() 1567 self._CopyAndLinkFiles()
1492 return 1568 return
@@ -1511,7 +1587,7 @@ class Project(object):
1511 if submodules: 1587 if submodules:
1512 self._SyncSubmodules(quiet=True) 1588 self._SyncSubmodules(quiet=True)
1513 except GitError as e: 1589 except GitError as e:
1514 syncbuf.fail(self, e) 1590 fail(e)
1515 return 1591 return
1516 self._CopyAndLinkFiles() 1592 self._CopyAndLinkFiles()
1517 return 1593 return
@@ -1534,10 +1610,13 @@ class Project(object):
1534 # The user has published this branch and some of those 1610 # The user has published this branch and some of those
1535 # commits are not yet merged upstream. We do not want 1611 # commits are not yet merged upstream. We do not want
1536 # to rewrite the published commits so we punt. 1612 # to rewrite the published commits so we punt.
1537 syncbuf.fail( 1613 fail(
1538 self, 1614 LocalSyncFail(
1539 "branch %s is published (but not merged) and is now " 1615 "branch %s is published (but not merged) and is "
1540 "%d commits behind" % (branch.name, len(upstream_gain)), 1616 "now %d commits behind"
1617 % (branch.name, len(upstream_gain)),
1618 project=self.name,
1619 )
1541 ) 1620 )
1542 return 1621 return
1543 elif pub == head: 1622 elif pub == head:
@@ -1565,7 +1644,7 @@ class Project(object):
1565 return 1644 return
1566 1645
1567 if self.IsDirty(consider_untracked=False): 1646 if self.IsDirty(consider_untracked=False):
1568 syncbuf.fail(self, _DirtyError()) 1647 fail(_DirtyError(project=self.name))
1569 return 1648 return
1570 1649
1571 # If the upstream switched on us, warn the user. 1650 # If the upstream switched on us, warn the user.
@@ -1615,7 +1694,7 @@ class Project(object):
1615 self._SyncSubmodules(quiet=True) 1694 self._SyncSubmodules(quiet=True)
1616 self._CopyAndLinkFiles() 1695 self._CopyAndLinkFiles()
1617 except GitError as e: 1696 except GitError as e:
1618 syncbuf.fail(self, e) 1697 fail(e)
1619 return 1698 return
1620 else: 1699 else:
1621 syncbuf.later1(self, _doff) 1700 syncbuf.later1(self, _doff)
@@ -1687,12 +1766,12 @@ class Project(object):
1687 file=sys.stderr, 1766 file=sys.stderr,
1688 ) 1767 )
1689 else: 1768 else:
1690 print( 1769 msg = (
1691 "error: %s: Cannot remove project: uncommitted changes are " 1770 "error: %s: Cannot remove project: uncommitted"
1692 "present.\n" % (self.RelPath(local=False),), 1771 "changes are present.\n" % self.RelPath(local=False)
1693 file=sys.stderr,
1694 ) 1772 )
1695 return False 1773 print(msg, file=sys.stderr)
1774 raise DeleteDirtyWorktreeError(msg, project=self)
1696 1775
1697 if not quiet: 1776 if not quiet:
1698 print( 1777 print(
@@ -1745,12 +1824,13 @@ class Project(object):
1745 % (self.RelPath(local=False),), 1824 % (self.RelPath(local=False),),
1746 file=sys.stderr, 1825 file=sys.stderr,
1747 ) 1826 )
1748 return False 1827 raise DeleteWorktreeError(aggregate_errors=[e])
1749 1828
1750 # Delete everything under the worktree, except for directories that 1829 # Delete everything under the worktree, except for directories that
1751 # contain another git project. 1830 # contain another git project.
1752 dirs_to_remove = [] 1831 dirs_to_remove = []
1753 failed = False 1832 failed = False
1833 errors = []
1754 for root, dirs, files in platform_utils.walk(self.worktree): 1834 for root, dirs, files in platform_utils.walk(self.worktree):
1755 for f in files: 1835 for f in files:
1756 path = os.path.join(root, f) 1836 path = os.path.join(root, f)
@@ -1763,6 +1843,7 @@ class Project(object):
1763 file=sys.stderr, 1843 file=sys.stderr,
1764 ) 1844 )
1765 failed = True 1845 failed = True
1846 errors.append(e)
1766 dirs[:] = [ 1847 dirs[:] = [
1767 d 1848 d
1768 for d in dirs 1849 for d in dirs
@@ -1784,6 +1865,7 @@ class Project(object):
1784 file=sys.stderr, 1865 file=sys.stderr,
1785 ) 1866 )
1786 failed = True 1867 failed = True
1868 errors.append(e)
1787 elif not platform_utils.listdir(d): 1869 elif not platform_utils.listdir(d):
1788 try: 1870 try:
1789 platform_utils.rmdir(d) 1871 platform_utils.rmdir(d)
@@ -1794,6 +1876,7 @@ class Project(object):
1794 file=sys.stderr, 1876 file=sys.stderr,
1795 ) 1877 )
1796 failed = True 1878 failed = True
1879 errors.append(e)
1797 if failed: 1880 if failed:
1798 print( 1881 print(
1799 "error: %s: Failed to delete obsolete checkout." 1882 "error: %s: Failed to delete obsolete checkout."
@@ -1804,7 +1887,7 @@ class Project(object):
1804 " Remove manually, then run `repo sync -l`.", 1887 " Remove manually, then run `repo sync -l`.",
1805 file=sys.stderr, 1888 file=sys.stderr,
1806 ) 1889 )
1807 return False 1890 raise DeleteWorktreeError(aggregate_errors=errors)
1808 1891
1809 # Try deleting parent dirs if they are empty. 1892 # Try deleting parent dirs if they are empty.
1810 path = self.worktree 1893 path = self.worktree
@@ -2264,11 +2347,14 @@ class Project(object):
2264 cmd.append(self.revisionExpr) 2347 cmd.append(self.revisionExpr)
2265 2348
2266 command = GitCommand( 2349 command = GitCommand(
2267 self, cmd, cwd=cwd, capture_stdout=True, capture_stderr=True 2350 self,
2351 cmd,
2352 cwd=cwd,
2353 capture_stdout=True,
2354 capture_stderr=True,
2355 verify_command=True,
2268 ) 2356 )
2269 2357 command.Wait()
2270 if command.Wait() != 0:
2271 raise GitError("git archive %s: %s" % (self.name, command.stderr))
2272 2358
2273 def _RemoteFetch( 2359 def _RemoteFetch(
2274 self, 2360 self,
@@ -2289,7 +2375,7 @@ class Project(object):
2289 retry_fetches=2, 2375 retry_fetches=2,
2290 retry_sleep_initial_sec=4.0, 2376 retry_sleep_initial_sec=4.0,
2291 retry_exp_factor=2.0, 2377 retry_exp_factor=2.0,
2292 ): 2378 ) -> bool:
2293 is_sha1 = False 2379 is_sha1 = False
2294 tag_name = None 2380 tag_name = None
2295 # The depth should not be used when fetching to a mirror because 2381 # The depth should not be used when fetching to a mirror because
@@ -2473,6 +2559,7 @@ class Project(object):
2473 retry_cur_sleep = retry_sleep_initial_sec 2559 retry_cur_sleep = retry_sleep_initial_sec
2474 ok = prune_tried = False 2560 ok = prune_tried = False
2475 for try_n in range(retry_fetches): 2561 for try_n in range(retry_fetches):
2562 verify_command = try_n == retry_fetches - 1
2476 gitcmd = GitCommand( 2563 gitcmd = GitCommand(
2477 self, 2564 self,
2478 cmd, 2565 cmd,
@@ -2481,6 +2568,7 @@ class Project(object):
2481 ssh_proxy=ssh_proxy, 2568 ssh_proxy=ssh_proxy,
2482 merge_output=True, 2569 merge_output=True,
2483 capture_stdout=quiet or bool(output_redir), 2570 capture_stdout=quiet or bool(output_redir),
2571 verify_command=verify_command,
2484 ) 2572 )
2485 if gitcmd.stdout and not quiet and output_redir: 2573 if gitcmd.stdout and not quiet and output_redir:
2486 output_redir.write(gitcmd.stdout) 2574 output_redir.write(gitcmd.stdout)
@@ -2732,7 +2820,9 @@ class Project(object):
2732 cmd.append("--") 2820 cmd.append("--")
2733 if GitCommand(self, cmd).Wait() != 0: 2821 if GitCommand(self, cmd).Wait() != 0:
2734 if self._allrefs: 2822 if self._allrefs:
2735 raise GitError("%s checkout %s " % (self.name, rev)) 2823 raise GitError(
2824 "%s checkout %s " % (self.name, rev), project=self.name
2825 )
2736 2826
2737 def _CherryPick(self, rev, ffonly=False, record_origin=False): 2827 def _CherryPick(self, rev, ffonly=False, record_origin=False):
2738 cmd = ["cherry-pick"] 2828 cmd = ["cherry-pick"]
@@ -2744,7 +2834,9 @@ class Project(object):
2744 cmd.append("--") 2834 cmd.append("--")
2745 if GitCommand(self, cmd).Wait() != 0: 2835 if GitCommand(self, cmd).Wait() != 0:
2746 if self._allrefs: 2836 if self._allrefs:
2747 raise GitError("%s cherry-pick %s " % (self.name, rev)) 2837 raise GitError(
2838 "%s cherry-pick %s " % (self.name, rev), project=self.name
2839 )
2748 2840
2749 def _LsRemote(self, refs): 2841 def _LsRemote(self, refs):
2750 cmd = ["ls-remote", self.remote.name, refs] 2842 cmd = ["ls-remote", self.remote.name, refs]
@@ -2760,7 +2852,9 @@ class Project(object):
2760 cmd.append("--") 2852 cmd.append("--")
2761 if GitCommand(self, cmd).Wait() != 0: 2853 if GitCommand(self, cmd).Wait() != 0:
2762 if self._allrefs: 2854 if self._allrefs:
2763 raise GitError("%s revert %s " % (self.name, rev)) 2855 raise GitError(
2856 "%s revert %s " % (self.name, rev), project=self.name
2857 )
2764 2858
2765 def _ResetHard(self, rev, quiet=True): 2859 def _ResetHard(self, rev, quiet=True):
2766 cmd = ["reset", "--hard"] 2860 cmd = ["reset", "--hard"]
@@ -2768,7 +2862,9 @@ class Project(object):
2768 cmd.append("-q") 2862 cmd.append("-q")
2769 cmd.append(rev) 2863 cmd.append(rev)
2770 if GitCommand(self, cmd).Wait() != 0: 2864 if GitCommand(self, cmd).Wait() != 0:
2771 raise GitError("%s reset --hard %s " % (self.name, rev)) 2865 raise GitError(
2866 "%s reset --hard %s " % (self.name, rev), project=self.name
2867 )
2772 2868
2773 def _SyncSubmodules(self, quiet=True): 2869 def _SyncSubmodules(self, quiet=True):
2774 cmd = ["submodule", "update", "--init", "--recursive"] 2870 cmd = ["submodule", "update", "--init", "--recursive"]
@@ -2776,7 +2872,8 @@ class Project(object):
2776 cmd.append("-q") 2872 cmd.append("-q")
2777 if GitCommand(self, cmd).Wait() != 0: 2873 if GitCommand(self, cmd).Wait() != 0:
2778 raise GitError( 2874 raise GitError(
2779 "%s submodule update --init --recursive " % self.name 2875 "%s submodule update --init --recursive " % self.name,
2876 project=self.name,
2780 ) 2877 )
2781 2878
2782 def _Rebase(self, upstream, onto=None): 2879 def _Rebase(self, upstream, onto=None):
@@ -2785,14 +2882,18 @@ class Project(object):
2785 cmd.extend(["--onto", onto]) 2882 cmd.extend(["--onto", onto])
2786 cmd.append(upstream) 2883 cmd.append(upstream)
2787 if GitCommand(self, cmd).Wait() != 0: 2884 if GitCommand(self, cmd).Wait() != 0:
2788 raise GitError("%s rebase %s " % (self.name, upstream)) 2885 raise GitError(
2886 "%s rebase %s " % (self.name, upstream), project=self.name
2887 )
2789 2888
2790 def _FastForward(self, head, ffonly=False): 2889 def _FastForward(self, head, ffonly=False):
2791 cmd = ["merge", "--no-stat", head] 2890 cmd = ["merge", "--no-stat", head]
2792 if ffonly: 2891 if ffonly:
2793 cmd.append("--ff-only") 2892 cmd.append("--ff-only")
2794 if GitCommand(self, cmd).Wait() != 0: 2893 if GitCommand(self, cmd).Wait() != 0:
2795 raise GitError("%s merge %s " % (self.name, head)) 2894 raise GitError(
2895 "%s merge %s " % (self.name, head), project=self.name
2896 )
2796 2897
2797 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False): 2898 def _InitGitDir(self, mirror_git=None, force_sync=False, quiet=False):
2798 init_git_dir = not os.path.exists(self.gitdir) 2899 init_git_dir = not os.path.exists(self.gitdir)
@@ -2964,7 +3065,9 @@ class Project(object):
2964 try: 3065 try:
2965 os.link(stock_hook, dst) 3066 os.link(stock_hook, dst)
2966 except OSError: 3067 except OSError:
2967 raise GitError(self._get_symlink_error_message()) 3068 raise GitError(
3069 self._get_symlink_error_message(), project=self.name
3070 )
2968 else: 3071 else:
2969 raise 3072 raise
2970 3073
@@ -3065,7 +3168,8 @@ class Project(object):
3065 "work tree. If you're comfortable with the " 3168 "work tree. If you're comfortable with the "
3066 "possibility of losing the work tree's git metadata," 3169 "possibility of losing the work tree's git metadata,"
3067 " use `repo sync --force-sync {0}` to " 3170 " use `repo sync --force-sync {0}` to "
3068 "proceed.".format(self.RelPath(local=False)) 3171 "proceed.".format(self.RelPath(local=False)),
3172 project=self.name,
3069 ) 3173 )
3070 3174
3071 def _ReferenceGitDir(self, gitdir, dotgit, copy_all): 3175 def _ReferenceGitDir(self, gitdir, dotgit, copy_all):
@@ -3175,7 +3279,7 @@ class Project(object):
3175 3279
3176 # If using an old layout style (a directory), migrate it. 3280 # If using an old layout style (a directory), migrate it.
3177 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit): 3281 if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
3178 self._MigrateOldWorkTreeGitDir(dotgit) 3282 self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
3179 3283
3180 init_dotgit = not os.path.exists(dotgit) 3284 init_dotgit = not os.path.exists(dotgit)
3181 if self.use_git_worktrees: 3285 if self.use_git_worktrees:
@@ -3205,7 +3309,8 @@ class Project(object):
3205 cmd = ["read-tree", "--reset", "-u", "-v", HEAD] 3309 cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
3206 if GitCommand(self, cmd).Wait() != 0: 3310 if GitCommand(self, cmd).Wait() != 0:
3207 raise GitError( 3311 raise GitError(
3208 "Cannot initialize work tree for " + self.name 3312 "Cannot initialize work tree for " + self.name,
3313 project=self.name,
3209 ) 3314 )
3210 3315
3211 if submodules: 3316 if submodules:
@@ -3213,7 +3318,7 @@ class Project(object):
3213 self._CopyAndLinkFiles() 3318 self._CopyAndLinkFiles()
3214 3319
3215 @classmethod 3320 @classmethod
3216 def _MigrateOldWorkTreeGitDir(cls, dotgit): 3321 def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
3217 """Migrate the old worktree .git/ dir style to a symlink. 3322 """Migrate the old worktree .git/ dir style to a symlink.
3218 3323
3219 This logic specifically only uses state from |dotgit| to figure out 3324 This logic specifically only uses state from |dotgit| to figure out
@@ -3223,7 +3328,9 @@ class Project(object):
3223 """ 3328 """
3224 # Figure out where in .repo/projects/ it's pointing to. 3329 # Figure out where in .repo/projects/ it's pointing to.
3225 if not os.path.islink(os.path.join(dotgit, "refs")): 3330 if not os.path.islink(os.path.join(dotgit, "refs")):
3226 raise GitError(f"{dotgit}: unsupported checkout state") 3331 raise GitError(
3332 f"{dotgit}: unsupported checkout state", project=project
3333 )
3227 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs"))) 3334 gitdir = os.path.dirname(os.path.realpath(os.path.join(dotgit, "refs")))
3228 3335
3229 # Remove known symlink paths that exist in .repo/projects/. 3336 # Remove known symlink paths that exist in .repo/projects/.
@@ -3271,7 +3378,10 @@ class Project(object):
3271 f"{dotgit_path}: unknown file; please file a bug" 3378 f"{dotgit_path}: unknown file; please file a bug"
3272 ) 3379 )
3273 if unknown_paths: 3380 if unknown_paths:
3274 raise GitError("Aborting migration: " + "\n".join(unknown_paths)) 3381 raise GitError(
3382 "Aborting migration: " + "\n".join(unknown_paths),
3383 project=project,
3384 )
3275 3385
3276 # Now walk the paths and sync the .git/ to .repo/projects/. 3386 # Now walk the paths and sync the .git/ to .repo/projects/.
3277 for name in platform_utils.listdir(dotgit): 3387 for name in platform_utils.listdir(dotgit):
@@ -3537,12 +3647,9 @@ class Project(object):
3537 gitdir=self._gitdir, 3647 gitdir=self._gitdir,
3538 capture_stdout=True, 3648 capture_stdout=True,
3539 capture_stderr=True, 3649 capture_stderr=True,
3650 verify_command=True,
3540 ) 3651 )
3541 if p.Wait() != 0: 3652 p.Wait()
3542 raise GitError(
3543 "%s rev-list %s: %s"
3544 % (self._project.name, str(args), p.stderr)
3545 )
3546 return p.stdout.splitlines() 3653 return p.stdout.splitlines()
3547 3654
3548 def __getattr__(self, name): 3655 def __getattr__(self, name):
@@ -3588,11 +3695,9 @@ class Project(object):
3588 gitdir=self._gitdir, 3695 gitdir=self._gitdir,
3589 capture_stdout=True, 3696 capture_stdout=True,
3590 capture_stderr=True, 3697 capture_stderr=True,
3698 verify_command=True,
3591 ) 3699 )
3592 if p.Wait() != 0: 3700 p.Wait()
3593 raise GitError(
3594 "%s %s: %s" % (self._project.name, name, p.stderr)
3595 )
3596 r = p.stdout 3701 r = p.stdout
3597 if r.endswith("\n") and r.index("\n") == len(r) - 1: 3702 if r.endswith("\n") and r.index("\n") == len(r) - 1:
3598 return r[:-1] 3703 return r[:-1]
@@ -3601,12 +3706,16 @@ class Project(object):
3601 return runner 3706 return runner
3602 3707
3603 3708
3604class _PriorSyncFailedError(Exception): 3709class LocalSyncFail(RepoError):
3710 """Default error when there is an Sync_LocalHalf error."""
3711
3712
3713class _PriorSyncFailedError(LocalSyncFail):
3605 def __str__(self): 3714 def __str__(self):
3606 return "prior sync failed; rebase still in progress" 3715 return "prior sync failed; rebase still in progress"
3607 3716
3608 3717
3609class _DirtyError(Exception): 3718class _DirtyError(LocalSyncFail):
3610 def __str__(self): 3719 def __str__(self):
3611 return "contains uncommitted changes" 3720 return "contains uncommitted changes"
3612 3721
diff --git a/subcmds/download.py b/subcmds/download.py
index d81d1f8c..475c0bc2 100644
--- a/subcmds/download.py
+++ b/subcmds/download.py
@@ -118,7 +118,7 @@ If no project is specified try to use current directory as a project.
118 ), 118 ),
119 file=sys.stderr, 119 file=sys.stderr,
120 ) 120 )
121 sys.exit(1) 121 raise NoSuchProjectError()
122 else: 122 else:
123 project = projects[0] 123 project = projects[0]
124 print("Defaulting to cwd project", project.name) 124 print("Defaulting to cwd project", project.name)
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 5f8bc2f0..eaca50c9 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -63,9 +63,16 @@ from command import (
63 MirrorSafeCommand, 63 MirrorSafeCommand,
64 WORKER_BATCH_SIZE, 64 WORKER_BATCH_SIZE,
65) 65)
66from error import RepoChangedException, GitError 66from error import (
67 RepoChangedException,
68 GitError,
69 RepoExitError,
70 SyncError,
71 UpdateManifestError,
72 RepoUnhandledExceptionError,
73)
67import platform_utils 74import platform_utils
68from project import SyncBuffer 75from project import SyncBuffer, DeleteWorktreeError
69from progress import Progress, elapsed_str, jobs_str 76from progress import Progress, elapsed_str, jobs_str
70from repo_trace import Trace 77from repo_trace import Trace
71import ssh 78import ssh
@@ -94,6 +101,7 @@ class _FetchOneResult(NamedTuple):
94 """ 101 """
95 102
96 success: bool 103 success: bool
104 errors: List[Exception]
97 project: Project 105 project: Project
98 start: float 106 start: float
99 finish: float 107 finish: float
@@ -110,6 +118,7 @@ class _FetchResult(NamedTuple):
110 118
111 success: bool 119 success: bool
112 projects: Set[str] 120 projects: Set[str]
121 errors: List[Exception]
113 122
114 123
115class _FetchMainResult(NamedTuple): 124class _FetchMainResult(NamedTuple):
@@ -120,6 +129,7 @@ class _FetchMainResult(NamedTuple):
120 """ 129 """
121 130
122 all_projects: List[Project] 131 all_projects: List[Project]
132 errors: List[Exception]
123 133
124 134
125class _CheckoutOneResult(NamedTuple): 135class _CheckoutOneResult(NamedTuple):
@@ -133,11 +143,24 @@ class _CheckoutOneResult(NamedTuple):
133 """ 143 """
134 144
135 success: bool 145 success: bool
146 errors: List[Exception]
136 project: Project 147 project: Project
137 start: float 148 start: float
138 finish: float 149 finish: float
139 150
140 151
152class SuperprojectError(SyncError):
153 """Superproject sync repo."""
154
155
156class SyncFailFastError(SyncError):
157 """Sync exit error when --fail-fast set."""
158
159
160class SmartSyncError(SyncError):
161 """Smart sync exit error."""
162
163
141class Sync(Command, MirrorSafeCommand): 164class Sync(Command, MirrorSafeCommand):
142 COMMON = True 165 COMMON = True
143 MULTI_MANIFEST_SUPPORT = True 166 MULTI_MANIFEST_SUPPORT = True
@@ -588,7 +611,7 @@ later is required to fix a server side protocol bug.
588 file=sys.stderr, 611 file=sys.stderr,
589 ) 612 )
590 if update_result.fatal and opt.use_superproject is not None: 613 if update_result.fatal and opt.use_superproject is not None:
591 sys.exit(1) 614 raise SuperprojectError()
592 if need_unload: 615 if need_unload:
593 m.outer_client.manifest.Unload() 616 m.outer_client.manifest.Unload()
594 617
@@ -621,6 +644,7 @@ later is required to fix a server side protocol bug.
621 self._sync_dict[k] = start 644 self._sync_dict[k] = start
622 success = False 645 success = False
623 remote_fetched = False 646 remote_fetched = False
647 errors = []
624 buf = io.StringIO() 648 buf = io.StringIO()
625 try: 649 try:
626 sync_result = project.Sync_NetworkHalf( 650 sync_result = project.Sync_NetworkHalf(
@@ -644,6 +668,8 @@ later is required to fix a server side protocol bug.
644 ) 668 )
645 success = sync_result.success 669 success = sync_result.success
646 remote_fetched = sync_result.remote_fetched 670 remote_fetched = sync_result.remote_fetched
671 if sync_result.error:
672 errors.append(sync_result.error)
647 673
648 output = buf.getvalue() 674 output = buf.getvalue()
649 if (opt.verbose or not success) and output: 675 if (opt.verbose or not success) and output:
@@ -659,6 +685,7 @@ later is required to fix a server side protocol bug.
659 print(f"Keyboard interrupt while processing {project.name}") 685 print(f"Keyboard interrupt while processing {project.name}")
660 except GitError as e: 686 except GitError as e:
661 print("error.GitError: Cannot fetch %s" % str(e), file=sys.stderr) 687 print("error.GitError: Cannot fetch %s" % str(e), file=sys.stderr)
688 errors.append(e)
662 except Exception as e: 689 except Exception as e:
663 print( 690 print(
664 "error: Cannot fetch %s (%s: %s)" 691 "error: Cannot fetch %s (%s: %s)"
@@ -666,11 +693,14 @@ later is required to fix a server side protocol bug.
666 file=sys.stderr, 693 file=sys.stderr,
667 ) 694 )
668 del self._sync_dict[k] 695 del self._sync_dict[k]
696 errors.append(e)
669 raise 697 raise
670 698
671 finish = time.time() 699 finish = time.time()
672 del self._sync_dict[k] 700 del self._sync_dict[k]
673 return _FetchOneResult(success, project, start, finish, remote_fetched) 701 return _FetchOneResult(
702 success, errors, project, start, finish, remote_fetched
703 )
674 704
675 @classmethod 705 @classmethod
676 def _FetchInitChild(cls, ssh_proxy): 706 def _FetchInitChild(cls, ssh_proxy):
@@ -701,6 +731,7 @@ later is required to fix a server side protocol bug.
701 jobs = opt.jobs_network 731 jobs = opt.jobs_network
702 fetched = set() 732 fetched = set()
703 remote_fetched = set() 733 remote_fetched = set()
734 errors = []
704 pm = Progress( 735 pm = Progress(
705 "Fetching", 736 "Fetching",
706 len(projects), 737 len(projects),
@@ -745,6 +776,8 @@ later is required to fix a server side protocol bug.
745 finish, 776 finish,
746 success, 777 success,
747 ) 778 )
779 if result.errors:
780 errors.extend(result.errors)
748 if result.remote_fetched: 781 if result.remote_fetched:
749 remote_fetched.add(project) 782 remote_fetched.add(project)
750 # Check for any errors before running any more tasks. 783 # Check for any errors before running any more tasks.
@@ -813,7 +846,7 @@ later is required to fix a server side protocol bug.
813 if not self.outer_client.manifest.IsArchive: 846 if not self.outer_client.manifest.IsArchive:
814 self._GCProjects(projects, opt, err_event) 847 self._GCProjects(projects, opt, err_event)
815 848
816 return _FetchResult(ret, fetched) 849 return _FetchResult(ret, fetched, errors)
817 850
818 def _FetchMain( 851 def _FetchMain(
819 self, opt, args, all_projects, err_event, ssh_proxy, manifest 852 self, opt, args, all_projects, err_event, ssh_proxy, manifest
@@ -832,6 +865,7 @@ later is required to fix a server side protocol bug.
832 List of all projects that should be checked out. 865 List of all projects that should be checked out.
833 """ 866 """
834 rp = manifest.repoProject 867 rp = manifest.repoProject
868 errors = []
835 869
836 to_fetch = [] 870 to_fetch = []
837 now = time.time() 871 now = time.time()
@@ -843,6 +877,9 @@ later is required to fix a server side protocol bug.
843 result = self._Fetch(to_fetch, opt, err_event, ssh_proxy) 877 result = self._Fetch(to_fetch, opt, err_event, ssh_proxy)
844 success = result.success 878 success = result.success
845 fetched = result.projects 879 fetched = result.projects
880 if result.errors:
881 errors.extend(result.errors)
882
846 if not success: 883 if not success:
847 err_event.set() 884 err_event.set()
848 885
@@ -854,8 +891,11 @@ later is required to fix a server side protocol bug.
854 "\nerror: Exited sync due to fetch errors.\n", 891 "\nerror: Exited sync due to fetch errors.\n",
855 file=sys.stderr, 892 file=sys.stderr,
856 ) 893 )
857 sys.exit(1) 894 raise SyncError(
858 return _FetchMainResult([]) 895 "error: Exited sync due to fetch errors.",
896 aggregate_errors=errors,
897 )
898 return _FetchMainResult([], errors)
859 899
860 # Iteratively fetch missing and/or nested unregistered submodules. 900 # Iteratively fetch missing and/or nested unregistered submodules.
861 previously_missing_set = set() 901 previously_missing_set = set()
@@ -883,11 +923,13 @@ later is required to fix a server side protocol bug.
883 result = self._Fetch(missing, opt, err_event, ssh_proxy) 923 result = self._Fetch(missing, opt, err_event, ssh_proxy)
884 success = result.success 924 success = result.success
885 new_fetched = result.projects 925 new_fetched = result.projects
926 if result.errors:
927 errors.extend(result.errors)
886 if not success: 928 if not success:
887 err_event.set() 929 err_event.set()
888 fetched.update(new_fetched) 930 fetched.update(new_fetched)
889 931
890 return _FetchMainResult(all_projects) 932 return _FetchMainResult(all_projects, errors)
891 933
892 def _CheckoutOne(self, detach_head, force_sync, project): 934 def _CheckoutOne(self, detach_head, force_sync, project):
893 """Checkout work tree for one project 935 """Checkout work tree for one project
@@ -905,8 +947,11 @@ later is required to fix a server side protocol bug.
905 project.manifest.manifestProject.config, detach_head=detach_head 947 project.manifest.manifestProject.config, detach_head=detach_head
906 ) 948 )
907 success = False 949 success = False
950 errors = []
908 try: 951 try:
909 project.Sync_LocalHalf(syncbuf, force_sync=force_sync) 952 project.Sync_LocalHalf(
953 syncbuf, force_sync=force_sync, errors=errors
954 )
910 success = syncbuf.Finish() 955 success = syncbuf.Finish()
911 except GitError as e: 956 except GitError as e:
912 print( 957 print(
@@ -914,6 +959,7 @@ later is required to fix a server side protocol bug.
914 % (project.name, str(e)), 959 % (project.name, str(e)),
915 file=sys.stderr, 960 file=sys.stderr,
916 ) 961 )
962 errors.append(e)
917 except Exception as e: 963 except Exception as e:
918 print( 964 print(
919 "error: Cannot checkout %s: %s: %s" 965 "error: Cannot checkout %s: %s: %s"
@@ -925,9 +971,9 @@ later is required to fix a server side protocol bug.
925 if not success: 971 if not success:
926 print("error: Cannot checkout %s" % (project.name), file=sys.stderr) 972 print("error: Cannot checkout %s" % (project.name), file=sys.stderr)
927 finish = time.time() 973 finish = time.time()
928 return _CheckoutOneResult(success, project, start, finish) 974 return _CheckoutOneResult(success, errors, project, start, finish)
929 975
930 def _Checkout(self, all_projects, opt, err_results): 976 def _Checkout(self, all_projects, opt, err_results, checkout_errors):
931 """Checkout projects listed in all_projects 977 """Checkout projects listed in all_projects
932 978
933 Args: 979 Args:
@@ -949,6 +995,10 @@ later is required to fix a server side protocol bug.
949 self.event_log.AddSync( 995 self.event_log.AddSync(
950 project, event_log.TASK_SYNC_LOCAL, start, finish, success 996 project, event_log.TASK_SYNC_LOCAL, start, finish, success
951 ) 997 )
998
999 if result.errors:
1000 checkout_errors.extend(result.errors)
1001
952 # Check for any errors before running any more tasks. 1002 # Check for any errors before running any more tasks.
953 # ...we'll let existing jobs finish, though. 1003 # ...we'll let existing jobs finish, though.
954 if success: 1004 if success:
@@ -1214,10 +1264,9 @@ later is required to fix a server side protocol bug.
1214 revisionId=None, 1264 revisionId=None,
1215 groups=None, 1265 groups=None,
1216 ) 1266 )
1217 if not project.DeleteWorktree( 1267 project.DeleteWorktree(
1218 quiet=opt.quiet, force=opt.force_remove_dirty 1268 quiet=opt.quiet, force=opt.force_remove_dirty
1219 ): 1269 )
1220 return 1
1221 1270
1222 new_project_paths.sort() 1271 new_project_paths.sort()
1223 with open(file_path, "w") as fd: 1272 with open(file_path, "w") as fd:
@@ -1260,7 +1309,7 @@ later is required to fix a server side protocol bug.
1260 file=sys.stderr, 1309 file=sys.stderr,
1261 ) 1310 )
1262 platform_utils.remove(copylinkfile_path) 1311 platform_utils.remove(copylinkfile_path)
1263 return False 1312 raise
1264 1313
1265 need_remove_files = [] 1314 need_remove_files = []
1266 need_remove_files.extend( 1315 need_remove_files.extend(
@@ -1285,12 +1334,10 @@ later is required to fix a server side protocol bug.
1285 1334
1286 def _SmartSyncSetup(self, opt, smart_sync_manifest_path, manifest): 1335 def _SmartSyncSetup(self, opt, smart_sync_manifest_path, manifest):
1287 if not manifest.manifest_server: 1336 if not manifest.manifest_server:
1288 print( 1337 raise SmartSyncError(
1289 "error: cannot smart sync: no manifest server defined in " 1338 "error: cannot smart sync: no manifest server defined in "
1290 "manifest", 1339 "manifest"
1291 file=sys.stderr,
1292 ) 1340 )
1293 sys.exit(1)
1294 1341
1295 manifest_server = manifest.manifest_server 1342 manifest_server = manifest.manifest_server
1296 if not opt.quiet: 1343 if not opt.quiet:
@@ -1368,33 +1415,28 @@ later is required to fix a server side protocol bug.
1368 with open(smart_sync_manifest_path, "w") as f: 1415 with open(smart_sync_manifest_path, "w") as f:
1369 f.write(manifest_str) 1416 f.write(manifest_str)
1370 except IOError as e: 1417 except IOError as e:
1371 print( 1418 raise SmartSyncError(
1372 "error: cannot write manifest to %s:\n%s" 1419 "error: cannot write manifest to %s:\n%s"
1373 % (smart_sync_manifest_path, e), 1420 % (smart_sync_manifest_path, e),
1374 file=sys.stderr, 1421 aggregate_errors=[e],
1375 ) 1422 )
1376 sys.exit(1)
1377 self._ReloadManifest(manifest_name, manifest) 1423 self._ReloadManifest(manifest_name, manifest)
1378 else: 1424 else:
1379 print( 1425 raise SmartSyncError(
1380 "error: manifest server RPC call failed: %s" % manifest_str, 1426 "error: manifest server RPC call failed: %s" % manifest_str
1381 file=sys.stderr,
1382 ) 1427 )
1383 sys.exit(1)
1384 except (socket.error, IOError, xmlrpc.client.Fault) as e: 1428 except (socket.error, IOError, xmlrpc.client.Fault) as e:
1385 print( 1429 raise SmartSyncError(
1386 "error: cannot connect to manifest server %s:\n%s" 1430 "error: cannot connect to manifest server %s:\n%s"
1387 % (manifest.manifest_server, e), 1431 % (manifest.manifest_server, e),
1388 file=sys.stderr, 1432 aggregate_errors=[e],
1389 ) 1433 )
1390 sys.exit(1)
1391 except xmlrpc.client.ProtocolError as e: 1434 except xmlrpc.client.ProtocolError as e:
1392 print( 1435 raise SmartSyncError(
1393 "error: cannot connect to manifest server %s:\n%d %s" 1436 "error: cannot connect to manifest server %s:\n%d %s"
1394 % (manifest.manifest_server, e.errcode, e.errmsg), 1437 % (manifest.manifest_server, e.errcode, e.errmsg),
1395 file=sys.stderr, 1438 aggregate_errors=[e],
1396 ) 1439 )
1397 sys.exit(1)
1398 1440
1399 return manifest_name 1441 return manifest_name
1400 1442
@@ -1436,7 +1478,7 @@ later is required to fix a server side protocol bug.
1436 """ 1478 """
1437 if not opt.local_only: 1479 if not opt.local_only:
1438 start = time.time() 1480 start = time.time()
1439 success = mp.Sync_NetworkHalf( 1481 result = mp.Sync_NetworkHalf(
1440 quiet=opt.quiet, 1482 quiet=opt.quiet,
1441 verbose=opt.verbose, 1483 verbose=opt.verbose,
1442 current_branch_only=self._GetCurrentBranchOnly( 1484 current_branch_only=self._GetCurrentBranchOnly(
@@ -1453,19 +1495,24 @@ later is required to fix a server side protocol bug.
1453 ) 1495 )
1454 finish = time.time() 1496 finish = time.time()
1455 self.event_log.AddSync( 1497 self.event_log.AddSync(
1456 mp, event_log.TASK_SYNC_NETWORK, start, finish, success 1498 mp, event_log.TASK_SYNC_NETWORK, start, finish, result.success
1457 ) 1499 )
1458 1500
1459 if mp.HasChanges: 1501 if mp.HasChanges:
1502 errors = []
1460 syncbuf = SyncBuffer(mp.config) 1503 syncbuf = SyncBuffer(mp.config)
1461 start = time.time() 1504 start = time.time()
1462 mp.Sync_LocalHalf(syncbuf, submodules=mp.manifest.HasSubmodules) 1505 mp.Sync_LocalHalf(
1506 syncbuf, submodules=mp.manifest.HasSubmodules, errors=errors
1507 )
1463 clean = syncbuf.Finish() 1508 clean = syncbuf.Finish()
1464 self.event_log.AddSync( 1509 self.event_log.AddSync(
1465 mp, event_log.TASK_SYNC_LOCAL, start, time.time(), clean 1510 mp, event_log.TASK_SYNC_LOCAL, start, time.time(), clean
1466 ) 1511 )
1467 if not clean: 1512 if not clean:
1468 sys.exit(1) 1513 raise UpdateManifestError(
1514 aggregate_errors=errors, project=mp.name
1515 )
1469 self._ReloadManifest(manifest_name, mp.manifest) 1516 self._ReloadManifest(manifest_name, mp.manifest)
1470 1517
1471 def ValidateOptions(self, opt, args): 1518 def ValidateOptions(self, opt, args):
@@ -1546,6 +1593,15 @@ later is required to fix a server side protocol bug.
1546 opt.jobs_checkout = min(opt.jobs_checkout, jobs_soft_limit) 1593 opt.jobs_checkout = min(opt.jobs_checkout, jobs_soft_limit)
1547 1594
1548 def Execute(self, opt, args): 1595 def Execute(self, opt, args):
1596 errors = []
1597 try:
1598 self._ExecuteHelper(opt, args, errors)
1599 except RepoExitError:
1600 raise
1601 except (KeyboardInterrupt, Exception) as e:
1602 raise RepoUnhandledExceptionError(e, aggregate_errors=errors)
1603
1604 def _ExecuteHelper(self, opt, args, errors):
1549 manifest = self.outer_manifest 1605 manifest = self.outer_manifest
1550 if not opt.outer_manifest: 1606 if not opt.outer_manifest:
1551 manifest = self.manifest 1607 manifest = self.manifest
@@ -1695,6 +1751,8 @@ later is required to fix a server side protocol bug.
1695 result = self._FetchMain( 1751 result = self._FetchMain(
1696 opt, args, all_projects, err_event, ssh_proxy, manifest 1752 opt, args, all_projects, err_event, ssh_proxy, manifest
1697 ) 1753 )
1754 if result.errors:
1755 errors.extend(result.errors)
1698 all_projects = result.all_projects 1756 all_projects = result.all_projects
1699 1757
1700 if opt.network_only: 1758 if opt.network_only:
@@ -1712,36 +1770,47 @@ later is required to fix a server side protocol bug.
1712 "`repo sync -l` will update some local checkouts.", 1770 "`repo sync -l` will update some local checkouts.",
1713 file=sys.stderr, 1771 file=sys.stderr,
1714 ) 1772 )
1715 sys.exit(1) 1773 raise SyncFailFastError(aggregate_errors=errors)
1716 1774
1717 for m in self.ManifestList(opt): 1775 for m in self.ManifestList(opt):
1718 if m.IsMirror or m.IsArchive: 1776 if m.IsMirror or m.IsArchive:
1719 # Bail out now, we have no working tree. 1777 # Bail out now, we have no working tree.
1720 continue 1778 continue
1721 1779
1722 if self.UpdateProjectList(opt, m): 1780 try:
1781 self.UpdateProjectList(opt, m)
1782 except Exception as e:
1723 err_event.set() 1783 err_event.set()
1724 err_update_projects = True 1784 err_update_projects = True
1785 errors.append(e)
1786 if isinstance(e, DeleteWorktreeError):
1787 errors.extend(e.aggregate_errors)
1725 if opt.fail_fast: 1788 if opt.fail_fast:
1726 print( 1789 print(
1727 "\nerror: Local checkouts *not* updated.", 1790 "\nerror: Local checkouts *not* updated.",
1728 file=sys.stderr, 1791 file=sys.stderr,
1729 ) 1792 )
1730 sys.exit(1) 1793 raise SyncFailFastError(aggregate_errors=errors)
1731 1794
1732 err_update_linkfiles = not self.UpdateCopyLinkfileList(m) 1795 err_update_linkfiles = False
1733 if err_update_linkfiles: 1796 try:
1797 self.UpdateCopyLinkfileList(m)
1798 except Exception as e:
1799 err_update_linkfiles = True
1800 errors.append(e)
1734 err_event.set() 1801 err_event.set()
1735 if opt.fail_fast: 1802 if opt.fail_fast:
1736 print( 1803 print(
1737 "\nerror: Local update copyfile or linkfile failed.", 1804 "\nerror: Local update copyfile or linkfile failed.",
1738 file=sys.stderr, 1805 file=sys.stderr,
1739 ) 1806 )
1740 sys.exit(1) 1807 raise SyncFailFastError(aggregate_errors=errors)
1741 1808
1742 err_results = [] 1809 err_results = []
1743 # NB: We don't exit here because this is the last step. 1810 # NB: We don't exit here because this is the last step.
1744 err_checkout = not self._Checkout(all_projects, opt, err_results) 1811 err_checkout = not self._Checkout(
1812 all_projects, opt, err_results, errors
1813 )
1745 if err_checkout: 1814 if err_checkout:
1746 err_event.set() 1815 err_event.set()
1747 1816
@@ -1784,7 +1853,7 @@ later is required to fix a server side protocol bug.
1784 "error.", 1853 "error.",
1785 file=sys.stderr, 1854 file=sys.stderr,
1786 ) 1855 )
1787 sys.exit(1) 1856 raise SyncError(aggregate_errors=errors)
1788 1857
1789 # Log the previous sync analysis state from the config. 1858 # Log the previous sync analysis state from the config.
1790 self.git_event_log.LogDataConfigEvents( 1859 self.git_event_log.LogDataConfigEvents(
@@ -1842,7 +1911,7 @@ def _PostRepoFetch(rp, repo_verify=True, verbose=False):
1842 try: 1911 try:
1843 rp.work_git.reset("--keep", new_rev) 1912 rp.work_git.reset("--keep", new_rev)
1844 except GitError as e: 1913 except GitError as e:
1845 sys.exit(str(e)) 1914 raise RepoUnhandledExceptionError(e)
1846 print("info: Restarting repo with latest version", file=sys.stderr) 1915 print("info: Restarting repo with latest version", file=sys.stderr)
1847 raise RepoChangedException(["--repo-upgraded"]) 1916 raise RepoChangedException(["--repo-upgraded"])
1848 else: 1917 else:
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py
index 057478ef..00c34852 100644
--- a/tests/test_subcmds_sync.py
+++ b/tests/test_subcmds_sync.py
@@ -17,12 +17,15 @@ import os
17import shutil 17import shutil
18import tempfile 18import tempfile
19import unittest 19import unittest
20import time
20from unittest import mock 21from unittest import mock
21 22
22import pytest 23import pytest
23 24
24import command 25import command
25from subcmds import sync 26from subcmds import sync
27from project import SyncNetworkHalfResult
28from error import GitError, RepoExitError
26 29
27 30
28@pytest.mark.parametrize( 31@pytest.mark.parametrize(
@@ -233,3 +236,83 @@ class GetPreciousObjectsState(unittest.TestCase):
233 self.assertFalse( 236 self.assertFalse(
234 self.cmd._GetPreciousObjectsState(self.project, self.opt) 237 self.cmd._GetPreciousObjectsState(self.project, self.opt)
235 ) 238 )
239
240
241class SyncCommand(unittest.TestCase):
242 """Tests for cmd.Execute."""
243
244 def setUp(self):
245 """Common setup."""
246 self.repodir = tempfile.mkdtemp(".repo")
247 self.manifest = manifest = mock.MagicMock(
248 repodir=self.repodir,
249 )
250
251 git_event_log = mock.MagicMock(ErrorEvent=mock.Mock(return_value=None))
252 self.outer_client = outer_client = mock.MagicMock()
253 outer_client.manifest.IsArchive = True
254 manifest.manifestProject.worktree = "worktree_path/"
255 manifest.repoProject.LastFetch = time.time()
256 self.sync_network_half_error = None
257 self.sync_local_half_error = None
258 self.cmd = sync.Sync(
259 manifest=manifest,
260 outer_client=outer_client,
261 git_event_log=git_event_log,
262 )
263
264 def Sync_NetworkHalf(*args, **kwargs):
265 return SyncNetworkHalfResult(True, self.sync_network_half_error)
266
267 def Sync_LocalHalf(*args, **kwargs):
268 if self.sync_local_half_error:
269 raise self.sync_local_half_error
270
271 self.project = p = mock.MagicMock(
272 use_git_worktrees=False,
273 UseAlternates=False,
274 name="project",
275 Sync_NetworkHalf=Sync_NetworkHalf,
276 Sync_LocalHalf=Sync_LocalHalf,
277 RelPath=mock.Mock(return_value="rel_path"),
278 )
279 p.manifest.GetProjectsWithName.return_value = [p]
280
281 mock.patch.object(
282 sync,
283 "_PostRepoFetch",
284 return_value=None,
285 ).start()
286
287 mock.patch.object(
288 self.cmd, "GetProjects", return_value=[self.project]
289 ).start()
290
291 opt, _ = self.cmd.OptionParser.parse_args([])
292 opt.clone_bundle = False
293 opt.jobs = 4
294 opt.quiet = True
295 opt.use_superproject = False
296 opt.current_branch_only = True
297 opt.optimized_fetch = True
298 opt.retry_fetches = 1
299 opt.prune = False
300 opt.auto_gc = False
301 opt.repo_verify = False
302 self.opt = opt
303
304 def tearDown(self):
305 mock.patch.stopall()
306
307 def test_command_exit_error(self):
308 """Ensure unsuccessful commands raise expected errors."""
309 self.sync_network_half_error = GitError(
310 "sync_network_half_error error", project=self.project
311 )
312 self.sync_local_half_error = GitError(
313 "sync_local_half_error", project=self.project
314 )
315 with self.assertRaises(RepoExitError) as e:
316 self.cmd.Execute(self.opt, [])
317 self.assertIn(self.sync_local_half_error, e.aggregate_errors)
318 self.assertIn(self.sync_network_half_error, e.aggregate_errors)