blob: e2045cfd75adef1059359587b6a0398283db4005 [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
28from error import GitError
29from git_command import GitCommand
Raman Tenneti6a872c92021-01-14 19:17:50 -080030
31
32class Superproject(object):
33 """Get SHAs from superproject.
34
35 It does a 'git clone' of superproject and 'git ls-tree' to get list of SHAs for all projects.
36 It contains project_shas which is a dictionary with project/sha entries.
37 """
38 def __init__(self, repodir, superproject_dir='exp-superproject'):
39 """Initializes superproject.
40
41 Args:
42 repodir: Path to the .repo/ dir for holding all internal checkout state.
43 superproject_dir: Relative path under |repodir| to checkout superproject.
44 """
45 self._project_shas = None
46 self._repodir = os.path.abspath(repodir)
47 self._superproject_dir = superproject_dir
48 self._superproject_path = os.path.join(self._repodir, superproject_dir)
49
50 @property
51 def project_shas(self):
52 """Returns a dictionary of projects and their SHAs."""
53 return self._project_shas
54
55 def _Clone(self, url, branch=None):
56 """Do a 'git clone' for the given url and branch.
57
58 Args:
59 url: superproject's url to be passed to git clone.
60 branch: the branchname to be passed as argument to git clone.
61
62 Returns:
63 True if 'git clone <url> <branch>' is successful, or False.
64 """
Raman Tenneti9e787532021-02-01 11:47:06 -080065 os.mkdir(self._superproject_path)
66 cmd = ['clone', url, '--filter', 'blob:none']
Raman Tenneti6a872c92021-01-14 19:17:50 -080067 if branch:
68 cmd += ['--branch', branch]
69 p = GitCommand(None,
70 cmd,
71 cwd=self._superproject_path,
72 capture_stdout=True,
73 capture_stderr=True)
74 retval = p.Wait()
75 if retval:
76 # `git clone` is documented to produce an exit status of `128` if
77 # the requested url or branch are not present in the configuration.
78 print('repo: error: git clone call failed with return code: %r, stderr: %r' %
79 (retval, p.stderr), file=sys.stderr)
80 return False
81 return True
82
Raman Tenneti9e787532021-02-01 11:47:06 -080083 def _Pull(self):
84 """Do a 'git pull' to to fetch the latest content.
85
86 Returns:
87 True if 'git pull <branch>' is successful, or False.
88 """
89 git_dir = os.path.join(self._superproject_path, 'superproject')
90 if not os.path.exists(git_dir):
91 raise GitError('git pull. Missing drectory: %s' % git_dir)
92 cmd = ['pull']
93 p = GitCommand(None,
94 cmd,
95 cwd=git_dir,
96 capture_stdout=True,
97 capture_stderr=True)
98 retval = p.Wait()
99 if retval:
100 print('repo: error: git pull call failed with return code: %r, stderr: %r' %
101 (retval, p.stderr), file=sys.stderr)
102 return False
103 return True
104
Raman Tenneti6a872c92021-01-14 19:17:50 -0800105 def _LsTree(self):
106 """Returns the data from 'git ls-tree -r HEAD'.
107
108 Works only in git repositories.
109
110 Returns:
111 data: data returned from 'git ls-tree -r HEAD' instead of None.
112 """
113 git_dir = os.path.join(self._superproject_path, 'superproject')
114 if not os.path.exists(git_dir):
115 raise GitError('git ls-tree. Missing drectory: %s' % git_dir)
116 data = None
117 cmd = ['ls-tree', '-z', '-r', 'HEAD']
118 p = GitCommand(None,
119 cmd,
120 cwd=git_dir,
121 capture_stdout=True,
122 capture_stderr=True)
123 retval = p.Wait()
124 if retval == 0:
125 data = p.stdout
126 else:
127 # `git clone` is documented to produce an exit status of `128` if
128 # the requested url or branch are not present in the configuration.
129 print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
130 retval, p.stderr), file=sys.stderr)
131 return data
132
133 def GetAllProjectsSHAs(self, url, branch=None):
134 """Get SHAs for all projects from superproject and save them in _project_shas.
135
136 Args:
137 url: superproject's url to be passed to git clone.
138 branch: the branchname to be passed as argument to git clone.
139
140 Returns:
141 A dictionary with the projects/SHAs instead of None.
142 """
143 if not url:
144 raise ValueError('url argument is not supplied.')
145 if os.path.exists(self._superproject_path):
Raman Tenneti9e787532021-02-01 11:47:06 -0800146 if not self._Pull():
147 raise GitError('git pull failed for url: %s' % url)
148 else:
149 if not self._Clone(url, branch):
150 raise GitError('git clone failed for url: %s' % url)
Raman Tenneti6a872c92021-01-14 19:17:50 -0800151
152 data = self._LsTree()
153 if not data:
154 raise GitError('git ls-tree failed for url: %s' % url)
155
156 # Parse lines like the following to select lines starting with '160000' and
157 # build a dictionary with project path (last element) and its SHA (3rd element).
158 #
159 # 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
160 # 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
161 shas = {}
162 for line in data.split('\x00'):
163 ls_data = line.split(None, 3)
164 if not ls_data:
165 break
166 if ls_data[0] == '160000':
167 shas[ls_data[3]] = ls_data[2]
168
169 self._project_shas = shas
170 return shas