diff options
-rw-r--r-- | docs/internal-fs-layout.md | 5 | ||||
-rw-r--r-- | git_config.py | 87 | ||||
-rw-r--r-- | git_trace2_event_log.py | 20 | ||||
-rw-r--r-- | subcmds/sync.py | 23 | ||||
-rw-r--r-- | tests/fixtures/test.gitconfig | 9 | ||||
-rw-r--r-- | tests/test_git_config.py | 17 |
6 files changed, 152 insertions, 9 deletions
diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md index 8cc3cabd..e3be1731 100644 --- a/docs/internal-fs-layout.md +++ b/docs/internal-fs-layout.md | |||
@@ -146,7 +146,12 @@ Instead, you should use standard Git workflows like [git worktree] or | |||
146 | 146 | ||
147 | The `.repo/manifests.git/config` file is used to track settings for the entire | 147 | The `.repo/manifests.git/config` file is used to track settings for the entire |
148 | repo client checkout. | 148 | repo client checkout. |
149 | |||
149 | Most settings use the `[repo]` section to avoid conflicts with git. | 150 | Most settings use the `[repo]` section to avoid conflicts with git. |
151 | |||
152 | Everything under `[repo.syncstate.*]` is used to keep track of sync details for logging | ||
153 | purposes. | ||
154 | |||
150 | User controlled settings are initialized when running `repo init`. | 155 | User controlled settings are initialized when running `repo init`. |
151 | 156 | ||
152 | | Setting | `repo init` Option | Use/Meaning | | 157 | | Setting | `repo init` Option | Use/Meaning | |
diff --git a/git_config.py b/git_config.py index 3eaf201c..05d824cb 100644 --- a/git_config.py +++ b/git_config.py | |||
@@ -13,6 +13,7 @@ | |||
13 | # limitations under the License. | 13 | # limitations under the License. |
14 | 14 | ||
15 | import contextlib | 15 | import contextlib |
16 | import datetime | ||
16 | import errno | 17 | import errno |
17 | from http.client import HTTPException | 18 | from http.client import HTTPException |
18 | import json | 19 | import json |
@@ -30,6 +31,10 @@ from repo_trace import Trace | |||
30 | from git_command import GitCommand | 31 | from git_command import GitCommand |
31 | from git_refs import R_CHANGES, R_HEADS, R_TAGS | 32 | from git_refs import R_CHANGES, R_HEADS, R_TAGS |
32 | 33 | ||
34 | # Prefix that is prepended to all the keys of SyncAnalysisState's data | ||
35 | # that is saved in the config. | ||
36 | SYNC_STATE_PREFIX = 'repo.syncstate.' | ||
37 | |||
33 | ID_RE = re.compile(r'^[0-9a-f]{40}$') | 38 | ID_RE = re.compile(r'^[0-9a-f]{40}$') |
34 | 39 | ||
35 | REVIEW_CACHE = dict() | 40 | REVIEW_CACHE = dict() |
@@ -262,6 +267,22 @@ class GitConfig(object): | |||
262 | self._branches[b.name] = b | 267 | self._branches[b.name] = b |
263 | return b | 268 | return b |
264 | 269 | ||
270 | def GetSyncAnalysisStateData(self): | ||
271 | """Returns data to be logged for the analysis of sync performance.""" | ||
272 | return {k: v for k, v in self.DumpConfigDict().items() if k.startswith(SYNC_STATE_PREFIX)} | ||
273 | |||
274 | def UpdateSyncAnalysisState(self, options, superproject_logging_data): | ||
275 | """Update Config's SYNC_STATE_PREFIX* data with the latest sync data. | ||
276 | |||
277 | Args: | ||
278 | options: Options passed to sync returned from optparse. See _Options(). | ||
279 | superproject_logging_data: A dictionary of superproject data that is to be logged. | ||
280 | |||
281 | Returns: | ||
282 | SyncAnalysisState object. | ||
283 | """ | ||
284 | return SyncAnalysisState(self, options, superproject_logging_data) | ||
285 | |||
265 | def GetSubSections(self, section): | 286 | def GetSubSections(self, section): |
266 | """List all subsection names matching $section.*.* | 287 | """List all subsection names matching $section.*.* |
267 | """ | 288 | """ |
@@ -717,3 +738,69 @@ class Branch(object): | |||
717 | def _Get(self, key, all_keys=False): | 738 | def _Get(self, key, all_keys=False): |
718 | key = 'branch.%s.%s' % (self.name, key) | 739 | key = 'branch.%s.%s' % (self.name, key) |
719 | return self._config.GetString(key, all_keys=all_keys) | 740 | return self._config.GetString(key, all_keys=all_keys) |
741 | |||
742 | |||
743 | class SyncAnalysisState: | ||
744 | """Configuration options related to logging of sync state for analysis. | ||
745 | |||
746 | This object is versioned. | ||
747 | """ | ||
748 | def __init__(self, config, options, superproject_logging_data): | ||
749 | """Initializes SyncAnalysisState. | ||
750 | |||
751 | Saves the following data into the |config| object. | ||
752 | - sys.argv, options, superproject's logging data. | ||
753 | - repo.*, branch.* and remote.* parameters from config object. | ||
754 | - Current time as synctime. | ||
755 | - Version number of the object. | ||
756 | |||
757 | All the keys saved by this object are prepended with SYNC_STATE_PREFIX. | ||
758 | |||
759 | Args: | ||
760 | config: GitConfig object to store all options. | ||
761 | options: Options passed to sync returned from optparse. See _Options(). | ||
762 | superproject_logging_data: A dictionary of superproject data that is to be logged. | ||
763 | """ | ||
764 | self._config = config | ||
765 | now = datetime.datetime.utcnow() | ||
766 | self._Set('main.synctime', now.isoformat() + 'Z') | ||
767 | self._Set('main.version', '1') | ||
768 | self._Set('sys.argv', sys.argv) | ||
769 | for key, value in superproject_logging_data.items(): | ||
770 | self._Set(f'superproject.{key}', value) | ||
771 | for key, value in options.__dict__.items(): | ||
772 | self._Set(f'options.{key}', value) | ||
773 | config_items = config.DumpConfigDict().items() | ||
774 | EXTRACT_NAMESPACES = {'repo', 'branch', 'remote'} | ||
775 | self._SetDictionary({k: v for k, v in config_items | ||
776 | if not k.startswith(SYNC_STATE_PREFIX) and | ||
777 | k.split('.', 1)[0] in EXTRACT_NAMESPACES}) | ||
778 | |||
779 | def _SetDictionary(self, data): | ||
780 | """Save all key/value pairs of |data| dictionary. | ||
781 | |||
782 | Args: | ||
783 | data: A dictionary whose key/value are to be saved. | ||
784 | """ | ||
785 | for key, value in data.items(): | ||
786 | self._Set(key, value) | ||
787 | |||
788 | def _Set(self, key, value): | ||
789 | """Set the |value| for a |key| in the |_config| member. | ||
790 | |||
791 | |key| is prepended with the value of SYNC_STATE_PREFIX constant. | ||
792 | |||
793 | Args: | ||
794 | key: Name of the key. | ||
795 | value: |value| could be of any type. If it is 'bool', it will be saved | ||
796 | as a Boolean and for all other types, it will be saved as a String. | ||
797 | """ | ||
798 | if value is None: | ||
799 | return | ||
800 | sync_key = f'{SYNC_STATE_PREFIX}{key}' | ||
801 | if isinstance(value, str): | ||
802 | self._config.SetString(sync_key, value) | ||
803 | elif isinstance(value, bool): | ||
804 | self._config.SetBoolean(sync_key, value) | ||
805 | else: | ||
806 | self._config.SetString(sync_key, str(value)) | ||
diff --git a/git_trace2_event_log.py b/git_trace2_event_log.py index fae3d4c8..9c9e5a70 100644 --- a/git_trace2_event_log.py +++ b/git_trace2_event_log.py | |||
@@ -144,6 +144,19 @@ class EventLog(object): | |||
144 | command_event['subcommands'] = subcommands | 144 | command_event['subcommands'] = subcommands |
145 | self._log.append(command_event) | 145 | self._log.append(command_event) |
146 | 146 | ||
147 | def LogConfigEvents(self, config, event_dict_name): | ||
148 | """Append a |event_dict_name| event for each config key in |config|. | ||
149 | |||
150 | Args: | ||
151 | config: Configuration dictionary. | ||
152 | event_dict_name: Name of the event dictionary for items to be logged under. | ||
153 | """ | ||
154 | for param, value in config.items(): | ||
155 | event = self._CreateEventDict(event_dict_name) | ||
156 | event['param'] = param | ||
157 | event['value'] = value | ||
158 | self._log.append(event) | ||
159 | |||
147 | def DefParamRepoEvents(self, config): | 160 | def DefParamRepoEvents(self, config): |
148 | """Append a 'def_param' event for each repo.* config key to the current log. | 161 | """Append a 'def_param' event for each repo.* config key to the current log. |
149 | 162 | ||
@@ -152,12 +165,7 @@ class EventLog(object): | |||
152 | """ | 165 | """ |
153 | # Only output the repo.* config parameters. | 166 | # Only output the repo.* config parameters. |
154 | repo_config = {k: v for k, v in config.items() if k.startswith('repo.')} | 167 | repo_config = {k: v for k, v in config.items() if k.startswith('repo.')} |
155 | 168 | self.LogConfigEvents(repo_config, 'def_param') | |
156 | for param, value in repo_config.items(): | ||
157 | def_param_event = self._CreateEventDict('def_param') | ||
158 | def_param_event['param'] = param | ||
159 | def_param_event['value'] = value | ||
160 | self._log.append(def_param_event) | ||
161 | 169 | ||
162 | def ErrorEvent(self, msg, fmt): | 170 | def ErrorEvent(self, msg, fmt): |
163 | """Append a 'error' event to the current log.""" | 171 | """Append a 'error' event to the current log.""" |
diff --git a/subcmds/sync.py b/subcmds/sync.py index 74617544..ed656b8c 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py | |||
@@ -282,7 +282,7 @@ later is required to fix a server side protocol bug. | |||
282 | """Returns True if current-branch or use-superproject options are enabled.""" | 282 | """Returns True if current-branch or use-superproject options are enabled.""" |
283 | return opt.current_branch_only or git_superproject.UseSuperproject(opt, self.manifest) | 283 | return opt.current_branch_only or git_superproject.UseSuperproject(opt, self.manifest) |
284 | 284 | ||
285 | def _UpdateProjectsRevisionId(self, opt, args, load_local_manifests): | 285 | def _UpdateProjectsRevisionId(self, opt, args, load_local_manifests, superproject_logging_data): |
286 | """Update revisionId of every project with the SHA from superproject. | 286 | """Update revisionId of every project with the SHA from superproject. |
287 | 287 | ||
288 | This function updates each project's revisionId with SHA from superproject. | 288 | This function updates each project's revisionId with SHA from superproject. |
@@ -293,6 +293,7 @@ later is required to fix a server side protocol bug. | |||
293 | args: Arguments to pass to GetProjects. See the GetProjects | 293 | args: Arguments to pass to GetProjects. See the GetProjects |
294 | docstring for details. | 294 | docstring for details. |
295 | load_local_manifests: Whether to load local manifests. | 295 | load_local_manifests: Whether to load local manifests. |
296 | superproject_logging_data: A dictionary of superproject data that is to be logged. | ||
296 | 297 | ||
297 | Returns: | 298 | Returns: |
298 | Returns path to the overriding manifest file instead of None. | 299 | Returns path to the overriding manifest file instead of None. |
@@ -312,6 +313,7 @@ later is required to fix a server side protocol bug. | |||
312 | submodules_ok=opt.fetch_submodules) | 313 | submodules_ok=opt.fetch_submodules) |
313 | update_result = superproject.UpdateProjectsRevisionId(all_projects) | 314 | update_result = superproject.UpdateProjectsRevisionId(all_projects) |
314 | manifest_path = update_result.manifest_path | 315 | manifest_path = update_result.manifest_path |
316 | superproject_logging_data['updatedrevisionid'] = bool(manifest_path) | ||
315 | if manifest_path: | 317 | if manifest_path: |
316 | self._ReloadManifest(manifest_path, load_local_manifests) | 318 | self._ReloadManifest(manifest_path, load_local_manifests) |
317 | else: | 319 | else: |
@@ -964,8 +966,14 @@ later is required to fix a server side protocol bug. | |||
964 | self._UpdateManifestProject(opt, mp, manifest_name) | 966 | self._UpdateManifestProject(opt, mp, manifest_name) |
965 | 967 | ||
966 | load_local_manifests = not self.manifest.HasLocalManifests | 968 | load_local_manifests = not self.manifest.HasLocalManifests |
967 | if git_superproject.UseSuperproject(opt, self.manifest): | 969 | use_superproject = git_superproject.UseSuperproject(opt, self.manifest) |
968 | manifest_name = self._UpdateProjectsRevisionId(opt, args, load_local_manifests) or opt.manifest_name | 970 | superproject_logging_data = { |
971 | 'superproject': use_superproject, | ||
972 | 'haslocalmanifests': bool(self.manifest.HasLocalManifests), | ||
973 | } | ||
974 | if use_superproject: | ||
975 | manifest_name = self._UpdateProjectsRevisionId( | ||
976 | opt, args, load_local_manifests, superproject_logging_data) or opt.manifest_name | ||
969 | 977 | ||
970 | if self.gitc_manifest: | 978 | if self.gitc_manifest: |
971 | gitc_manifest_projects = self.GetProjects(args, | 979 | gitc_manifest_projects = self.GetProjects(args, |
@@ -1079,6 +1087,15 @@ later is required to fix a server side protocol bug. | |||
1079 | file=sys.stderr) | 1087 | file=sys.stderr) |
1080 | sys.exit(1) | 1088 | sys.exit(1) |
1081 | 1089 | ||
1090 | # Log the previous sync analysis state from the config. | ||
1091 | self.git_event_log.LogConfigEvents(mp.config.GetSyncAnalysisStateData(), | ||
1092 | 'previous_sync_state') | ||
1093 | |||
1094 | # Update and log with the new sync analysis state. | ||
1095 | mp.config.UpdateSyncAnalysisState(opt, superproject_logging_data) | ||
1096 | self.git_event_log.LogConfigEvents(mp.config.GetSyncAnalysisStateData(), | ||
1097 | 'current_sync_state') | ||
1098 | |||
1082 | if not opt.quiet: | 1099 | if not opt.quiet: |
1083 | print('repo sync has finished successfully.') | 1100 | print('repo sync has finished successfully.') |
1084 | 1101 | ||
diff --git a/tests/fixtures/test.gitconfig b/tests/fixtures/test.gitconfig index 9b3f2574..e3f51db3 100644 --- a/tests/fixtures/test.gitconfig +++ b/tests/fixtures/test.gitconfig | |||
@@ -11,3 +11,12 @@ | |||
11 | intk = 10k | 11 | intk = 10k |
12 | intm = 10m | 12 | intm = 10m |
13 | intg = 10g | 13 | intg = 10g |
14 | [repo "syncstate.main"] | ||
15 | synctime = 2021-07-29T19:18:53.201328Z | ||
16 | version = 1 | ||
17 | [repo "syncstate.sys"] | ||
18 | argv = ['/usr/bin/pytest-3'] | ||
19 | [repo "syncstate.superproject"] | ||
20 | test = false | ||
21 | [repo "syncstate.options"] | ||
22 | verbose = true | ||
diff --git a/tests/test_git_config.py b/tests/test_git_config.py index 3300c12f..44ff5974 100644 --- a/tests/test_git_config.py +++ b/tests/test_git_config.py | |||
@@ -104,6 +104,23 @@ class GitConfigReadOnlyTests(unittest.TestCase): | |||
104 | for key, value in TESTS: | 104 | for key, value in TESTS: |
105 | self.assertEqual(value, self.config.GetInt('section.%s' % (key,))) | 105 | self.assertEqual(value, self.config.GetInt('section.%s' % (key,))) |
106 | 106 | ||
107 | def test_GetSyncAnalysisStateData(self): | ||
108 | """Test config entries with a sync state analysis data.""" | ||
109 | superproject_logging_data = {} | ||
110 | superproject_logging_data['test'] = False | ||
111 | options = type('options', (object,), {})() | ||
112 | options.verbose = 'true' | ||
113 | TESTS = ( | ||
114 | ('superproject.test', 'false'), | ||
115 | ('options.verbose', 'true'), | ||
116 | ('main.version', '1'), | ||
117 | ) | ||
118 | self.config.UpdateSyncAnalysisState(options, superproject_logging_data) | ||
119 | sync_data = self.config.GetSyncAnalysisStateData() | ||
120 | for key, value in TESTS: | ||
121 | self.assertEqual(sync_data[f'{git_config.SYNC_STATE_PREFIX}{key}'], value) | ||
122 | self.assertTrue(sync_data[f'{git_config.SYNC_STATE_PREFIX}main.synctime']) | ||
123 | |||
107 | 124 | ||
108 | class GitConfigReadWriteTests(unittest.TestCase): | 125 | class GitConfigReadWriteTests(unittest.TestCase): |
109 | """Read/write tests of the GitConfig class.""" | 126 | """Read/write tests of the GitConfig class.""" |