git_superproject.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. # 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. """Provide functionality to get all projects and their SHAs from Superproject.
  15. For more information on superproject, check out:
  16. https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
  17. Examples:
  18. superproject = Superproject()
  19. project_shas = superproject.GetAllProjectsSHAs()
  20. """
  21. import os
  22. import sys
  23. from error import GitError
  24. from git_command import GitCommand
  25. import platform_utils
  26. class Superproject(object):
  27. """Get SHAs from superproject.
  28. It does a 'git clone' of superproject and 'git ls-tree' to get list of SHAs for all projects.
  29. It contains project_shas which is a dictionary with project/sha entries.
  30. """
  31. def __init__(self, repodir, superproject_dir='exp-superproject'):
  32. """Initializes superproject.
  33. Args:
  34. repodir: Path to the .repo/ dir for holding all internal checkout state.
  35. superproject_dir: Relative path under |repodir| to checkout superproject.
  36. """
  37. self._project_shas = None
  38. self._repodir = os.path.abspath(repodir)
  39. self._superproject_dir = superproject_dir
  40. self._superproject_path = os.path.join(self._repodir, superproject_dir)
  41. @property
  42. def project_shas(self):
  43. """Returns a dictionary of projects and their SHAs."""
  44. return self._project_shas
  45. def _Clone(self, url, branch=None):
  46. """Do a 'git clone' for the given url and branch.
  47. Args:
  48. url: superproject's url to be passed to git clone.
  49. branch: the branchname to be passed as argument to git clone.
  50. Returns:
  51. True if 'git clone <url> <branch>' is successful, or False.
  52. """
  53. cmd = ['clone', url, '--depth', '1']
  54. if branch:
  55. cmd += ['--branch', branch]
  56. p = GitCommand(None,
  57. cmd,
  58. cwd=self._superproject_path,
  59. capture_stdout=True,
  60. capture_stderr=True)
  61. retval = p.Wait()
  62. if retval:
  63. # `git clone` is documented to produce an exit status of `128` if
  64. # the requested url or branch are not present in the configuration.
  65. print('repo: error: git clone call failed with return code: %r, stderr: %r' %
  66. (retval, p.stderr), file=sys.stderr)
  67. return False
  68. return True
  69. def _LsTree(self):
  70. """Returns the data from 'git ls-tree -r HEAD'.
  71. Works only in git repositories.
  72. Returns:
  73. data: data returned from 'git ls-tree -r HEAD' instead of None.
  74. """
  75. git_dir = os.path.join(self._superproject_path, 'superproject')
  76. if not os.path.exists(git_dir):
  77. raise GitError('git ls-tree. Missing drectory: %s' % git_dir)
  78. data = None
  79. cmd = ['ls-tree', '-z', '-r', 'HEAD']
  80. p = GitCommand(None,
  81. cmd,
  82. cwd=git_dir,
  83. capture_stdout=True,
  84. capture_stderr=True)
  85. retval = p.Wait()
  86. if retval == 0:
  87. data = p.stdout
  88. else:
  89. # `git clone` is documented to produce an exit status of `128` if
  90. # the requested url or branch are not present in the configuration.
  91. print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
  92. retval, p.stderr), file=sys.stderr)
  93. return data
  94. def GetAllProjectsSHAs(self, url, branch=None):
  95. """Get SHAs for all projects from superproject and save them in _project_shas.
  96. Args:
  97. url: superproject's url to be passed to git clone.
  98. branch: the branchname to be passed as argument to git clone.
  99. Returns:
  100. A dictionary with the projects/SHAs instead of None.
  101. """
  102. if not url:
  103. raise ValueError('url argument is not supplied.')
  104. if os.path.exists(self._superproject_path):
  105. platform_utils.rmtree(self._superproject_path)
  106. os.mkdir(self._superproject_path)
  107. # TODO(rtenneti): we shouldn't be cloning the repo from scratch every time.
  108. if not self._Clone(url, branch):
  109. raise GitError('git clone failed for url: %s' % url)
  110. data = self._LsTree()
  111. if not data:
  112. raise GitError('git ls-tree failed for url: %s' % url)
  113. # Parse lines like the following to select lines starting with '160000' and
  114. # build a dictionary with project path (last element) and its SHA (3rd element).
  115. #
  116. # 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
  117. # 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
  118. shas = {}
  119. for line in data.split('\x00'):
  120. ls_data = line.split(None, 3)
  121. if not ls_data:
  122. break
  123. if ls_data[0] == '160000':
  124. shas[ls_data[3]] = ls_data[2]
  125. self._project_shas = shas
  126. return shas