blob: 378ede25d2d230fa78d674f4a5ba2f9e460d732e [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
15"""Provide functionality to get all projects and their SHAs from Superproject.
16
17For more information on superproject, check out:
18https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
19
20Examples:
21 superproject = Superproject()
22 project_shas = superproject.GetAllProjectsSHAs()
23"""
24
25import os
26import sys
27
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080028from error import BUG_REPORT_URL, GitError
Raman Tenneti6a872c92021-01-14 19:17:50 -080029from git_command import GitCommand
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080030import platform_utils
Raman Tenneti6a872c92021-01-14 19:17:50 -080031
Raman Tenneti8d43dea2021-02-07 16:30:27 -080032_SUPERPROJECT_GIT_NAME = 'superproject.git'
33_SUPERPROJECT_MANIFEST_NAME = 'superproject_override.xml'
34
Raman Tenneti6a872c92021-01-14 19:17:50 -080035
36class Superproject(object):
37 """Get SHAs from superproject.
38
39 It does a 'git clone' of superproject and 'git ls-tree' to get list of SHAs for all projects.
40 It contains project_shas which is a dictionary with project/sha entries.
41 """
42 def __init__(self, repodir, superproject_dir='exp-superproject'):
43 """Initializes superproject.
44
45 Args:
46 repodir: Path to the .repo/ dir for holding all internal checkout state.
47 superproject_dir: Relative path under |repodir| to checkout superproject.
48 """
49 self._project_shas = None
50 self._repodir = os.path.abspath(repodir)
51 self._superproject_dir = superproject_dir
52 self._superproject_path = os.path.join(self._repodir, superproject_dir)
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080053 self._manifest_path = os.path.join(self._superproject_path,
Raman Tenneti8d43dea2021-02-07 16:30:27 -080054 _SUPERPROJECT_MANIFEST_NAME)
55 self._work_git = os.path.join(self._superproject_path,
56 _SUPERPROJECT_GIT_NAME)
Raman Tenneti6a872c92021-01-14 19:17:50 -080057
58 @property
59 def project_shas(self):
60 """Returns a dictionary of projects and their SHAs."""
61 return self._project_shas
62
63 def _Clone(self, url, branch=None):
64 """Do a 'git clone' for the given url and branch.
65
66 Args:
67 url: superproject's url to be passed to git clone.
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080068 branch: The branchname to be passed as argument to git clone.
Raman Tenneti6a872c92021-01-14 19:17:50 -080069
70 Returns:
71 True if 'git clone <url> <branch>' is successful, or False.
72 """
Raman Tenneti8d43dea2021-02-07 16:30:27 -080073 if not os.path.exists(self._superproject_path):
74 os.mkdir(self._superproject_path)
75 cmd = ['clone', url, '--filter', 'blob:none', '--bare']
Raman Tenneti6a872c92021-01-14 19:17:50 -080076 if branch:
77 cmd += ['--branch', branch]
78 p = GitCommand(None,
79 cmd,
80 cwd=self._superproject_path,
81 capture_stdout=True,
82 capture_stderr=True)
83 retval = p.Wait()
84 if retval:
85 # `git clone` is documented to produce an exit status of `128` if
86 # the requested url or branch are not present in the configuration.
87 print('repo: error: git clone call failed with return code: %r, stderr: %r' %
88 (retval, p.stderr), file=sys.stderr)
89 return False
90 return True
91
Raman Tenneti8d43dea2021-02-07 16:30:27 -080092 def _Fetch(self):
93 """Do a 'git fetch' to to fetch the latest content.
Raman Tenneti9e787532021-02-01 11:47:06 -080094
95 Returns:
Raman Tenneti8d43dea2021-02-07 16:30:27 -080096 True if 'git fetch' is successful, or False.
Raman Tenneti9e787532021-02-01 11:47:06 -080097 """
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080098 if not os.path.exists(self._work_git):
Raman Tenneti8d43dea2021-02-07 16:30:27 -080099 print('git fetch missing drectory: %s' % self._work_git,
100 file=sys.stderr)
101 return False
102 cmd = ['fetch', 'origin', '+refs/heads/*:refs/heads/*', '--prune']
Raman Tenneti9e787532021-02-01 11:47:06 -0800103 p = GitCommand(None,
104 cmd,
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800105 cwd=self._work_git,
Raman Tenneti9e787532021-02-01 11:47:06 -0800106 capture_stdout=True,
107 capture_stderr=True)
108 retval = p.Wait()
109 if retval:
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800110 print('repo: error: git fetch call failed with return code: %r, stderr: %r' %
Raman Tenneti9e787532021-02-01 11:47:06 -0800111 (retval, p.stderr), file=sys.stderr)
112 return False
113 return True
114
Raman Tennetice64e3d2021-02-08 13:27:41 -0800115 def _LsTree(self, branch='HEAD'):
116 """Returns the data from 'git ls-tree -r <branch>'.
Raman Tenneti6a872c92021-01-14 19:17:50 -0800117
118 Works only in git repositories.
119
Raman Tennetice64e3d2021-02-08 13:27:41 -0800120 Args:
121 branch: The branchname to be passed as argument to git ls-tree.
122
Raman Tenneti6a872c92021-01-14 19:17:50 -0800123 Returns:
124 data: data returned from 'git ls-tree -r HEAD' instead of None.
125 """
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800126 if not os.path.exists(self._work_git):
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800127 print('git ls-tree missing drectory: %s' % self._work_git,
128 file=sys.stderr)
129 return None
Raman Tenneti6a872c92021-01-14 19:17:50 -0800130 data = None
Raman Tennetice64e3d2021-02-08 13:27:41 -0800131 cmd = ['ls-tree', '-z', '-r', branch]
132
Raman Tenneti6a872c92021-01-14 19:17:50 -0800133 p = GitCommand(None,
134 cmd,
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800135 cwd=self._work_git,
Raman Tenneti6a872c92021-01-14 19:17:50 -0800136 capture_stdout=True,
137 capture_stderr=True)
138 retval = p.Wait()
139 if retval == 0:
140 data = p.stdout
141 else:
142 # `git clone` is documented to produce an exit status of `128` if
143 # the requested url or branch are not present in the configuration.
144 print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
145 retval, p.stderr), file=sys.stderr)
146 return data
147
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800148 def _GetAllProjectsSHAs(self, url, branch=None):
Raman Tenneti6a872c92021-01-14 19:17:50 -0800149 """Get SHAs for all projects from superproject and save them in _project_shas.
150
151 Args:
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800152 url: superproject's url to be passed to git clone or fetch.
153 branch: The branchname to be passed as argument to git clone or fetch.
Raman Tenneti6a872c92021-01-14 19:17:50 -0800154
155 Returns:
156 A dictionary with the projects/SHAs instead of None.
157 """
158 if not url:
159 raise ValueError('url argument is not supplied.')
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800160
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800161 do_clone = True
Raman Tenneti6a872c92021-01-14 19:17:50 -0800162 if os.path.exists(self._superproject_path):
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800163 if not self._Fetch():
164 # If fetch fails due to a corrupted git directory, then do a git clone.
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800165 platform_utils.rmtree(self._superproject_path)
166 else:
167 do_clone = False
168 if do_clone:
Raman Tenneti9e787532021-02-01 11:47:06 -0800169 if not self._Clone(url, branch):
170 raise GitError('git clone failed for url: %s' % url)
Raman Tenneti6a872c92021-01-14 19:17:50 -0800171
Raman Tennetice64e3d2021-02-08 13:27:41 -0800172 data = self._LsTree(branch)
Raman Tenneti6a872c92021-01-14 19:17:50 -0800173 if not data:
174 raise GitError('git ls-tree failed for url: %s' % url)
175
176 # Parse lines like the following to select lines starting with '160000' and
177 # build a dictionary with project path (last element) and its SHA (3rd element).
178 #
179 # 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
180 # 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
181 shas = {}
182 for line in data.split('\x00'):
183 ls_data = line.split(None, 3)
184 if not ls_data:
185 break
186 if ls_data[0] == '160000':
187 shas[ls_data[3]] = ls_data[2]
188
189 self._project_shas = shas
190 return shas
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800191
192 def _WriteManfiestFile(self, manifest):
193 """Writes manifest to a file.
194
195 Args:
196 manifest: A Manifest object that is to be written to a file.
197
198 Returns:
199 manifest_path: Path name of the file into which manifest is written instead of None.
200 """
201 if not os.path.exists(self._superproject_path):
202 print('error: missing superproject directory %s' %
203 self._superproject_path,
204 file=sys.stderr)
205 return None
206 manifest_str = manifest.ToXml().toxml()
207 manifest_path = self._manifest_path
208 try:
209 with open(manifest_path, 'w', encoding='utf-8') as fp:
210 fp.write(manifest_str)
211 except IOError as e:
212 print('error: cannot write manifest to %s:\n%s'
213 % (manifest_path, e),
214 file=sys.stderr)
215 return None
216 return manifest_path
217
218 def UpdateProjectsRevisionId(self, manifest, projects, url, branch=None):
219 """Update revisionId of every project in projects with the SHA.
220
221 Args:
222 manifest: A Manifest object that is to be written to a file.
223 projects: List of projects whose revisionId needs to be updated.
224 url: superproject's url to be passed to git clone or fetch.
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800225 branch: The branchname to be passed as argument to git clone or fetch.
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800226
227 Returns:
228 manifest_path: Path name of the overriding manfiest file instead of None.
229 """
230 try:
231 shas = self._GetAllProjectsSHAs(url=url, branch=branch)
232 except Exception as e:
233 print('error: Cannot get project SHAs for %s: %s: %s' %
234 (url, type(e).__name__, str(e)),
235 file=sys.stderr)
236 return None
237
238 projects_missing_shas = []
239 for project in projects:
240 path = project.relpath
241 if not path:
242 continue
243 sha = shas.get(path)
244 if sha:
245 project.SetRevisionId(sha)
246 else:
247 projects_missing_shas.append(path)
248 if projects_missing_shas:
249 print('error: please file a bug using %s to report missing shas for: %s' %
250 (BUG_REPORT_URL, projects_missing_shas), file=sys.stderr)
251 return None
252
253 manifest_path = self._WriteManfiestFile(manifest)
254 return manifest_path