summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--error.py4
-rw-r--r--git_superproject.py149
-rw-r--r--project.py3
-rw-r--r--subcmds/sync.py40
-rw-r--r--tests/test_git_superproject.py82
-rw-r--r--tests/test_manifest_xml.py14
6 files changed, 285 insertions, 7 deletions
diff --git a/error.py b/error.py
index 225eb59d..8bb64b8f 100644
--- a/error.py
+++ b/error.py
@@ -13,6 +13,10 @@
13# limitations under the License. 13# limitations under the License.
14 14
15 15
16# URL to file bug reports for repo tool issues.
17BUG_REPORT_URL = 'https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue'
18
19
16class ManifestParseError(Exception): 20class ManifestParseError(Exception):
17 """Failed to parse the manifest file. 21 """Failed to parse the manifest file.
18 """ 22 """
diff --git a/git_superproject.py b/git_superproject.py
new file mode 100644
index 00000000..3e87e929
--- /dev/null
+++ b/git_superproject.py
@@ -0,0 +1,149 @@
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
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
30import platform_utils
31
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)
50
51 @property
52 def project_shas(self):
53 """Returns a dictionary of projects and their SHAs."""
54 return self._project_shas
55
56 def _Clone(self, url, branch=None):
57 """Do a 'git clone' for the given url and branch.
58
59 Args:
60 url: superproject's url to be passed to git clone.
61 branch: the branchname to be passed as argument to git clone.
62
63 Returns:
64 True if 'git clone <url> <branch>' is successful, or False.
65 """
66 cmd = ['clone', url, '--depth', '1']
67 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
83 def _LsTree(self):
84 """Returns the data from 'git ls-tree -r HEAD'.
85
86 Works only in git repositories.
87
88 Returns:
89 data: data returned from 'git ls-tree -r HEAD' instead of None.
90 """
91 git_dir = os.path.join(self._superproject_path, 'superproject')
92 if not os.path.exists(git_dir):
93 raise GitError('git ls-tree. Missing drectory: %s' % git_dir)
94 data = None
95 cmd = ['ls-tree', '-z', '-r', 'HEAD']
96 p = GitCommand(None,
97 cmd,
98 cwd=git_dir,
99 capture_stdout=True,
100 capture_stderr=True)
101 retval = p.Wait()
102 if retval == 0:
103 data = p.stdout
104 else:
105 # `git clone` is documented to produce an exit status of `128` if
106 # the requested url or branch are not present in the configuration.
107 print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
108 retval, p.stderr), file=sys.stderr)
109 return data
110
111 def GetAllProjectsSHAs(self, url, branch=None):
112 """Get SHAs for all projects from superproject and save them in _project_shas.
113
114 Args:
115 url: superproject's url to be passed to git clone.
116 branch: the branchname to be passed as argument to git clone.
117
118 Returns:
119 A dictionary with the projects/SHAs instead of None.
120 """
121 if not url:
122 raise ValueError('url argument is not supplied.')
123 if os.path.exists(self._superproject_path):
124 platform_utils.rmtree(self._superproject_path)
125 os.mkdir(self._superproject_path)
126
127 # TODO(rtenneti): we shouldn't be cloning the repo from scratch every time.
128 if not self._Clone(url, branch):
129 raise GitError('git clone failed for url: %s' % url)
130
131 data = self._LsTree()
132 if not data:
133 raise GitError('git ls-tree failed for url: %s' % url)
134
135 # Parse lines like the following to select lines starting with '160000' and
136 # build a dictionary with project path (last element) and its SHA (3rd element).
137 #
138 # 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
139 # 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
140 shas = {}
141 for line in data.split('\x00'):
142 ls_data = line.split(None, 3)
143 if not ls_data:
144 break
145 if ls_data[0] == '160000':
146 shas[ls_data[3]] = ls_data[2]
147
148 self._project_shas = shas
149 return shas
diff --git a/project.py b/project.py
index 6c6534d8..17c75b4d 100644
--- a/project.py
+++ b/project.py
@@ -1197,6 +1197,9 @@ class Project(object):
1197 raise ManifestInvalidRevisionError('revision %s in %s not found' % 1197 raise ManifestInvalidRevisionError('revision %s in %s not found' %
1198 (self.revisionExpr, self.name)) 1198 (self.revisionExpr, self.name))
1199 1199
1200 def SetRevisionId(self, revisionId):
1201 self.revisionId = revisionId
1202
1200 def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False): 1203 def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
1201 """Perform only the local IO portion of the sync process. 1204 """Perform only the local IO portion of the sync process.
1202 Network access is not required. 1205 Network access is not required.
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 3482946d..d6b8f9dc 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -51,11 +51,12 @@ import event_log
51from git_command import GIT, git_require 51from git_command import GIT, git_require
52from git_config import GetUrlCookieFile 52from git_config import GetUrlCookieFile
53from git_refs import R_HEADS, HEAD 53from git_refs import R_HEADS, HEAD
54import git_superproject
54import gitc_utils 55import gitc_utils
55from project import Project 56from project import Project
56from project import RemoteSpec 57from project import RemoteSpec
57from command import Command, MirrorSafeCommand 58from command import Command, MirrorSafeCommand
58from error import RepoChangedException, GitError, ManifestParseError 59from error import BUG_REPORT_URL, RepoChangedException, GitError, ManifestParseError
59import platform_utils 60import platform_utils
60from project import SyncBuffer 61from project import SyncBuffer
61from progress import Progress 62from progress import Progress
@@ -241,6 +242,8 @@ later is required to fix a server side protocol bug.
241 p.add_option('--fetch-submodules', 242 p.add_option('--fetch-submodules',
242 dest='fetch_submodules', action='store_true', 243 dest='fetch_submodules', action='store_true',
243 help='fetch submodules from server') 244 help='fetch submodules from server')
245 p.add_option('--use-superproject', action='store_true',
246 help='use the manifest superproject to sync projects')
244 p.add_option('--no-tags', 247 p.add_option('--no-tags',
245 dest='tags', default=True, action='store_false', 248 dest='tags', default=True, action='store_false',
246 help="don't fetch tags") 249 help="don't fetch tags")
@@ -894,6 +897,41 @@ later is required to fix a server side protocol bug.
894 missing_ok=True, 897 missing_ok=True,
895 submodules_ok=opt.fetch_submodules) 898 submodules_ok=opt.fetch_submodules)
896 899
900 if opt.use_superproject:
901 if not self.manifest.superproject:
902 print('error: superproject tag is not defined in manifest.xml',
903 file=sys.stderr)
904 sys.exit(1)
905 print('WARNING: --use-superproject is experimental and not '
906 'for general use', file=sys.stderr)
907 superproject_url = self.manifest.superproject['remote'].url
908 if not superproject_url:
909 print('error: superproject URL is not defined in manifest.xml',
910 file=sys.stderr)
911 sys.exit(1)
912 superproject = git_superproject.Superproject(self.manifest.repodir)
913 try:
914 superproject_shas = superproject.GetAllProjectsSHAs(url=superproject_url)
915 except Exception as e:
916 print('error: Cannot get project SHAs for %s: %s: %s' %
917 (superproject_url, type(e).__name__, str(e)),
918 file=sys.stderr)
919 sys.exit(1)
920 projects_missing_shas = []
921 for project in all_projects:
922 path = project.relpath
923 if not path:
924 continue
925 sha = superproject_shas.get(path)
926 if sha:
927 project.SetRevisionId(sha)
928 else:
929 projects_missing_shas.append(path)
930 if projects_missing_shas:
931 print('error: please file a bug using %s to report missing shas for: %s' %
932 (BUG_REPORT_URL, projects_missing_shas), file=sys.stderr)
933 sys.exit(1)
934
897 err_network_sync = False 935 err_network_sync = False
898 err_update_projects = False 936 err_update_projects = False
899 err_checkout = False 937 err_checkout = False
diff --git a/tests/test_git_superproject.py b/tests/test_git_superproject.py
new file mode 100644
index 00000000..67a75a17
--- /dev/null
+++ b/tests/test_git_superproject.py
@@ -0,0 +1,82 @@
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
15"""Unittests for the git_superproject.py module."""
16
17import os
18import tempfile
19import unittest
20from unittest import mock
21
22from error import GitError
23import git_superproject
24import platform_utils
25
26
27class SuperprojectTestCase(unittest.TestCase):
28 """TestCase for the Superproject module."""
29
30 def setUp(self):
31 """Set up superproject every time."""
32 self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
33 self.repodir = os.path.join(self.tempdir, '.repo')
34 os.mkdir(self.repodir)
35 self._superproject = git_superproject.Superproject(self.repodir)
36
37 def tearDown(self):
38 """Tear down superproject every time."""
39 platform_utils.rmtree(self.tempdir)
40
41 def test_superproject_get_project_shas_no_url(self):
42 """Test with no url."""
43 with self.assertRaises(ValueError):
44 self._superproject.GetAllProjectsSHAs(url=None)
45
46 def test_superproject_get_project_shas_invalid_url(self):
47 """Test with an invalid url."""
48 with self.assertRaises(GitError):
49 self._superproject.GetAllProjectsSHAs(url='localhost')
50
51 def test_superproject_get_project_shas_invalid_branch(self):
52 """Test with an invalid branch."""
53 with self.assertRaises(GitError):
54 self._superproject.GetAllProjectsSHAs(
55 url='sso://android/platform/superproject',
56 branch='junk')
57
58 def test_superproject_get_project_shas_mock_clone(self):
59 """Test with _Clone failing."""
60 with self.assertRaises(GitError):
61 with mock.patch.object(self._superproject, '_Clone', return_value=False):
62 self._superproject.GetAllProjectsSHAs(url='localhost')
63
64 def test_superproject_get_project_shas_mock_ls_tree(self):
65 """Test with LsTree being a mock."""
66 data = ('120000 blob 158258bdf146f159218e2b90f8b699c4d85b5804\tAndroid.bp\x00'
67 '160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00'
68 '160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tbootable/recovery\x00'
69 '120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00'
70 '160000 commit ade9b7a0d874e25fff4bf2552488825c6f111928\tbuild/bazel\x00')
71 with mock.patch.object(self._superproject, '_Clone', return_value=True):
72 with mock.patch.object(self._superproject, '_LsTree', return_value=data):
73 shas = self._superproject.GetAllProjectsSHAs(url='localhost', branch='junk')
74 self.assertEqual(shas, {
75 'art': '2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea',
76 'bootable/recovery': 'e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06',
77 'build/bazel': 'ade9b7a0d874e25fff4bf2552488825c6f111928'
78 })
79
80
81if __name__ == '__main__':
82 unittest.main()
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py
index e2c83af9..370eb4f5 100644
--- a/tests/test_manifest_xml.py
+++ b/tests/test_manifest_xml.py
@@ -232,6 +232,7 @@ class XmlManifestTests(unittest.TestCase):
232""") 232""")
233 self.assertEqual(manifest.superproject['name'], 'superproject') 233 self.assertEqual(manifest.superproject['name'], 'superproject')
234 self.assertEqual(manifest.superproject['remote'].name, 'test-remote') 234 self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
235 self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
235 self.assertEqual( 236 self.assertEqual(
236 manifest.ToXml().toxml(), 237 manifest.ToXml().toxml(),
237 '<?xml version="1.0" ?><manifest>' + 238 '<?xml version="1.0" ?><manifest>' +
@@ -245,20 +246,21 @@ class XmlManifestTests(unittest.TestCase):
245 manifest = self.getXmlManifest(""" 246 manifest = self.getXmlManifest("""
246<manifest> 247<manifest>
247 <remote name="default-remote" fetch="http://localhost" /> 248 <remote name="default-remote" fetch="http://localhost" />
248 <remote name="test-remote" fetch="http://localhost" /> 249 <remote name="superproject-remote" fetch="http://localhost" />
249 <default remote="default-remote" revision="refs/heads/main" /> 250 <default remote="default-remote" revision="refs/heads/main" />
250 <superproject name="superproject" remote="test-remote"/> 251 <superproject name="platform/superproject" remote="superproject-remote"/>
251</manifest> 252</manifest>
252""") 253""")
253 self.assertEqual(manifest.superproject['name'], 'superproject') 254 self.assertEqual(manifest.superproject['name'], 'platform/superproject')
254 self.assertEqual(manifest.superproject['remote'].name, 'test-remote') 255 self.assertEqual(manifest.superproject['remote'].name, 'superproject-remote')
256 self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject')
255 self.assertEqual( 257 self.assertEqual(
256 manifest.ToXml().toxml(), 258 manifest.ToXml().toxml(),
257 '<?xml version="1.0" ?><manifest>' + 259 '<?xml version="1.0" ?><manifest>' +
258 '<remote name="default-remote" fetch="http://localhost"/>' + 260 '<remote name="default-remote" fetch="http://localhost"/>' +
259 '<remote name="test-remote" fetch="http://localhost"/>' + 261 '<remote name="superproject-remote" fetch="http://localhost"/>' +
260 '<default remote="default-remote" revision="refs/heads/main"/>' + 262 '<default remote="default-remote" revision="refs/heads/main"/>' +
261 '<superproject name="superproject" remote="test-remote"/>' + 263 '<superproject name="platform/superproject" remote="superproject-remote"/>' +
262 '</manifest>') 264 '</manifest>')
263 265
264 def test_superproject_with_defalut_remote(self): 266 def test_superproject_with_defalut_remote(self):