blob: 465d1f8773762f43e0ba4f84a56c8229dee79056 [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
32
33class Superproject(object):
34 """Get SHAs from superproject.
35
36 It does a 'git clone' of superproject and 'git ls-tree' to get list of SHAs for all projects.
37 It contains project_shas which is a dictionary with project/sha entries.
38 """
39 def __init__(self, repodir, superproject_dir='exp-superproject'):
40 """Initializes superproject.
41
42 Args:
43 repodir: Path to the .repo/ dir for holding all internal checkout state.
44 superproject_dir: Relative path under |repodir| to checkout superproject.
45 """
46 self._project_shas = None
47 self._repodir = os.path.abspath(repodir)
48 self._superproject_dir = superproject_dir
49 self._superproject_path = os.path.join(self._repodir, superproject_dir)
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080050 self._manifest_path = os.path.join(self._superproject_path,
51 'superproject_override.xml')
52 self._work_git = os.path.join(self._superproject_path, 'superproject')
Raman Tenneti6a872c92021-01-14 19:17:50 -080053
54 @property
55 def project_shas(self):
56 """Returns a dictionary of projects and their SHAs."""
57 return self._project_shas
58
59 def _Clone(self, url, branch=None):
60 """Do a 'git clone' for the given url and branch.
61
62 Args:
63 url: superproject's url to be passed to git clone.
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080064 branch: The branchname to be passed as argument to git clone.
Raman Tenneti6a872c92021-01-14 19:17:50 -080065
66 Returns:
67 True if 'git clone <url> <branch>' is successful, or False.
68 """
Raman Tenneti9e787532021-02-01 11:47:06 -080069 os.mkdir(self._superproject_path)
Raman Tennetief267722021-02-01 12:37:35 -080070 cmd = ['clone', url, '--filter', 'blob:none']
Raman Tenneti6a872c92021-01-14 19:17:50 -080071 if branch:
72 cmd += ['--branch', branch]
73 p = GitCommand(None,
74 cmd,
75 cwd=self._superproject_path,
76 capture_stdout=True,
77 capture_stderr=True)
78 retval = p.Wait()
79 if retval:
80 # `git clone` is documented to produce an exit status of `128` if
81 # the requested url or branch are not present in the configuration.
82 print('repo: error: git clone call failed with return code: %r, stderr: %r' %
83 (retval, p.stderr), file=sys.stderr)
84 return False
85 return True
86
Raman Tenneti9e787532021-02-01 11:47:06 -080087 def _Pull(self):
88 """Do a 'git pull' to to fetch the latest content.
89
90 Returns:
91 True if 'git pull <branch>' is successful, or False.
92 """
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080093 if not os.path.exists(self._work_git):
94 raise GitError('git pull missing drectory: %s' % self._work_git)
Raman Tenneti9e787532021-02-01 11:47:06 -080095 cmd = ['pull']
96 p = GitCommand(None,
97 cmd,
Raman Tenneti1fd7bc22021-02-04 14:39:38 -080098 cwd=self._work_git,
Raman Tenneti9e787532021-02-01 11:47:06 -080099 capture_stdout=True,
100 capture_stderr=True)
101 retval = p.Wait()
102 if retval:
103 print('repo: error: git pull call failed with return code: %r, stderr: %r' %
104 (retval, p.stderr), file=sys.stderr)
105 return False
106 return True
107
Raman Tenneti6a872c92021-01-14 19:17:50 -0800108 def _LsTree(self):
109 """Returns the data from 'git ls-tree -r HEAD'.
110
111 Works only in git repositories.
112
113 Returns:
114 data: data returned from 'git ls-tree -r HEAD' instead of None.
115 """
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800116 if not os.path.exists(self._work_git):
117 raise GitError('git ls-tree. Missing drectory: %s' % self._work_git)
Raman Tenneti6a872c92021-01-14 19:17:50 -0800118 data = None
119 cmd = ['ls-tree', '-z', '-r', 'HEAD']
120 p = GitCommand(None,
121 cmd,
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800122 cwd=self._work_git,
Raman Tenneti6a872c92021-01-14 19:17:50 -0800123 capture_stdout=True,
124 capture_stderr=True)
125 retval = p.Wait()
126 if retval == 0:
127 data = p.stdout
128 else:
129 # `git clone` is documented to produce an exit status of `128` if
130 # the requested url or branch are not present in the configuration.
131 print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
132 retval, p.stderr), file=sys.stderr)
133 return data
134
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800135 def _GetAllProjectsSHAs(self, url, branch=None):
Raman Tenneti6a872c92021-01-14 19:17:50 -0800136 """Get SHAs for all projects from superproject and save them in _project_shas.
137
138 Args:
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800139 url: superproject's url to be passed to git clone or pull.
140 branch: The branchname to be passed as argument to git clone or pull.
Raman Tenneti6a872c92021-01-14 19:17:50 -0800141
142 Returns:
143 A dictionary with the projects/SHAs instead of None.
144 """
145 if not url:
146 raise ValueError('url argument is not supplied.')
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800147 do_clone = True
Raman Tenneti6a872c92021-01-14 19:17:50 -0800148 if os.path.exists(self._superproject_path):
Raman Tenneti9e787532021-02-01 11:47:06 -0800149 if not self._Pull():
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800150 # If pull fails due to a corrupted git directory, then do a git clone.
151 platform_utils.rmtree(self._superproject_path)
152 else:
153 do_clone = False
154 if do_clone:
Raman Tenneti9e787532021-02-01 11:47:06 -0800155 if not self._Clone(url, branch):
156 raise GitError('git clone failed for url: %s' % url)
Raman Tenneti6a872c92021-01-14 19:17:50 -0800157
158 data = self._LsTree()
159 if not data:
160 raise GitError('git ls-tree failed for url: %s' % url)
161
162 # Parse lines like the following to select lines starting with '160000' and
163 # build a dictionary with project path (last element) and its SHA (3rd element).
164 #
165 # 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
166 # 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
167 shas = {}
168 for line in data.split('\x00'):
169 ls_data = line.split(None, 3)
170 if not ls_data:
171 break
172 if ls_data[0] == '160000':
173 shas[ls_data[3]] = ls_data[2]
174
175 self._project_shas = shas
176 return shas
Raman Tenneti1fd7bc22021-02-04 14:39:38 -0800177
178 def _WriteManfiestFile(self, manifest):
179 """Writes manifest to a file.
180
181 Args:
182 manifest: A Manifest object that is to be written to a file.
183
184 Returns:
185 manifest_path: Path name of the file into which manifest is written instead of None.
186 """
187 if not os.path.exists(self._superproject_path):
188 print('error: missing superproject directory %s' %
189 self._superproject_path,
190 file=sys.stderr)
191 return None
192 manifest_str = manifest.ToXml().toxml()
193 manifest_path = self._manifest_path
194 try:
195 with open(manifest_path, 'w', encoding='utf-8') as fp:
196 fp.write(manifest_str)
197 except IOError as e:
198 print('error: cannot write manifest to %s:\n%s'
199 % (manifest_path, e),
200 file=sys.stderr)
201 return None
202 return manifest_path
203
204 def UpdateProjectsRevisionId(self, manifest, projects, url, branch=None):
205 """Update revisionId of every project in projects with the SHA.
206
207 Args:
208 manifest: A Manifest object that is to be written to a file.
209 projects: List of projects whose revisionId needs to be updated.
210 url: superproject's url to be passed to git clone or fetch.
211 branch: The branchname to be passed as argument to git clone or pull.
212
213 Returns:
214 manifest_path: Path name of the overriding manfiest file instead of None.
215 """
216 try:
217 shas = self._GetAllProjectsSHAs(url=url, branch=branch)
218 except Exception as e:
219 print('error: Cannot get project SHAs for %s: %s: %s' %
220 (url, type(e).__name__, str(e)),
221 file=sys.stderr)
222 return None
223
224 projects_missing_shas = []
225 for project in projects:
226 path = project.relpath
227 if not path:
228 continue
229 sha = shas.get(path)
230 if sha:
231 project.SetRevisionId(sha)
232 else:
233 projects_missing_shas.append(path)
234 if projects_missing_shas:
235 print('error: please file a bug using %s to report missing shas for: %s' %
236 (BUG_REPORT_URL, projects_missing_shas), file=sys.stderr)
237 return None
238
239 manifest_path = self._WriteManfiestFile(manifest)
240 return manifest_path