summaryrefslogtreecommitdiffstats
path: root/git_superproject.py
blob: 465d1f8773762f43e0ba4f84a56c8229dee79056 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# Copyright (C) 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Provide functionality to get all projects and their SHAs from Superproject.

For more information on superproject, check out:
https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects

Examples:
  superproject = Superproject()
  project_shas = superproject.GetAllProjectsSHAs()
"""

import os
import sys

from error import BUG_REPORT_URL, GitError
from git_command import GitCommand
import platform_utils


class Superproject(object):
  """Get SHAs from superproject.

  It does a 'git clone' of superproject and 'git ls-tree' to get list of SHAs for all projects.
  It contains project_shas which is a dictionary with project/sha entries.
  """
  def __init__(self, repodir, superproject_dir='exp-superproject'):
    """Initializes superproject.

    Args:
      repodir: Path to the .repo/ dir for holding all internal checkout state.
      superproject_dir: Relative path under |repodir| to checkout superproject.
    """
    self._project_shas = None
    self._repodir = os.path.abspath(repodir)
    self._superproject_dir = superproject_dir
    self._superproject_path = os.path.join(self._repodir, superproject_dir)
    self._manifest_path = os.path.join(self._superproject_path,
                                       'superproject_override.xml')
    self._work_git = os.path.join(self._superproject_path, 'superproject')

  @property
  def project_shas(self):
    """Returns a dictionary of projects and their SHAs."""
    return self._project_shas

  def _Clone(self, url, branch=None):
    """Do a 'git clone' for the given url and branch.

    Args:
      url: superproject's url to be passed to git clone.
      branch: The branchname to be passed as argument to git clone.

    Returns:
      True if 'git clone <url> <branch>' is successful, or False.
    """
    os.mkdir(self._superproject_path)
    cmd = ['clone', url, '--filter', 'blob:none']
    if branch:
      cmd += ['--branch', branch]
    p = GitCommand(None,
                   cmd,
                   cwd=self._superproject_path,
                   capture_stdout=True,
                   capture_stderr=True)
    retval = p.Wait()
    if retval:
      # `git clone` is documented to produce an exit status of `128` if
      # the requested url or branch are not present in the configuration.
      print('repo: error: git clone call failed with return code: %r, stderr: %r' %
            (retval, p.stderr), file=sys.stderr)
      return False
    return True

  def _Pull(self):
    """Do a 'git pull' to to fetch the latest content.

    Returns:
      True if 'git pull <branch>' is successful, or False.
    """
    if not os.path.exists(self._work_git):
      raise GitError('git pull missing drectory: %s' % self._work_git)
    cmd = ['pull']
    p = GitCommand(None,
                   cmd,
                   cwd=self._work_git,
                   capture_stdout=True,
                   capture_stderr=True)
    retval = p.Wait()
    if retval:
      print('repo: error: git pull call failed with return code: %r, stderr: %r' %
            (retval, p.stderr), file=sys.stderr)
      return False
    return True

  def _LsTree(self):
    """Returns the data from 'git ls-tree -r HEAD'.

    Works only in git repositories.

    Returns:
      data: data returned from 'git ls-tree -r HEAD' instead of None.
    """
    if not os.path.exists(self._work_git):
      raise GitError('git ls-tree. Missing drectory: %s' % self._work_git)
    data = None
    cmd = ['ls-tree', '-z', '-r', 'HEAD']
    p = GitCommand(None,
                   cmd,
                   cwd=self._work_git,
                   capture_stdout=True,
                   capture_stderr=True)
    retval = p.Wait()
    if retval == 0:
      data = p.stdout
    else:
      # `git clone` is documented to produce an exit status of `128` if
      # the requested url or branch are not present in the configuration.
      print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
          retval, p.stderr), file=sys.stderr)
    return data

  def _GetAllProjectsSHAs(self, url, branch=None):
    """Get SHAs for all projects from superproject and save them in _project_shas.

    Args:
      url: superproject's url to be passed to git clone or pull.
      branch: The branchname to be passed as argument to git clone or pull.

    Returns:
      A dictionary with the projects/SHAs instead of None.
    """
    if not url:
      raise ValueError('url argument is not supplied.')
    do_clone = True
    if os.path.exists(self._superproject_path):
      if not self._Pull():
        # If pull fails due to a corrupted git directory, then do a git clone.
        platform_utils.rmtree(self._superproject_path)
      else:
        do_clone = False
    if do_clone:
      if not self._Clone(url, branch):
        raise GitError('git clone failed for url: %s' % url)

    data = self._LsTree()
    if not data:
      raise GitError('git ls-tree failed for url: %s' % url)

    # Parse lines like the following to select lines starting with '160000' and
    # build a dictionary with project path (last element) and its SHA (3rd element).
    #
    # 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
    # 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
    shas = {}
    for line in data.split('\x00'):
      ls_data = line.split(None, 3)
      if not ls_data:
        break
      if ls_data[0] == '160000':
        shas[ls_data[3]] = ls_data[2]

    self._project_shas = shas
    return shas

  def _WriteManfiestFile(self, manifest):
    """Writes manifest to a file.

    Args:
      manifest: A Manifest object that is to be written to a file.

    Returns:
      manifest_path: Path name of the file into which manifest is written instead of None.
    """
    if not os.path.exists(self._superproject_path):
      print('error: missing superproject directory %s' %
            self._superproject_path,
            file=sys.stderr)
      return None
    manifest_str = manifest.ToXml().toxml()
    manifest_path = self._manifest_path
    try:
      with open(manifest_path, 'w', encoding='utf-8') as fp:
        fp.write(manifest_str)
    except IOError as e:
      print('error: cannot write manifest to %s:\n%s'
            % (manifest_path, e),
            file=sys.stderr)
      return None
    return manifest_path

  def UpdateProjectsRevisionId(self, manifest, projects, url, branch=None):
    """Update revisionId of every project in projects with the SHA.

    Args:
      manifest: A Manifest object that is to be written to a file.
      projects: List of projects whose revisionId needs to be updated.
      url: superproject's url to be passed to git clone or fetch.
      branch: The branchname to be passed as argument to git clone or pull.

    Returns:
      manifest_path: Path name of the overriding manfiest file instead of None.
    """
    try:
      shas = self._GetAllProjectsSHAs(url=url, branch=branch)
    except Exception as e:
      print('error: Cannot get project SHAs for %s: %s: %s' %
            (url, type(e).__name__, str(e)),
            file=sys.stderr)
      return None

    projects_missing_shas = []
    for project in projects:
      path = project.relpath
      if not path:
        continue
      sha = shas.get(path)
      if sha:
        project.SetRevisionId(sha)
      else:
        projects_missing_shas.append(path)
    if projects_missing_shas:
      print('error: please file a bug using %s to report missing shas for: %s' %
            (BUG_REPORT_URL, projects_missing_shas), file=sys.stderr)
      return None

    manifest_path = self._WriteManfiestFile(manifest)
    return manifest_path