summaryrefslogtreecommitdiffstats
path: root/git_superproject.py
diff options
context:
space:
mode:
Diffstat (limited to 'git_superproject.py')
-rw-r--r--git_superproject.py293
1 files changed, 214 insertions, 79 deletions
diff --git a/git_superproject.py b/git_superproject.py
index 89320971..4ca84a58 100644
--- a/git_superproject.py
+++ b/git_superproject.py
@@ -19,21 +19,52 @@ https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
19 19
20Examples: 20Examples:
21 superproject = Superproject() 21 superproject = Superproject()
22 project_commit_ids = superproject.UpdateProjectsRevisionId(projects) 22 UpdateProjectsResult = superproject.UpdateProjectsRevisionId(projects)
23""" 23"""
24 24
25import hashlib 25import hashlib
26import functools
26import os 27import os
27import sys 28import sys
29import time
30from typing import NamedTuple
28 31
29from error import BUG_REPORT_URL 32from git_command import git_require, GitCommand
30from git_command import GitCommand 33from git_config import RepoConfig
31from git_refs import R_HEADS 34from git_refs import R_HEADS
35from manifest_xml import LOCAL_MANIFEST_GROUP_PREFIX
32 36
33_SUPERPROJECT_GIT_NAME = 'superproject.git' 37_SUPERPROJECT_GIT_NAME = 'superproject.git'
34_SUPERPROJECT_MANIFEST_NAME = 'superproject_override.xml' 38_SUPERPROJECT_MANIFEST_NAME = 'superproject_override.xml'
35 39
36 40
41class SyncResult(NamedTuple):
42 """Return the status of sync and whether caller should exit."""
43
44 # Whether the superproject sync was successful.
45 success: bool
46 # Whether the caller should exit.
47 fatal: bool
48
49
50class CommitIdsResult(NamedTuple):
51 """Return the commit ids and whether caller should exit."""
52
53 # A dictionary with the projects/commit ids on success, otherwise None.
54 commit_ids: dict
55 # Whether the caller should exit.
56 fatal: bool
57
58
59class UpdateProjectsResult(NamedTuple):
60 """Return the overriding manifest file and whether caller should exit."""
61
62 # Path name of the overriding manifest file if successful, otherwise None.
63 manifest_path: str
64 # Whether the caller should exit.
65 fatal: bool
66
67
37class Superproject(object): 68class Superproject(object):
38 """Get commit ids from superproject. 69 """Get commit ids from superproject.
39 70
@@ -41,21 +72,25 @@ class Superproject(object):
41 lookup of commit ids for all projects. It contains _project_commit_ids which 72 lookup of commit ids for all projects. It contains _project_commit_ids which
42 is a dictionary with project/commit id entries. 73 is a dictionary with project/commit id entries.
43 """ 74 """
44 def __init__(self, manifest, repodir, superproject_dir='exp-superproject', 75 def __init__(self, manifest, repodir, git_event_log,
45 quiet=False): 76 superproject_dir='exp-superproject', quiet=False, print_messages=False):
46 """Initializes superproject. 77 """Initializes superproject.
47 78
48 Args: 79 Args:
49 manifest: A Manifest object that is to be written to a file. 80 manifest: A Manifest object that is to be written to a file.
50 repodir: Path to the .repo/ dir for holding all internal checkout state. 81 repodir: Path to the .repo/ dir for holding all internal checkout state.
51 It must be in the top directory of the repo client checkout. 82 It must be in the top directory of the repo client checkout.
83 git_event_log: A git trace2 event log to log events.
52 superproject_dir: Relative path under |repodir| to checkout superproject. 84 superproject_dir: Relative path under |repodir| to checkout superproject.
53 quiet: If True then only print the progress messages. 85 quiet: If True then only print the progress messages.
86 print_messages: if True then print error/warning messages.
54 """ 87 """
55 self._project_commit_ids = None 88 self._project_commit_ids = None
56 self._manifest = manifest 89 self._manifest = manifest
90 self._git_event_log = git_event_log
57 self._quiet = quiet 91 self._quiet = quiet
58 self._branch = self._GetBranch() 92 self._print_messages = print_messages
93 self._branch = manifest.branch
59 self._repodir = os.path.abspath(repodir) 94 self._repodir = os.path.abspath(repodir)
60 self._superproject_dir = superproject_dir 95 self._superproject_dir = superproject_dir
61 self._superproject_path = os.path.join(self._repodir, superproject_dir) 96 self._superproject_path = os.path.join(self._repodir, superproject_dir)
@@ -63,8 +98,12 @@ class Superproject(object):
63 _SUPERPROJECT_MANIFEST_NAME) 98 _SUPERPROJECT_MANIFEST_NAME)
64 git_name = '' 99 git_name = ''
65 if self._manifest.superproject: 100 if self._manifest.superproject:
66 remote_name = self._manifest.superproject['remote'].name 101 remote = self._manifest.superproject['remote']
67 git_name = hashlib.md5(remote_name.encode('utf8')).hexdigest() + '-' 102 git_name = hashlib.md5(remote.name.encode('utf8')).hexdigest() + '-'
103 self._branch = self._manifest.superproject['revision']
104 self._remote_url = remote.url
105 else:
106 self._remote_url = None
68 self._work_git_name = git_name + _SUPERPROJECT_GIT_NAME 107 self._work_git_name = git_name + _SUPERPROJECT_GIT_NAME
69 self._work_git = os.path.join(self._superproject_path, self._work_git_name) 108 self._work_git = os.path.join(self._superproject_path, self._work_git_name)
70 109
@@ -73,16 +112,28 @@ class Superproject(object):
73 """Returns a dictionary of projects and their commit ids.""" 112 """Returns a dictionary of projects and their commit ids."""
74 return self._project_commit_ids 113 return self._project_commit_ids
75 114
76 def _GetBranch(self): 115 @property
77 """Returns the branch name for getting the approved manifest.""" 116 def manifest_path(self):
78 p = self._manifest.manifestProject 117 """Returns the manifest path if the path exists or None."""
79 b = p.GetBranch(p.CurrentBranch) 118 return self._manifest_path if os.path.exists(self._manifest_path) else None
80 if not b: 119
81 return None 120 def _LogMessage(self, message):
82 branch = b.merge 121 """Logs message to stderr and _git_event_log."""
83 if branch and branch.startswith(R_HEADS): 122 if self._print_messages:
84 branch = branch[len(R_HEADS):] 123 print(message, file=sys.stderr)
85 return branch 124 self._git_event_log.ErrorEvent(message, f'{message}')
125
126 def _LogMessagePrefix(self):
127 """Returns the prefix string to be logged in each log message"""
128 return f'repo superproject branch: {self._branch} url: {self._remote_url}'
129
130 def _LogError(self, message):
131 """Logs error message to stderr and _git_event_log."""
132 self._LogMessage(f'{self._LogMessagePrefix()} error: {message}')
133
134 def _LogWarning(self, message):
135 """Logs warning message to stderr and _git_event_log."""
136 self._LogMessage(f'{self._LogMessagePrefix()} warning: {message}')
86 137
87 def _Init(self): 138 def _Init(self):
88 """Sets up a local Git repository to get a copy of a superproject. 139 """Sets up a local Git repository to get a copy of a superproject.
@@ -103,25 +154,25 @@ class Superproject(object):
103 capture_stderr=True) 154 capture_stderr=True)
104 retval = p.Wait() 155 retval = p.Wait()
105 if retval: 156 if retval:
106 print('repo: error: git init call failed with return code: %r, stderr: %r' % 157 self._LogWarning(f'git init call failed, command: git {cmd}, '
107 (retval, p.stderr), file=sys.stderr) 158 f'return code: {retval}, stderr: {p.stderr}')
108 return False 159 return False
109 return True 160 return True
110 161
111 def _Fetch(self, url): 162 def _Fetch(self):
112 """Fetches a local copy of a superproject for the manifest based on url. 163 """Fetches a local copy of a superproject for the manifest based on |_remote_url|.
113
114 Args:
115 url: superproject's url.
116 164
117 Returns: 165 Returns:
118 True if fetch is successful, or False. 166 True if fetch is successful, or False.
119 """ 167 """
120 if not os.path.exists(self._work_git): 168 if not os.path.exists(self._work_git):
121 print('git fetch missing drectory: %s' % self._work_git, 169 self._LogWarning(f'git fetch missing directory: {self._work_git}')
122 file=sys.stderr)
123 return False 170 return False
124 cmd = ['fetch', url, '--depth', '1', '--force', '--no-tags', '--filter', 'blob:none'] 171 if not git_require((2, 28, 0)):
172 self._LogWarning('superproject requires a git version 2.28 or later')
173 return False
174 cmd = ['fetch', self._remote_url, '--depth', '1', '--force', '--no-tags',
175 '--filter', 'blob:none']
125 if self._branch: 176 if self._branch:
126 cmd += [self._branch + ':' + self._branch] 177 cmd += [self._branch + ':' + self._branch]
127 p = GitCommand(None, 178 p = GitCommand(None,
@@ -131,8 +182,8 @@ class Superproject(object):
131 capture_stderr=True) 182 capture_stderr=True)
132 retval = p.Wait() 183 retval = p.Wait()
133 if retval: 184 if retval:
134 print('repo: error: git fetch call failed with return code: %r, stderr: %r' % 185 self._LogWarning(f'git fetch call failed, command: git {cmd}, '
135 (retval, p.stderr), file=sys.stderr) 186 f'return code: {retval}, stderr: {p.stderr}')
136 return False 187 return False
137 return True 188 return True
138 189
@@ -145,8 +196,7 @@ class Superproject(object):
145 data: data returned from 'git ls-tree ...' instead of None. 196 data: data returned from 'git ls-tree ...' instead of None.
146 """ 197 """
147 if not os.path.exists(self._work_git): 198 if not os.path.exists(self._work_git):
148 print('git ls-tree missing drectory: %s' % self._work_git, 199 self._LogWarning(f'git ls-tree missing directory: {self._work_git}')
149 file=sys.stderr)
150 return None 200 return None
151 data = None 201 data = None
152 branch = 'HEAD' if not self._branch else self._branch 202 branch = 'HEAD' if not self._branch else self._branch
@@ -161,52 +211,52 @@ class Superproject(object):
161 if retval == 0: 211 if retval == 0:
162 data = p.stdout 212 data = p.stdout
163 else: 213 else:
164 print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % ( 214 self._LogWarning(f'git ls-tree call failed, command: git {cmd}, '
165 retval, p.stderr), file=sys.stderr) 215 f'return code: {retval}, stderr: {p.stderr}')
166 return data 216 return data
167 217
168 def Sync(self): 218 def Sync(self):
169 """Gets a local copy of a superproject for the manifest. 219 """Gets a local copy of a superproject for the manifest.
170 220
171 Returns: 221 Returns:
172 True if sync of superproject is successful, or False. 222 SyncResult
173 """ 223 """
174 print('WARNING: --use-superproject is experimental and not '
175 'for general use', file=sys.stderr)
176
177 if not self._manifest.superproject: 224 if not self._manifest.superproject:
178 print('error: superproject tag is not defined in manifest', 225 self._LogWarning(f'superproject tag is not defined in manifest: '
179 file=sys.stderr) 226 f'{self._manifest.manifestFile}')
180 return False 227 return SyncResult(False, False)
181 228
182 url = self._manifest.superproject['remote'].url 229 print('NOTICE: --use-superproject is in beta; report any issues to the '
183 if not url: 230 'address described in `repo version`', file=sys.stderr)
184 print('error: superproject URL is not defined in manifest', 231 should_exit = True
185 file=sys.stderr) 232 if not self._remote_url:
186 return False 233 self._LogWarning(f'superproject URL is not defined in manifest: '
234 f'{self._manifest.manifestFile}')
235 return SyncResult(False, should_exit)
187 236
188 if not self._Init(): 237 if not self._Init():
189 return False 238 return SyncResult(False, should_exit)
190 if not self._Fetch(url): 239 if not self._Fetch():
191 return False 240 return SyncResult(False, should_exit)
192 if not self._quiet: 241 if not self._quiet:
193 print('%s: Initial setup for superproject completed.' % self._work_git) 242 print('%s: Initial setup for superproject completed.' % self._work_git)
194 return True 243 return SyncResult(True, False)
195 244
196 def _GetAllProjectsCommitIds(self): 245 def _GetAllProjectsCommitIds(self):
197 """Get commit ids for all projects from superproject and save them in _project_commit_ids. 246 """Get commit ids for all projects from superproject and save them in _project_commit_ids.
198 247
199 Returns: 248 Returns:
200 A dictionary with the projects/commit ids on success, otherwise None. 249 CommitIdsResult
201 """ 250 """
202 if not self.Sync(): 251 sync_result = self.Sync()
203 return None 252 if not sync_result.success:
253 return CommitIdsResult(None, sync_result.fatal)
204 254
205 data = self._LsTree() 255 data = self._LsTree()
206 if not data: 256 if not data:
207 print('error: git ls-tree failed to return data for superproject', 257 self._LogWarning(f'git ls-tree failed to return data for manifest: '
208 file=sys.stderr) 258 f'{self._manifest.manifestFile}')
209 return None 259 return CommitIdsResult(None, True)
210 260
211 # Parse lines like the following to select lines starting with '160000' and 261 # Parse lines like the following to select lines starting with '160000' and
212 # build a dictionary with project path (last element) and its commit id (3rd element). 262 # build a dictionary with project path (last element) and its commit id (3rd element).
@@ -222,18 +272,16 @@ class Superproject(object):
222 commit_ids[ls_data[3]] = ls_data[2] 272 commit_ids[ls_data[3]] = ls_data[2]
223 273
224 self._project_commit_ids = commit_ids 274 self._project_commit_ids = commit_ids
225 return commit_ids 275 return CommitIdsResult(commit_ids, False)
226 276
227 def _WriteManfiestFile(self): 277 def _WriteManifestFile(self):
228 """Writes manifest to a file. 278 """Writes manifest to a file.
229 279
230 Returns: 280 Returns:
231 manifest_path: Path name of the file into which manifest is written instead of None. 281 manifest_path: Path name of the file into which manifest is written instead of None.
232 """ 282 """
233 if not os.path.exists(self._superproject_path): 283 if not os.path.exists(self._superproject_path):
234 print('error: missing superproject directory %s' % 284 self._LogWarning(f'missing superproject directory: {self._superproject_path}')
235 self._superproject_path,
236 file=sys.stderr)
237 return None 285 return None
238 manifest_str = self._manifest.ToXml(groups=self._manifest.GetGroupsStr()).toxml() 286 manifest_str = self._manifest.ToXml(groups=self._manifest.GetGroupsStr()).toxml()
239 manifest_path = self._manifest_path 287 manifest_path = self._manifest_path
@@ -241,12 +289,30 @@ class Superproject(object):
241 with open(manifest_path, 'w', encoding='utf-8') as fp: 289 with open(manifest_path, 'w', encoding='utf-8') as fp:
242 fp.write(manifest_str) 290 fp.write(manifest_str)
243 except IOError as e: 291 except IOError as e:
244 print('error: cannot write manifest to %s:\n%s' 292 self._LogError(f'cannot write manifest to : {manifest_path} {e}')
245 % (manifest_path, e),
246 file=sys.stderr)
247 return None 293 return None
248 return manifest_path 294 return manifest_path
249 295
296 def _SkipUpdatingProjectRevisionId(self, project):
297 """Checks if a project's revision id needs to be updated or not.
298
299 Revision id for projects from local manifest will not be updated.
300
301 Args:
302 project: project whose revision id is being updated.
303
304 Returns:
305 True if a project's revision id should not be updated, or False,
306 """
307 path = project.relpath
308 if not path:
309 return True
310 # Skip the project with revisionId.
311 if project.revisionId:
312 return True
313 # Skip the project if it comes from the local manifest.
314 return any(s.startswith(LOCAL_MANIFEST_GROUP_PREFIX) for s in project.groups)
315
250 def UpdateProjectsRevisionId(self, projects): 316 def UpdateProjectsRevisionId(self, projects):
251 """Update revisionId of every project in projects with the commit id. 317 """Update revisionId of every project in projects with the commit id.
252 318
@@ -254,27 +320,96 @@ class Superproject(object):
254 projects: List of projects whose revisionId needs to be updated. 320 projects: List of projects whose revisionId needs to be updated.
255 321
256 Returns: 322 Returns:
257 manifest_path: Path name of the overriding manfiest file instead of None. 323 UpdateProjectsResult
258 """ 324 """
259 commit_ids = self._GetAllProjectsCommitIds() 325 commit_ids_result = self._GetAllProjectsCommitIds()
326 commit_ids = commit_ids_result.commit_ids
260 if not commit_ids: 327 if not commit_ids:
261 print('error: Cannot get project commit ids from manifest', file=sys.stderr) 328 return UpdateProjectsResult(None, commit_ids_result.fatal)
262 return None
263 329
264 projects_missing_commit_ids = [] 330 projects_missing_commit_ids = []
265 for project in projects: 331 for project in projects:
266 path = project.relpath 332 if self._SkipUpdatingProjectRevisionId(project):
267 if not path:
268 continue 333 continue
334 path = project.relpath
269 commit_id = commit_ids.get(path) 335 commit_id = commit_ids.get(path)
270 if commit_id: 336 if not commit_id:
271 project.SetRevisionId(commit_id)
272 else:
273 projects_missing_commit_ids.append(path) 337 projects_missing_commit_ids.append(path)
338
339 # If superproject doesn't have a commit id for a project, then report an
340 # error event and continue as if do not use superproject is specified.
274 if projects_missing_commit_ids: 341 if projects_missing_commit_ids:
275 print('error: please file a bug using %s to report missing commit_ids for: %s' % 342 self._LogWarning(f'please file a bug using {self._manifest.contactinfo.bugurl} '
276 (BUG_REPORT_URL, projects_missing_commit_ids), file=sys.stderr) 343 f'to report missing commit_ids for: {projects_missing_commit_ids}')
277 return None 344 return UpdateProjectsResult(None, False)
278 345
279 manifest_path = self._WriteManfiestFile() 346 for project in projects:
280 return manifest_path 347 if not self._SkipUpdatingProjectRevisionId(project):
348 project.SetRevisionId(commit_ids.get(project.relpath))
349
350 manifest_path = self._WriteManifestFile()
351 return UpdateProjectsResult(manifest_path, False)
352
353
354@functools.lru_cache(maxsize=None)
355def _UseSuperprojectFromConfiguration():
356 """Returns the user choice of whether to use superproject."""
357 user_cfg = RepoConfig.ForUser()
358 time_now = int(time.time())
359
360 user_value = user_cfg.GetBoolean('repo.superprojectChoice')
361 if user_value is not None:
362 user_expiration = user_cfg.GetInt('repo.superprojectChoiceExpire')
363 if user_expiration is None or user_expiration <= 0 or user_expiration >= time_now:
364 # TODO(b/190688390) - Remove prompt when we are comfortable with the new
365 # default value.
366 if user_value:
367 print(('You are currently enrolled in Git submodules experiment '
368 '(go/android-submodules-quickstart). Use --no-use-superproject '
369 'to override.\n'), file=sys.stderr)
370 else:
371 print(('You are not currently enrolled in Git submodules experiment '
372 '(go/android-submodules-quickstart). Use --use-superproject '
373 'to override.\n'), file=sys.stderr)
374 return user_value
375
376 # We don't have an unexpired choice, ask for one.
377 system_cfg = RepoConfig.ForSystem()
378 system_value = system_cfg.GetBoolean('repo.superprojectChoice')
379 if system_value:
380 # The system configuration is proposing that we should enable the
381 # use of superproject. Treat the user as enrolled for two weeks.
382 #
383 # TODO(b/190688390) - Remove prompt when we are comfortable with the new
384 # default value.
385 userchoice = True
386 time_choiceexpire = time_now + (86400 * 14)
387 user_cfg.SetString('repo.superprojectChoiceExpire', str(time_choiceexpire))
388 user_cfg.SetBoolean('repo.superprojectChoice', userchoice)
389 print('You are automatically enrolled in Git submodules experiment '
390 '(go/android-submodules-quickstart) for another two weeks.\n',
391 file=sys.stderr)
392 return True
393
394 # For all other cases, we would not use superproject by default.
395 return False
396
397
398def PrintMessages(opt, manifest):
399 """Returns a boolean if error/warning messages are to be printed."""
400 return opt.use_superproject is not None or manifest.superproject
401
402
403def UseSuperproject(opt, manifest):
404 """Returns a boolean if use-superproject option is enabled."""
405
406 if opt.use_superproject is not None:
407 return opt.use_superproject
408 else:
409 client_value = manifest.manifestProject.config.GetBoolean('repo.superproject')
410 if client_value is not None:
411 return client_value
412 else:
413 if not manifest.superproject:
414 return False
415 return _UseSuperprojectFromConfiguration()