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