diff options
-rwxr-xr-x | main.py | 56 | ||||
-rw-r--r-- | project.py | 283 | ||||
-rw-r--r-- | subcmds/download.py | 2 | ||||
-rw-r--r-- | subcmds/sync.py | 159 | ||||
-rw-r--r-- | tests/test_subcmds_sync.py | 83 |
5 files changed, 441 insertions, 142 deletions
@@ -30,6 +30,7 @@ import sys | |||
30 | import textwrap | 30 | import textwrap |
31 | import time | 31 | import time |
32 | import urllib.request | 32 | import urllib.request |
33 | import json | ||
33 | 34 | ||
34 | try: | 35 | try: |
35 | import kerberos | 36 | import kerberos |
@@ -50,10 +51,12 @@ from editor import Editor | |||
50 | from error import DownloadError | 51 | from error import DownloadError |
51 | from error import InvalidProjectGroupsError | 52 | from error import InvalidProjectGroupsError |
52 | from error import ManifestInvalidRevisionError | 53 | from error import ManifestInvalidRevisionError |
53 | from error import ManifestParseError | ||
54 | from error import NoManifestException | 54 | from error import NoManifestException |
55 | from error import NoSuchProjectError | 55 | from error import NoSuchProjectError |
56 | from error import RepoChangedException | 56 | from error import RepoChangedException |
57 | from error import RepoExitError | ||
58 | from error import RepoUnhandledExceptionError | ||
59 | from error import RepoError | ||
57 | import gitc_utils | 60 | import gitc_utils |
58 | from manifest_xml import GitcClient, RepoClient | 61 | from manifest_xml import GitcClient, RepoClient |
59 | from pager import RunPager, TerminatePager | 62 | from pager import RunPager, TerminatePager |
@@ -97,6 +100,7 @@ else: | |||
97 | ) | 100 | ) |
98 | 101 | ||
99 | KEYBOARD_INTERRUPT_EXIT = 128 + signal.SIGINT | 102 | KEYBOARD_INTERRUPT_EXIT = 128 + signal.SIGINT |
103 | MAX_PRINT_ERRORS = 5 | ||
100 | 104 | ||
101 | global_options = optparse.OptionParser( | 105 | global_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 | # |
@@ -26,7 +26,7 @@ import sys | |||
26 | import tarfile | 26 | import tarfile |
27 | import tempfile | 27 | import tempfile |
28 | import time | 28 | import time |
29 | from typing import NamedTuple | 29 | from typing import NamedTuple, List |
30 | import urllib.parse | 30 | import urllib.parse |
31 | 31 | ||
32 | from color import Coloring | 32 | from color import Coloring |
@@ -41,7 +41,12 @@ from git_config import ( | |||
41 | ) | 41 | ) |
42 | import git_superproject | 42 | import git_superproject |
43 | from git_trace2_event_log import EventLog | 43 | from git_trace2_event_log import EventLog |
44 | from error import GitError, UploadError, DownloadError | 44 | from error import ( |
45 | GitError, | ||
46 | UploadError, | ||
47 | DownloadError, | ||
48 | RepoError, | ||
49 | ) | ||
45 | from error import ManifestInvalidRevisionError, ManifestInvalidPathError | 50 | from error import ManifestInvalidRevisionError, ManifestInvalidPathError |
46 | from error import NoManifestException, ManifestParseError | 51 | from error import NoManifestException, ManifestParseError |
47 | import platform_utils | 52 | import platform_utils |
@@ -54,11 +59,33 @@ from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M, R_WORKTREE_M | |||
54 | class SyncNetworkHalfResult(NamedTuple): | 59 | class 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 | |||
73 | class SyncNetworkHalfError(RepoError): | ||
74 | """Failure trying to sync.""" | ||
75 | |||
76 | |||
77 | class 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 | |||
87 | class 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 | ||
3604 | class _PriorSyncFailedError(Exception): | 3709 | class LocalSyncFail(RepoError): |
3710 | """Default error when there is an Sync_LocalHalf error.""" | ||
3711 | |||
3712 | |||
3713 | class _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 | ||
3609 | class _DirtyError(Exception): | 3718 | class _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 | ) |
66 | from error import RepoChangedException, GitError | 66 | from error import ( |
67 | RepoChangedException, | ||
68 | GitError, | ||
69 | RepoExitError, | ||
70 | SyncError, | ||
71 | UpdateManifestError, | ||
72 | RepoUnhandledExceptionError, | ||
73 | ) | ||
67 | import platform_utils | 74 | import platform_utils |
68 | from project import SyncBuffer | 75 | from project import SyncBuffer, DeleteWorktreeError |
69 | from progress import Progress, elapsed_str, jobs_str | 76 | from progress import Progress, elapsed_str, jobs_str |
70 | from repo_trace import Trace | 77 | from repo_trace import Trace |
71 | import ssh | 78 | import 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 | ||
115 | class _FetchMainResult(NamedTuple): | 124 | class _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 | ||
125 | class _CheckoutOneResult(NamedTuple): | 135 | class _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 | ||
152 | class SuperprojectError(SyncError): | ||
153 | """Superproject sync repo.""" | ||
154 | |||
155 | |||
156 | class SyncFailFastError(SyncError): | ||
157 | """Sync exit error when --fail-fast set.""" | ||
158 | |||
159 | |||
160 | class SmartSyncError(SyncError): | ||
161 | """Smart sync exit error.""" | ||
162 | |||
163 | |||
141 | class Sync(Command, MirrorSafeCommand): | 164 | class 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 | |||
17 | import shutil | 17 | import shutil |
18 | import tempfile | 18 | import tempfile |
19 | import unittest | 19 | import unittest |
20 | import time | ||
20 | from unittest import mock | 21 | from unittest import mock |
21 | 22 | ||
22 | import pytest | 23 | import pytest |
23 | 24 | ||
24 | import command | 25 | import command |
25 | from subcmds import sync | 26 | from subcmds import sync |
27 | from project import SyncNetworkHalfResult | ||
28 | from 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 | |||
241 | class 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) | ||