blob: 57a3a533c65a71d8edabb063613e6cc6f224ec6d [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 Tenneti6a872c92021-01-14 19:17:50 -0800115 def _LsTree(self):
116 """Returns the data from 'git ls-tree -r HEAD'.
117
118 Works only in git repositories.
119
120 Returns:
121 data: data returned from 'git ls-tree -r HEAD' instead of None.
122 """
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800123 if not os.path.exists(self._work_git):
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800124 print('git ls-tree missing drectory: %s' % self._work_git,
125 file=sys.stderr)
126 return None
Raman Tenneti6a872c92021-01-14 19:17:50 -0800127 data = None
128 cmd = ['ls-tree', '-z', '-r', 'HEAD']
129 p = GitCommand(None,
130 cmd,
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800131 cwd=self._work_git,
Raman Tenneti6a872c92021-01-14 19:17:50 -0800132 capture_stdout=True,
133 capture_stderr=True)
134 retval = p.Wait()
135 if retval == 0:
136 data = p.stdout
137 else:
138 # `git clone` is documented to produce an exit status of `128` if
139 # the requested url or branch are not present in the configuration.
140 print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
141 retval, p.stderr), file=sys.stderr)
142 return data
143
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800144 def _GetAllProjectsSHAs(self, url, branch=None):
Raman Tenneti6a872c92021-01-14 19:17:50 -0800145 """Get SHAs for all projects from superproject and save them in _project_shas.
146
147 Args:
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800148 url: superproject's url to be passed to git clone or fetch.
149 branch: The branchname to be passed as argument to git clone or fetch.
Raman Tenneti6a872c92021-01-14 19:17:50 -0800150
151 Returns:
152 A dictionary with the projects/SHAs instead of None.
153 """
154 if not url:
155 raise ValueError('url argument is not supplied.')
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800156
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800157 do_clone = True
Raman Tenneti6a872c92021-01-14 19:17:50 -0800158 if os.path.exists(self._superproject_path):
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800159 if not self._Fetch():
160 # If fetch fails due to a corrupted git directory, then do a git clone.
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800161 platform_utils.rmtree(self._superproject_path)
162 else:
163 do_clone = False
164 if do_clone:
Raman Tenneti9e787532021-02-01 11:47:06 -0800165 if not self._Clone(url, branch):
166 raise GitError('git clone failed for url: %s' % url)
Raman Tenneti6a872c92021-01-14 19:17:50 -0800167
168 data = self._LsTree()
169 if not data:
170 raise GitError('git ls-tree failed for url: %s' % url)
171
172 # Parse lines like the following to select lines starting with '160000' and
173 # build a dictionary with project path (last element) and its SHA (3rd element).
174 #
175 # 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
176 # 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
177 shas = {}
178 for line in data.split('\x00'):
179 ls_data = line.split(None, 3)
180 if not ls_data:
181 break
182 if ls_data[0] == '160000':
183 shas[ls_data[3]] = ls_data[2]
184
185 self._project_shas = shas
186 return shas
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800187
188 def _WriteManfiestFile(self, manifest):
189 """Writes manifest to a file.
190
191 Args:
192 manifest: A Manifest object that is to be written to a file.
193
194 Returns:
195 manifest_path: Path name of the file into which manifest is written instead of None.
196 """
197 if not os.path.exists(self._superproject_path):
198 print('error: missing superproject directory %s' %
199 self._superproject_path,
200 file=sys.stderr)
201 return None
202 manifest_str = manifest.ToXml().toxml()
203 manifest_path = self._manifest_path
204 try:
205 with open(manifest_path, 'w', encoding='utf-8') as fp:
206 fp.write(manifest_str)
207 except IOError as e:
208 print('error: cannot write manifest to %s:\n%s'
209 % (manifest_path, e),
210 file=sys.stderr)
211 return None
212 return manifest_path
213
214 def UpdateProjectsRevisionId(self, manifest, projects, url, branch=None):
215 """Update revisionId of every project in projects with the SHA.
216
217 Args:
218 manifest: A Manifest object that is to be written to a file.
219 projects: List of projects whose revisionId needs to be updated.
220 url: superproject's url to be passed to git clone or fetch.
Raman Tenneti8d43dea2021-02-07 16:30:27 -0800221 branch: The branchname to be passed as argument to git clone or fetch.
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800222
223 Returns:
224 manifest_path: Path name of the overriding manfiest file instead of None.
225 """
226 try:
227 shas = self._GetAllProjectsSHAs(url=url, branch=branch)
228 except Exception as e:
229 print('error: Cannot get project SHAs for %s: %s: %s' %
230 (url, type(e).__name__, str(e)),
231 file=sys.stderr)
232 return None
233
234 projects_missing_shas = []
235 for project in projects:
236 path = project.relpath
237 if not path:
238 continue
239 sha = shas.get(path)
240 if sha:
241 project.SetRevisionId(sha)
242 else:
243 projects_missing_shas.append(path)
244 if projects_missing_shas:
245 print('error: please file a bug using %s to report missing shas for: %s' %
246 (BUG_REPORT_URL, projects_missing_shas), file=sys.stderr)
247 return None
248
249 manifest_path = self._WriteManfiestFile(manifest)
250 return manifest_path