diff options
| author | Jason Chang <jasonnc@google.com> | 2023-08-31 17:06:36 -0700 |
|---|---|---|
| committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2023-09-06 17:36:31 +0000 |
| commit | daf2ad38eb62fb990fadc3fc872f303113ac4768 (patch) | |
| tree | 433961d2f498093386ed651c0dff2c523a67e1fd /subcmds/sync.py | |
| parent | b861511db919b93483b69136fd0f2c6ddab6b4ea (diff) | |
| download | git-repo-daf2ad38eb62fb990fadc3fc872f303113ac4768.tar.gz | |
sync: Preserve errors on KeyboardInterrupt
If a KeyboardInterrupt is encountered before an error is aggregated then
the context surrounding the interrupt is lost. This change aggregates
errors as soon as possible for the sync command
Bug: b/293344017
Change-Id: Iac14f9d59723cc9dedbb960f14fdc1fa5b348ea3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/384974
Tested-by: Jason Chang <jasonnc@google.com>
Commit-Queue: Jason Chang <jasonnc@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Diffstat (limited to 'subcmds/sync.py')
| -rw-r--r-- | subcmds/sync.py | 119 |
1 files changed, 78 insertions, 41 deletions
diff --git a/subcmds/sync.py b/subcmds/sync.py index 7ccd680f..bdb5616c 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py | |||
| @@ -22,9 +22,10 @@ import netrc | |||
| 22 | import optparse | 22 | import optparse |
| 23 | import os | 23 | import os |
| 24 | import socket | 24 | import socket |
| 25 | import sys | ||
| 25 | import tempfile | 26 | import tempfile |
| 26 | import time | 27 | import time |
| 27 | from typing import List, NamedTuple, Set | 28 | from typing import List, NamedTuple, Set, Union |
| 28 | import urllib.error | 29 | import urllib.error |
| 29 | import urllib.parse | 30 | import urllib.parse |
| 30 | import urllib.request | 31 | import urllib.request |
| @@ -55,6 +56,7 @@ from command import MirrorSafeCommand | |||
| 55 | from command import WORKER_BATCH_SIZE | 56 | from command import WORKER_BATCH_SIZE |
| 56 | from error import GitError | 57 | from error import GitError |
| 57 | from error import RepoChangedException | 58 | from error import RepoChangedException |
| 59 | from error import RepoError | ||
| 58 | from error import RepoExitError | 60 | from error import RepoExitError |
| 59 | from error import RepoUnhandledExceptionError | 61 | from error import RepoUnhandledExceptionError |
| 60 | from error import SyncError | 62 | from error import SyncError |
| @@ -120,7 +122,6 @@ class _FetchResult(NamedTuple): | |||
| 120 | 122 | ||
| 121 | success: bool | 123 | success: bool |
| 122 | projects: Set[str] | 124 | projects: Set[str] |
| 123 | errors: List[Exception] | ||
| 124 | 125 | ||
| 125 | 126 | ||
| 126 | class _FetchMainResult(NamedTuple): | 127 | class _FetchMainResult(NamedTuple): |
| @@ -131,7 +132,6 @@ class _FetchMainResult(NamedTuple): | |||
| 131 | """ | 132 | """ |
| 132 | 133 | ||
| 133 | all_projects: List[Project] | 134 | all_projects: List[Project] |
| 134 | errors: List[Exception] | ||
| 135 | 135 | ||
| 136 | 136 | ||
| 137 | class _CheckoutOneResult(NamedTuple): | 137 | class _CheckoutOneResult(NamedTuple): |
| @@ -163,6 +163,34 @@ class SmartSyncError(SyncError): | |||
| 163 | """Smart sync exit error.""" | 163 | """Smart sync exit error.""" |
| 164 | 164 | ||
| 165 | 165 | ||
| 166 | class ManifestInterruptError(RepoError): | ||
| 167 | """Aggregate Error to be logged when a user interrupts a manifest update.""" | ||
| 168 | |||
| 169 | def __init__(self, output, **kwargs): | ||
| 170 | super().__init__(output, **kwargs) | ||
| 171 | self.output = output | ||
| 172 | |||
| 173 | def __str__(self): | ||
| 174 | error_type = type(self).__name__ | ||
| 175 | return f"{error_type}:{self.output}" | ||
| 176 | |||
| 177 | |||
| 178 | class TeeStringIO(io.StringIO): | ||
| 179 | """StringIO class that can write to an additional destination.""" | ||
| 180 | |||
| 181 | def __init__( | ||
| 182 | self, io: Union[io.TextIOWrapper, None], *args, **kwargs | ||
| 183 | ) -> None: | ||
| 184 | super().__init__(*args, **kwargs) | ||
| 185 | self.io = io | ||
| 186 | |||
| 187 | def write(self, s: str) -> int: | ||
| 188 | """Write to additional destination.""" | ||
| 189 | super().write(s) | ||
| 190 | if self.io is not None: | ||
| 191 | self.io.write(s) | ||
| 192 | |||
| 193 | |||
| 166 | class Sync(Command, MirrorSafeCommand): | 194 | class Sync(Command, MirrorSafeCommand): |
| 167 | COMMON = True | 195 | COMMON = True |
| 168 | MULTI_MANIFEST_SUPPORT = True | 196 | MULTI_MANIFEST_SUPPORT = True |
| @@ -648,7 +676,7 @@ later is required to fix a server side protocol bug. | |||
| 648 | success = False | 676 | success = False |
| 649 | remote_fetched = False | 677 | remote_fetched = False |
| 650 | errors = [] | 678 | errors = [] |
| 651 | buf = io.StringIO() | 679 | buf = TeeStringIO(sys.stdout if opt.verbose else None) |
| 652 | try: | 680 | try: |
| 653 | sync_result = project.Sync_NetworkHalf( | 681 | sync_result = project.Sync_NetworkHalf( |
| 654 | quiet=opt.quiet, | 682 | quiet=opt.quiet, |
| @@ -675,7 +703,7 @@ later is required to fix a server side protocol bug. | |||
| 675 | errors.append(sync_result.error) | 703 | errors.append(sync_result.error) |
| 676 | 704 | ||
| 677 | output = buf.getvalue() | 705 | output = buf.getvalue() |
| 678 | if (opt.verbose or not success) and output: | 706 | if output and buf.io is None and not success: |
| 679 | print("\n" + output.rstrip()) | 707 | print("\n" + output.rstrip()) |
| 680 | 708 | ||
| 681 | if not success: | 709 | if not success: |
| @@ -729,13 +757,12 @@ later is required to fix a server side protocol bug. | |||
| 729 | jobs = jobs_str(len(items)) | 757 | jobs = jobs_str(len(items)) |
| 730 | return f"{jobs} | {elapsed_str(elapsed)} {earliest_proj}" | 758 | return f"{jobs} | {elapsed_str(elapsed)} {earliest_proj}" |
| 731 | 759 | ||
| 732 | def _Fetch(self, projects, opt, err_event, ssh_proxy): | 760 | def _Fetch(self, projects, opt, err_event, ssh_proxy, errors): |
| 733 | ret = True | 761 | ret = True |
| 734 | 762 | ||
| 735 | jobs = opt.jobs_network | 763 | jobs = opt.jobs_network |
| 736 | fetched = set() | 764 | fetched = set() |
| 737 | remote_fetched = set() | 765 | remote_fetched = set() |
| 738 | errors = [] | ||
| 739 | pm = Progress( | 766 | pm = Progress( |
| 740 | "Fetching", | 767 | "Fetching", |
| 741 | len(projects), | 768 | len(projects), |
| @@ -850,10 +877,10 @@ later is required to fix a server side protocol bug. | |||
| 850 | if not self.outer_client.manifest.IsArchive: | 877 | if not self.outer_client.manifest.IsArchive: |
| 851 | self._GCProjects(projects, opt, err_event) | 878 | self._GCProjects(projects, opt, err_event) |
| 852 | 879 | ||
| 853 | return _FetchResult(ret, fetched, errors) | 880 | return _FetchResult(ret, fetched) |
| 854 | 881 | ||
| 855 | def _FetchMain( | 882 | def _FetchMain( |
| 856 | self, opt, args, all_projects, err_event, ssh_proxy, manifest | 883 | self, opt, args, all_projects, err_event, ssh_proxy, manifest, errors |
| 857 | ): | 884 | ): |
| 858 | """The main network fetch loop. | 885 | """The main network fetch loop. |
| 859 | 886 | ||
| @@ -869,7 +896,6 @@ later is required to fix a server side protocol bug. | |||
| 869 | List of all projects that should be checked out. | 896 | List of all projects that should be checked out. |
| 870 | """ | 897 | """ |
| 871 | rp = manifest.repoProject | 898 | rp = manifest.repoProject |
| 872 | errors = [] | ||
| 873 | 899 | ||
| 874 | to_fetch = [] | 900 | to_fetch = [] |
| 875 | now = time.time() | 901 | now = time.time() |
| @@ -878,11 +904,9 @@ later is required to fix a server side protocol bug. | |||
| 878 | to_fetch.extend(all_projects) | 904 | to_fetch.extend(all_projects) |
| 879 | to_fetch.sort(key=self._fetch_times.Get, reverse=True) | 905 | to_fetch.sort(key=self._fetch_times.Get, reverse=True) |
| 880 | 906 | ||
| 881 | result = self._Fetch(to_fetch, opt, err_event, ssh_proxy) | 907 | result = self._Fetch(to_fetch, opt, err_event, ssh_proxy, errors) |
| 882 | success = result.success | 908 | success = result.success |
| 883 | fetched = result.projects | 909 | fetched = result.projects |
| 884 | if result.errors: | ||
| 885 | errors.extend(result.errors) | ||
| 886 | 910 | ||
| 887 | if not success: | 911 | if not success: |
| 888 | err_event.set() | 912 | err_event.set() |
| @@ -898,7 +922,7 @@ later is required to fix a server side protocol bug. | |||
| 898 | 922 | ||
| 899 | logger.error(e) | 923 | logger.error(e) |
| 900 | raise e | 924 | raise e |
| 901 | return _FetchMainResult([], errors) | 925 | return _FetchMainResult([]) |
| 902 | 926 | ||
| 903 | # Iteratively fetch missing and/or nested unregistered submodules. | 927 | # Iteratively fetch missing and/or nested unregistered submodules. |
| 904 | previously_missing_set = set() | 928 | previously_missing_set = set() |
| @@ -923,16 +947,14 @@ later is required to fix a server side protocol bug. | |||
| 923 | if previously_missing_set == missing_set: | 947 | if previously_missing_set == missing_set: |
| 924 | break | 948 | break |
| 925 | previously_missing_set = missing_set | 949 | previously_missing_set = missing_set |
| 926 | result = self._Fetch(missing, opt, err_event, ssh_proxy) | 950 | result = self._Fetch(missing, opt, err_event, ssh_proxy, errors) |
| 927 | success = result.success | 951 | success = result.success |
| 928 | new_fetched = result.projects | 952 | new_fetched = result.projects |
| 929 | if result.errors: | ||
| 930 | errors.extend(result.errors) | ||
| 931 | if not success: | 953 | if not success: |
| 932 | err_event.set() | 954 | err_event.set() |
| 933 | fetched.update(new_fetched) | 955 | fetched.update(new_fetched) |
| 934 | 956 | ||
| 935 | return _FetchMainResult(all_projects, errors) | 957 | return _FetchMainResult(all_projects) |
| 936 | 958 | ||
| 937 | def _CheckoutOne(self, detach_head, force_sync, project): | 959 | def _CheckoutOne(self, detach_head, force_sync, project): |
| 938 | """Checkout work tree for one project | 960 | """Checkout work tree for one project |
| @@ -1440,7 +1462,7 @@ later is required to fix a server side protocol bug. | |||
| 1440 | 1462 | ||
| 1441 | return manifest_name | 1463 | return manifest_name |
| 1442 | 1464 | ||
| 1443 | def _UpdateAllManifestProjects(self, opt, mp, manifest_name): | 1465 | def _UpdateAllManifestProjects(self, opt, mp, manifest_name, errors): |
| 1444 | """Fetch & update the local manifest project. | 1466 | """Fetch & update the local manifest project. |
| 1445 | 1467 | ||
| 1446 | After syncing the manifest project, if the manifest has any sub | 1468 | After syncing the manifest project, if the manifest has any sub |
| @@ -1452,7 +1474,7 @@ later is required to fix a server side protocol bug. | |||
| 1452 | manifest_name: Manifest file to be reloaded. | 1474 | manifest_name: Manifest file to be reloaded. |
| 1453 | """ | 1475 | """ |
| 1454 | if not mp.standalone_manifest_url: | 1476 | if not mp.standalone_manifest_url: |
| 1455 | self._UpdateManifestProject(opt, mp, manifest_name) | 1477 | self._UpdateManifestProject(opt, mp, manifest_name, errors) |
| 1456 | 1478 | ||
| 1457 | if mp.manifest.submanifests: | 1479 | if mp.manifest.submanifests: |
| 1458 | for submanifest in mp.manifest.submanifests.values(): | 1480 | for submanifest in mp.manifest.submanifests.values(): |
| @@ -1465,10 +1487,10 @@ later is required to fix a server side protocol bug. | |||
| 1465 | git_event_log=self.git_event_log, | 1487 | git_event_log=self.git_event_log, |
| 1466 | ) | 1488 | ) |
| 1467 | self._UpdateAllManifestProjects( | 1489 | self._UpdateAllManifestProjects( |
| 1468 | opt, child.manifestProject, None | 1490 | opt, child.manifestProject, None, errors |
| 1469 | ) | 1491 | ) |
| 1470 | 1492 | ||
| 1471 | def _UpdateManifestProject(self, opt, mp, manifest_name): | 1493 | def _UpdateManifestProject(self, opt, mp, manifest_name, errors): |
| 1472 | """Fetch & update the local manifest project. | 1494 | """Fetch & update the local manifest project. |
| 1473 | 1495 | ||
| 1474 | Args: | 1496 | Args: |
| @@ -1478,21 +1500,32 @@ later is required to fix a server side protocol bug. | |||
| 1478 | """ | 1500 | """ |
| 1479 | if not opt.local_only: | 1501 | if not opt.local_only: |
| 1480 | start = time.time() | 1502 | start = time.time() |
| 1481 | result = mp.Sync_NetworkHalf( | 1503 | buf = TeeStringIO(sys.stdout) |
| 1482 | quiet=opt.quiet, | 1504 | try: |
| 1483 | verbose=opt.verbose, | 1505 | result = mp.Sync_NetworkHalf( |
| 1484 | current_branch_only=self._GetCurrentBranchOnly( | 1506 | quiet=opt.quiet, |
| 1485 | opt, mp.manifest | 1507 | output_redir=buf, |
| 1486 | ), | 1508 | verbose=opt.verbose, |
| 1487 | force_sync=opt.force_sync, | 1509 | current_branch_only=self._GetCurrentBranchOnly( |
| 1488 | tags=opt.tags, | 1510 | opt, mp.manifest |
| 1489 | optimized_fetch=opt.optimized_fetch, | 1511 | ), |
| 1490 | retry_fetches=opt.retry_fetches, | 1512 | force_sync=opt.force_sync, |
| 1491 | submodules=mp.manifest.HasSubmodules, | 1513 | tags=opt.tags, |
| 1492 | clone_filter=mp.manifest.CloneFilter, | 1514 | optimized_fetch=opt.optimized_fetch, |
| 1493 | partial_clone_exclude=mp.manifest.PartialCloneExclude, | 1515 | retry_fetches=opt.retry_fetches, |
| 1494 | clone_filter_for_depth=mp.manifest.CloneFilterForDepth, | 1516 | submodules=mp.manifest.HasSubmodules, |
| 1495 | ) | 1517 | clone_filter=mp.manifest.CloneFilter, |
| 1518 | partial_clone_exclude=mp.manifest.PartialCloneExclude, | ||
| 1519 | clone_filter_for_depth=mp.manifest.CloneFilterForDepth, | ||
| 1520 | ) | ||
| 1521 | if result.error: | ||
| 1522 | errors.append(result.error) | ||
| 1523 | except KeyboardInterrupt: | ||
| 1524 | errors.append( | ||
| 1525 | ManifestInterruptError(buf.getvalue(), project=mp.name) | ||
| 1526 | ) | ||
| 1527 | raise | ||
| 1528 | |||
| 1496 | finish = time.time() | 1529 | finish = time.time() |
| 1497 | self.event_log.AddSync( | 1530 | self.event_log.AddSync( |
| 1498 | mp, event_log.TASK_SYNC_NETWORK, start, finish, result.success | 1531 | mp, event_log.TASK_SYNC_NETWORK, start, finish, result.success |
| @@ -1664,7 +1697,7 @@ later is required to fix a server side protocol bug. | |||
| 1664 | mp.ConfigureCloneFilterForDepth("blob:none") | 1697 | mp.ConfigureCloneFilterForDepth("blob:none") |
| 1665 | 1698 | ||
| 1666 | if opt.mp_update: | 1699 | if opt.mp_update: |
| 1667 | self._UpdateAllManifestProjects(opt, mp, manifest_name) | 1700 | self._UpdateAllManifestProjects(opt, mp, manifest_name, errors) |
| 1668 | else: | 1701 | else: |
| 1669 | logger.info("Skipping update of local manifest project.") | 1702 | logger.info("Skipping update of local manifest project.") |
| 1670 | 1703 | ||
| @@ -1704,10 +1737,14 @@ later is required to fix a server side protocol bug. | |||
| 1704 | # Initialize the socket dir once in the parent. | 1737 | # Initialize the socket dir once in the parent. |
| 1705 | ssh_proxy.sock() | 1738 | ssh_proxy.sock() |
| 1706 | result = self._FetchMain( | 1739 | result = self._FetchMain( |
| 1707 | opt, args, all_projects, err_event, ssh_proxy, manifest | 1740 | opt, |
| 1741 | args, | ||
| 1742 | all_projects, | ||
| 1743 | err_event, | ||
| 1744 | ssh_proxy, | ||
| 1745 | manifest, | ||
| 1746 | errors, | ||
| 1708 | ) | 1747 | ) |
| 1709 | if result.errors: | ||
| 1710 | errors.extend(result.errors) | ||
| 1711 | all_projects = result.all_projects | 1748 | all_projects = result.all_projects |
| 1712 | 1749 | ||
| 1713 | if opt.network_only: | 1750 | if opt.network_only: |
