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) | ||
