blob: 0c477060c5826ab932753d895b4dc867d52dc750 [file] [log] [blame]
Raman Tenneti6a872c92021-01-14 19:17:50 -08001# Copyright (C) 2021 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Raman Tenneti21dce3d2021-02-09 00:26:31 -080015"""Provide functionality to get all projects and their commit ids from Superproject.
Raman Tenneti6a872c92021-01-14 19:17:50 -080016
17For more information on superproject, check out:
18https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
19
20Examples:
21 superproject = Superproject()
Raman Tenneti784e16f2021-06-11 17:29:45 -070022 UpdateProjectsResult = superproject.UpdateProjectsRevisionId(projects)
Raman Tenneti6a872c92021-01-14 19:17:50 -080023"""
24
Raman Tenneticeba2dd2021-02-22 16:54:56 -080025import hashlib
Raman Tenneti6a872c92021-01-14 19:17:50 -080026import os
27import sys
Raman Tenneti784e16f2021-06-11 17:29:45 -070028from typing import NamedTuple
Raman Tenneti6a872c92021-01-14 19:17:50 -080029
Raman Tennetie253b432021-06-02 10:05:54 -070030from git_command import git_require, GitCommand
Raman Tenneti21dce3d2021-02-09 00:26:31 -080031from git_refs import R_HEADS
Raman Tenneti78f4dd32021-06-07 13:27:37 -070032from manifest_xml import LOCAL_MANIFEST_GROUP_PREFIX
Raman Tenneti6a872c92021-01-14 19:17:50 -080033
Raman Tenneti8d43dea2021-02-07 16:30:27 -080034_SUPERPROJECT_GIT_NAME = 'superproject.git'
35_SUPERPROJECT_MANIFEST_NAME = 'superproject_override.xml'
36
Raman Tenneti6a872c92021-01-14 19:17:50 -080037
Raman Tenneti784e16f2021-06-11 17:29:45 -070038class SyncResult(NamedTuple):
39 """Return the status of sync and whether caller should exit."""
40
41 # Whether the superproject sync was successful.
42 success: bool
43 # Whether the caller should exit.
44 fatal: bool
45
46
47class CommitIdsResult(NamedTuple):
48 """Return the commit ids and whether caller should exit."""
49
50 # A dictionary with the projects/commit ids on success, otherwise None.
51 commit_ids: dict
52 # Whether the caller should exit.
53 fatal: bool
54
55
56class UpdateProjectsResult(NamedTuple):
57 """Return the overriding manifest file and whether caller should exit."""
58
59 # Path name of the overriding manfiest file if successful, otherwise None.
60 manifest_path: str
61 # Whether the caller should exit.
62 fatal: bool
63
64
Raman Tenneti6a872c92021-01-14 19:17:50 -080065class Superproject(object):
Raman Tenneti21dce3d2021-02-09 00:26:31 -080066 """Get commit ids from superproject.
Raman Tenneti6a872c92021-01-14 19:17:50 -080067
Raman Tenneticeba2dd2021-02-22 16:54:56 -080068 Initializes a local copy of a superproject for the manifest. This allows
69 lookup of commit ids for all projects. It contains _project_commit_ids which
70 is a dictionary with project/commit id entries.
Raman Tenneti6a872c92021-01-14 19:17:50 -080071 """
Raman Tenneti784e16f2021-06-11 17:29:45 -070072 def __init__(self, manifest, repodir, git_event_log,
73 superproject_dir='exp-superproject', quiet=False):
Raman Tenneti6a872c92021-01-14 19:17:50 -080074 """Initializes superproject.
75
76 Args:
Raman Tenneti21dce3d2021-02-09 00:26:31 -080077 manifest: A Manifest object that is to be written to a file.
Raman Tenneti6a872c92021-01-14 19:17:50 -080078 repodir: Path to the .repo/ dir for holding all internal checkout state.
Raman Tenneti21dce3d2021-02-09 00:26:31 -080079 It must be in the top directory of the repo client checkout.
Raman Tenneti784e16f2021-06-11 17:29:45 -070080 git_event_log: A git trace2 event log to log events.
Raman Tenneti6a872c92021-01-14 19:17:50 -080081 superproject_dir: Relative path under |repodir| to checkout superproject.
Raman Tennetief99ec02021-03-04 10:29:40 -080082 quiet: If True then only print the progress messages.
Raman Tenneti6a872c92021-01-14 19:17:50 -080083 """
Raman Tenneti21dce3d2021-02-09 00:26:31 -080084 self._project_commit_ids = None
85 self._manifest = manifest
Raman Tenneti784e16f2021-06-11 17:29:45 -070086 self._git_event_log = git_event_log
Raman Tennetief99ec02021-03-04 10:29:40 -080087 self._quiet = quiet
Raman Tenneti21dce3d2021-02-09 00:26:31 -080088 self._branch = self._GetBranch()
Raman Tenneti6a872c92021-01-14 19:17:50 -080089 self._repodir = os.path.abspath(repodir)
90 self._superproject_dir = superproject_dir
91 self._superproject_path = os.path.join(self._repodir, superproject_dir)
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080092 self._manifest_path = os.path.join(self._superproject_path,
Raman Tenneti8d43dea2021-02-07 16:30:27 -080093 _SUPERPROJECT_MANIFEST_NAME)
Raman Tenneticeba2dd2021-02-22 16:54:56 -080094 git_name = ''
95 if self._manifest.superproject:
96 remote_name = self._manifest.superproject['remote'].name
97 git_name = hashlib.md5(remote_name.encode('utf8')).hexdigest() + '-'
98 self._work_git_name = git_name + _SUPERPROJECT_GIT_NAME
99 self._work_git = os.path.join(self._superproject_path, self._work_git_name)
Raman Tenneti6a872c92021-01-14 19:17:50 -0800100
101 @property
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800102 def project_commit_ids(self):
103 """Returns a dictionary of projects and their commit ids."""
104 return self._project_commit_ids
Raman Tenneti6a872c92021-01-14 19:17:50 -0800105
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800106 def _GetBranch(self):
107 """Returns the branch name for getting the approved manifest."""
108 p = self._manifest.manifestProject
109 b = p.GetBranch(p.CurrentBranch)
110 if not b:
111 return None
112 branch = b.merge
113 if branch and branch.startswith(R_HEADS):
114 branch = branch[len(R_HEADS):]
115 return branch
116
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800117 def _Init(self):
118 """Sets up a local Git repository to get a copy of a superproject.
Raman Tenneti6a872c92021-01-14 19:17:50 -0800119
120 Returns:
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800121 True if initialization is successful, or False.
Raman Tenneti6a872c92021-01-14 19:17:50 -0800122 """
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800123 if not os.path.exists(self._superproject_path):
124 os.mkdir(self._superproject_path)
Raman Tennetief99ec02021-03-04 10:29:40 -0800125 if not self._quiet and not os.path.exists(self._work_git):
126 print('%s: Performing initial setup for superproject; this might take '
127 'several minutes.' % self._work_git)
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800128 cmd = ['init', '--bare', self._work_git_name]
Raman Tenneti6a872c92021-01-14 19:17:50 -0800129 p = GitCommand(None,
130 cmd,
131 cwd=self._superproject_path,
132 capture_stdout=True,
133 capture_stderr=True)
134 retval = p.Wait()
135 if retval:
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800136 print('repo: error: git init call failed with return code: %r, stderr: %r' %
Raman Tenneti6a872c92021-01-14 19:17:50 -0800137 (retval, p.stderr), file=sys.stderr)
138 return False
139 return True
140
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800141 def _Fetch(self, url):
142 """Fetches a local copy of a superproject for the manifest based on url.
143
144 Args:
145 url: superproject's url.
Raman Tenneti9e787532021-02-01 11:47:06 -0800146
147 Returns:
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800148 True if fetch is successful, or False.
Raman Tenneti9e787532021-02-01 11:47:06 -0800149 """
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800150 if not os.path.exists(self._work_git):
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800151 print('git fetch missing drectory: %s' % self._work_git,
152 file=sys.stderr)
153 return False
Raman Tennetie253b432021-06-02 10:05:54 -0700154 if not git_require((2, 28, 0)):
155 print('superproject requires a git version 2.28 or later', file=sys.stderr)
156 return False
Raman Tenneti83670962021-03-19 13:53:43 -0700157 cmd = ['fetch', url, '--depth', '1', '--force', '--no-tags', '--filter', 'blob:none']
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800158 if self._branch:
159 cmd += [self._branch + ':' + self._branch]
Raman Tenneti9e787532021-02-01 11:47:06 -0800160 p = GitCommand(None,
161 cmd,
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800162 cwd=self._work_git,
Raman Tenneti9e787532021-02-01 11:47:06 -0800163 capture_stdout=True,
164 capture_stderr=True)
165 retval = p.Wait()
166 if retval:
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800167 print('repo: error: git fetch call failed with return code: %r, stderr: %r' %
Raman Tenneti9e787532021-02-01 11:47:06 -0800168 (retval, p.stderr), file=sys.stderr)
169 return False
170 return True
171
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800172 def _LsTree(self):
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800173 """Gets the commit ids for all projects.
Raman Tenneti6a872c92021-01-14 19:17:50 -0800174
175 Works only in git repositories.
176
177 Returns:
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800178 data: data returned from 'git ls-tree ...' instead of None.
Raman Tenneti6a872c92021-01-14 19:17:50 -0800179 """
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800180 if not os.path.exists(self._work_git):
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800181 print('git ls-tree missing drectory: %s' % self._work_git,
182 file=sys.stderr)
183 return None
Raman Tenneti6a872c92021-01-14 19:17:50 -0800184 data = None
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800185 branch = 'HEAD' if not self._branch else self._branch
Raman Tennetice64e3d2021-02-08 13:27:41 -0800186 cmd = ['ls-tree', '-z', '-r', branch]
187
Raman Tenneti6a872c92021-01-14 19:17:50 -0800188 p = GitCommand(None,
189 cmd,
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800190 cwd=self._work_git,
Raman Tenneti6a872c92021-01-14 19:17:50 -0800191 capture_stdout=True,
192 capture_stderr=True)
193 retval = p.Wait()
194 if retval == 0:
195 data = p.stdout
196 else:
Raman Tenneti6a872c92021-01-14 19:17:50 -0800197 print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
198 retval, p.stderr), file=sys.stderr)
199 return data
200
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800201 def Sync(self):
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800202 """Gets a local copy of a superproject for the manifest.
Raman Tenneti6a872c92021-01-14 19:17:50 -0800203
204 Returns:
Raman Tenneti784e16f2021-06-11 17:29:45 -0700205 SyncResult
Raman Tenneti6a872c92021-01-14 19:17:50 -0800206 """
Raman Tenneti2b37fa32021-06-02 17:46:25 -0700207 print('NOTICE: --use-superproject is in beta; report any issues to the '
208 'address described in `repo version`', file=sys.stderr)
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800209
210 if not self._manifest.superproject:
Raman Tenneti784e16f2021-06-11 17:29:45 -0700211 msg = (f'repo error: superproject tag is not defined in manifest: '
212 f'{self._manifest.manifestFile}')
213 print(msg, file=sys.stderr)
214 self._git_event_log.ErrorEvent(msg, '')
215 return SyncResult(False, False)
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800216
Raman Tenneti784e16f2021-06-11 17:29:45 -0700217 should_exit = True
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800218 url = self._manifest.superproject['remote'].url
Raman Tenneti6a872c92021-01-14 19:17:50 -0800219 if not url:
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800220 print('error: superproject URL is not defined in manifest',
221 file=sys.stderr)
Raman Tenneti784e16f2021-06-11 17:29:45 -0700222 return SyncResult(False, should_exit)
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800223
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800224 if not self._Init():
Raman Tenneti784e16f2021-06-11 17:29:45 -0700225 return SyncResult(False, should_exit)
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800226 if not self._Fetch(url):
Raman Tenneti784e16f2021-06-11 17:29:45 -0700227 return SyncResult(False, should_exit)
Raman Tennetief99ec02021-03-04 10:29:40 -0800228 if not self._quiet:
229 print('%s: Initial setup for superproject completed.' % self._work_git)
Raman Tenneti784e16f2021-06-11 17:29:45 -0700230 return SyncResult(True, False)
Raman Tenneti6a872c92021-01-14 19:17:50 -0800231
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800232 def _GetAllProjectsCommitIds(self):
233 """Get commit ids for all projects from superproject and save them in _project_commit_ids.
234
235 Returns:
Raman Tenneti784e16f2021-06-11 17:29:45 -0700236 CommitIdsResult
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800237 """
Raman Tenneti784e16f2021-06-11 17:29:45 -0700238 sync_result = self.Sync()
239 if not sync_result.success:
240 return CommitIdsResult(None, sync_result.fatal)
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800241
242 data = self._LsTree()
Raman Tenneti6a872c92021-01-14 19:17:50 -0800243 if not data:
Raman Tenneticeba2dd2021-02-22 16:54:56 -0800244 print('error: git ls-tree failed to return data for superproject',
245 file=sys.stderr)
Raman Tenneti784e16f2021-06-11 17:29:45 -0700246 return CommitIdsResult(None, True)
Raman Tenneti6a872c92021-01-14 19:17:50 -0800247
248 # Parse lines like the following to select lines starting with '160000' and
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800249 # build a dictionary with project path (last element) and its commit id (3rd element).
Raman Tenneti6a872c92021-01-14 19:17:50 -0800250 #
251 # 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
252 # 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800253 commit_ids = {}
Raman Tenneti6a872c92021-01-14 19:17:50 -0800254 for line in data.split('\x00'):
255 ls_data = line.split(None, 3)
256 if not ls_data:
257 break
258 if ls_data[0] == '160000':
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800259 commit_ids[ls_data[3]] = ls_data[2]
Raman Tenneti6a872c92021-01-14 19:17:50 -0800260
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800261 self._project_commit_ids = commit_ids
Raman Tenneti784e16f2021-06-11 17:29:45 -0700262 return CommitIdsResult(commit_ids, False)
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800263
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800264 def _WriteManfiestFile(self):
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800265 """Writes manifest to a file.
266
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800267 Returns:
268 manifest_path: Path name of the file into which manifest is written instead of None.
269 """
270 if not os.path.exists(self._superproject_path):
271 print('error: missing superproject directory %s' %
272 self._superproject_path,
273 file=sys.stderr)
274 return None
Raman Tenneti080877e2021-03-09 15:19:06 -0800275 manifest_str = self._manifest.ToXml(groups=self._manifest.GetGroupsStr()).toxml()
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800276 manifest_path = self._manifest_path
277 try:
278 with open(manifest_path, 'w', encoding='utf-8') as fp:
279 fp.write(manifest_str)
280 except IOError as e:
281 print('error: cannot write manifest to %s:\n%s'
282 % (manifest_path, e),
283 file=sys.stderr)
284 return None
285 return manifest_path
286
Raman Tenneti784e16f2021-06-11 17:29:45 -0700287 def _SkipUpdatingProjectRevisionId(self, project):
288 """Checks if a project's revision id needs to be updated or not.
289
290 Revision id for projects from local manifest will not be updated.
291
292 Args:
293 project: project whose revision id is being updated.
294
295 Returns:
296 True if a project's revision id should not be updated, or False,
297 """
298 path = project.relpath
299 if not path:
300 return True
Raman Tenneti1da6f302021-06-28 19:21:38 -0700301 # Skip the project with revisionId.
302 if project.revisionId:
303 return True
Raman Tenneti784e16f2021-06-11 17:29:45 -0700304 # Skip the project if it comes from the local manifest.
305 return any(s.startswith(LOCAL_MANIFEST_GROUP_PREFIX) for s in project.groups)
306
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800307 def UpdateProjectsRevisionId(self, projects):
308 """Update revisionId of every project in projects with the commit id.
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800309
310 Args:
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800311 projects: List of projects whose revisionId needs to be updated.
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800312
313 Returns:
Raman Tenneti784e16f2021-06-11 17:29:45 -0700314 UpdateProjectsResult
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800315 """
Raman Tenneti784e16f2021-06-11 17:29:45 -0700316 commit_ids_result = self._GetAllProjectsCommitIds()
317 commit_ids = commit_ids_result.commit_ids
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800318 if not commit_ids:
319 print('error: Cannot get project commit ids from manifest', file=sys.stderr)
Raman Tenneti784e16f2021-06-11 17:29:45 -0700320 return UpdateProjectsResult(None, commit_ids_result.fatal)
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800321
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800322 projects_missing_commit_ids = []
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800323 for project in projects:
Raman Tenneti784e16f2021-06-11 17:29:45 -0700324 if self._SkipUpdatingProjectRevisionId(project):
325 continue
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800326 path = project.relpath
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800327 commit_id = commit_ids.get(path)
Raman Tenneti784e16f2021-06-11 17:29:45 -0700328 if not commit_id:
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800329 projects_missing_commit_ids.append(path)
Raman Tenneti784e16f2021-06-11 17:29:45 -0700330
331 # If superproject doesn't have a commit id for a project, then report an
332 # error event and continue as if do not use superproject is specified.
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800333 if projects_missing_commit_ids:
Raman Tenneti784e16f2021-06-11 17:29:45 -0700334 msg = (f'error: please file a bug using {self._manifest.contactinfo.bugurl} '
335 f'to report missing commit_ids for: {projects_missing_commit_ids}')
336 print(msg, file=sys.stderr)
337 self._git_event_log.ErrorEvent(msg, '')
338 return UpdateProjectsResult(None, False)
339
340 for project in projects:
341 if not self._SkipUpdatingProjectRevisionId(project):
342 project.SetRevisionId(commit_ids.get(project.relpath))
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800343
Raman Tenneti21dce3d2021-02-09 00:26:31 -0800344 manifest_path = self._WriteManfiestFile()
Raman Tenneti784e16f2021-06-11 17:29:45 -0700345 return UpdateProjectsResult(manifest_path, False)