summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--git_superproject.py100
-rw-r--r--subcmds/sync.py81
-rw-r--r--tests/test_git_superproject.py89
3 files changed, 212 insertions, 58 deletions
diff --git a/git_superproject.py b/git_superproject.py
index e2045cfd..465d1f87 100644
--- a/git_superproject.py
+++ b/git_superproject.py
@@ -25,8 +25,9 @@ Examples:
25import os 25import os
26import sys 26import sys
27 27
28from error import GitError 28from error import BUG_REPORT_URL, GitError
29from git_command import GitCommand 29from git_command import GitCommand
30import platform_utils
30 31
31 32
32class Superproject(object): 33class Superproject(object):
@@ -46,6 +47,9 @@ class Superproject(object):
46 self._repodir = os.path.abspath(repodir) 47 self._repodir = os.path.abspath(repodir)
47 self._superproject_dir = superproject_dir 48 self._superproject_dir = superproject_dir
48 self._superproject_path = os.path.join(self._repodir, superproject_dir) 49 self._superproject_path = os.path.join(self._repodir, superproject_dir)
50 self._manifest_path = os.path.join(self._superproject_path,
51 'superproject_override.xml')
52 self._work_git = os.path.join(self._superproject_path, 'superproject')
49 53
50 @property 54 @property
51 def project_shas(self): 55 def project_shas(self):
@@ -57,7 +61,7 @@ class Superproject(object):
57 61
58 Args: 62 Args:
59 url: superproject's url to be passed to git clone. 63 url: superproject's url to be passed to git clone.
60 branch: the branchname to be passed as argument to git clone. 64 branch: The branchname to be passed as argument to git clone.
61 65
62 Returns: 66 Returns:
63 True if 'git clone <url> <branch>' is successful, or False. 67 True if 'git clone <url> <branch>' is successful, or False.
@@ -86,13 +90,12 @@ class Superproject(object):
86 Returns: 90 Returns:
87 True if 'git pull <branch>' is successful, or False. 91 True if 'git pull <branch>' is successful, or False.
88 """ 92 """
89 git_dir = os.path.join(self._superproject_path, 'superproject') 93 if not os.path.exists(self._work_git):
90 if not os.path.exists(git_dir): 94 raise GitError('git pull missing drectory: %s' % self._work_git)
91 raise GitError('git pull. Missing drectory: %s' % git_dir)
92 cmd = ['pull'] 95 cmd = ['pull']
93 p = GitCommand(None, 96 p = GitCommand(None,
94 cmd, 97 cmd,
95 cwd=git_dir, 98 cwd=self._work_git,
96 capture_stdout=True, 99 capture_stdout=True,
97 capture_stderr=True) 100 capture_stderr=True)
98 retval = p.Wait() 101 retval = p.Wait()
@@ -110,14 +113,13 @@ class Superproject(object):
110 Returns: 113 Returns:
111 data: data returned from 'git ls-tree -r HEAD' instead of None. 114 data: data returned from 'git ls-tree -r HEAD' instead of None.
112 """ 115 """
113 git_dir = os.path.join(self._superproject_path, 'superproject') 116 if not os.path.exists(self._work_git):
114 if not os.path.exists(git_dir): 117 raise GitError('git ls-tree. Missing drectory: %s' % self._work_git)
115 raise GitError('git ls-tree. Missing drectory: %s' % git_dir)
116 data = None 118 data = None
117 cmd = ['ls-tree', '-z', '-r', 'HEAD'] 119 cmd = ['ls-tree', '-z', '-r', 'HEAD']
118 p = GitCommand(None, 120 p = GitCommand(None,
119 cmd, 121 cmd,
120 cwd=git_dir, 122 cwd=self._work_git,
121 capture_stdout=True, 123 capture_stdout=True,
122 capture_stderr=True) 124 capture_stderr=True)
123 retval = p.Wait() 125 retval = p.Wait()
@@ -130,22 +132,26 @@ class Superproject(object):
130 retval, p.stderr), file=sys.stderr) 132 retval, p.stderr), file=sys.stderr)
131 return data 133 return data
132 134
133 def GetAllProjectsSHAs(self, url, branch=None): 135 def _GetAllProjectsSHAs(self, url, branch=None):
134 """Get SHAs for all projects from superproject and save them in _project_shas. 136 """Get SHAs for all projects from superproject and save them in _project_shas.
135 137
136 Args: 138 Args:
137 url: superproject's url to be passed to git clone. 139 url: superproject's url to be passed to git clone or pull.
138 branch: the branchname to be passed as argument to git clone. 140 branch: The branchname to be passed as argument to git clone or pull.
139 141
140 Returns: 142 Returns:
141 A dictionary with the projects/SHAs instead of None. 143 A dictionary with the projects/SHAs instead of None.
142 """ 144 """
143 if not url: 145 if not url:
144 raise ValueError('url argument is not supplied.') 146 raise ValueError('url argument is not supplied.')
147 do_clone = True
145 if os.path.exists(self._superproject_path): 148 if os.path.exists(self._superproject_path):
146 if not self._Pull(): 149 if not self._Pull():
147 raise GitError('git pull failed for url: %s' % url) 150 # If pull fails due to a corrupted git directory, then do a git clone.
148 else: 151 platform_utils.rmtree(self._superproject_path)
152 else:
153 do_clone = False
154 if do_clone:
149 if not self._Clone(url, branch): 155 if not self._Clone(url, branch):
150 raise GitError('git clone failed for url: %s' % url) 156 raise GitError('git clone failed for url: %s' % url)
151 157
@@ -168,3 +174,67 @@ class Superproject(object):
168 174
169 self._project_shas = shas 175 self._project_shas = shas
170 return shas 176 return shas
177
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
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 225e565a..c0f605a8 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -56,7 +56,7 @@ import gitc_utils
56from project import Project 56from project import Project
57from project import RemoteSpec 57from project import RemoteSpec
58from command import Command, MirrorSafeCommand 58from command import Command, MirrorSafeCommand
59from error import BUG_REPORT_URL, RepoChangedException, GitError, ManifestParseError 59from error import RepoChangedException, GitError, ManifestParseError
60import platform_utils 60import platform_utils
61from project import SyncBuffer 61from project import SyncBuffer
62from progress import Progress 62from progress import Progress
@@ -271,6 +271,47 @@ later is required to fix a server side protocol bug.
271 dest='repo_upgraded', action='store_true', 271 dest='repo_upgraded', action='store_true',
272 help=SUPPRESS_HELP) 272 help=SUPPRESS_HELP)
273 273
274 def _UpdateProjectsRevisionId(self, opt, args):
275 """Update revisionId of every project with the SHA from superproject.
276
277 This function updates each project's revisionId with SHA from superproject.
278 It writes the updated manifest into a file and reloads the manifest from it.
279
280 Args:
281 opt: Program options returned from optparse. See _Options().
282 args: Arguments to pass to GetProjects. See the GetProjects
283 docstring for details.
284
285 Returns:
286 Returns path to the overriding manifest file.
287 """
288 if not self.manifest.superproject:
289 print('error: superproject tag is not defined in manifest.xml',
290 file=sys.stderr)
291 sys.exit(1)
292 print('WARNING: --use-superproject is experimental and not '
293 'for general use', file=sys.stderr)
294
295 superproject_url = self.manifest.superproject['remote'].url
296 if not superproject_url:
297 print('error: superproject URL is not defined in manifest.xml',
298 file=sys.stderr)
299 sys.exit(1)
300
301 superproject = git_superproject.Superproject(self.manifest.repodir)
302 all_projects = self.GetProjects(args,
303 missing_ok=True,
304 submodules_ok=opt.fetch_submodules)
305 manifest_path = superproject.UpdateProjectsRevisionId(self.manifest,
306 all_projects,
307 url=superproject_url)
308 if not manifest_path:
309 print('error: Update of revsionId from superproject has failed',
310 file=sys.stderr)
311 sys.exit(1)
312 self._ReloadManifest(manifest_path)
313 return manifest_path
314
274 def _FetchProjectList(self, opt, projects, sem, *args, **kwargs): 315 def _FetchProjectList(self, opt, projects, sem, *args, **kwargs):
275 """Main function of the fetch threads. 316 """Main function of the fetch threads.
276 317
@@ -859,6 +900,9 @@ later is required to fix a server side protocol bug.
859 else: 900 else:
860 self._UpdateManifestProject(opt, mp, manifest_name) 901 self._UpdateManifestProject(opt, mp, manifest_name)
861 902
903 if opt.use_superproject:
904 manifest_name = self._UpdateProjectsRevisionId(opt, args)
905
862 if self.gitc_manifest: 906 if self.gitc_manifest:
863 gitc_manifest_projects = self.GetProjects(args, 907 gitc_manifest_projects = self.GetProjects(args,
864 missing_ok=True) 908 missing_ok=True)
@@ -898,41 +942,6 @@ later is required to fix a server side protocol bug.
898 missing_ok=True, 942 missing_ok=True,
899 submodules_ok=opt.fetch_submodules) 943 submodules_ok=opt.fetch_submodules)
900 944
901 if opt.use_superproject:
902 if not self.manifest.superproject:
903 print('error: superproject tag is not defined in manifest.xml',
904 file=sys.stderr)
905 sys.exit(1)
906 print('WARNING: --use-superproject is experimental and not '
907 'for general use', file=sys.stderr)
908 superproject_url = self.manifest.superproject['remote'].url
909 if not superproject_url:
910 print('error: superproject URL is not defined in manifest.xml',
911 file=sys.stderr)
912 sys.exit(1)
913 superproject = git_superproject.Superproject(self.manifest.repodir)
914 try:
915 superproject_shas = superproject.GetAllProjectsSHAs(url=superproject_url)
916 except Exception as e:
917 print('error: Cannot get project SHAs for %s: %s: %s' %
918 (superproject_url, type(e).__name__, str(e)),
919 file=sys.stderr)
920 sys.exit(1)
921 projects_missing_shas = []
922 for project in all_projects:
923 path = project.relpath
924 if not path:
925 continue
926 sha = superproject_shas.get(path)
927 if sha:
928 project.SetRevisionId(sha)
929 else:
930 projects_missing_shas.append(path)
931 if projects_missing_shas:
932 print('error: please file a bug using %s to report missing shas for: %s' %
933 (BUG_REPORT_URL, projects_missing_shas), file=sys.stderr)
934 sys.exit(1)
935
936 err_network_sync = False 945 err_network_sync = False
937 err_update_projects = False 946 err_update_projects = False
938 err_checkout = False 947 err_checkout = False
diff --git a/tests/test_git_superproject.py b/tests/test_git_superproject.py
index 4012ec26..d2c2f501 100644
--- a/tests/test_git_superproject.py
+++ b/tests/test_git_superproject.py
@@ -21,6 +21,7 @@ from unittest import mock
21 21
22from error import GitError 22from error import GitError
23import git_superproject 23import git_superproject
24import manifest_xml
24import platform_utils 25import platform_utils
25 26
26 27
@@ -31,27 +32,43 @@ class SuperprojectTestCase(unittest.TestCase):
31 """Set up superproject every time.""" 32 """Set up superproject every time."""
32 self.tempdir = tempfile.mkdtemp(prefix='repo_tests') 33 self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
33 self.repodir = os.path.join(self.tempdir, '.repo') 34 self.repodir = os.path.join(self.tempdir, '.repo')
34 os.mkdir(self.repodir)
35 self._superproject = git_superproject.Superproject(self.repodir) 35 self._superproject = git_superproject.Superproject(self.repodir)
36 self.manifest_file = os.path.join(
37 self.repodir, manifest_xml.MANIFEST_FILE_NAME)
38 os.mkdir(self.repodir)
39
40 # The manifest parsing really wants a git repo currently.
41 gitdir = os.path.join(self.repodir, 'manifests.git')
42 os.mkdir(gitdir)
43 with open(os.path.join(gitdir, 'config'), 'w') as fp:
44 fp.write("""[remote "origin"]
45 url = https://localhost:0/manifest
46""")
36 47
37 def tearDown(self): 48 def tearDown(self):
38 """Tear down superproject every time.""" 49 """Tear down superproject every time."""
39 platform_utils.rmtree(self.tempdir) 50 platform_utils.rmtree(self.tempdir)
40 51
52 def getXmlManifest(self, data):
53 """Helper to initialize a manifest for testing."""
54 with open(self.manifest_file, 'w') as fp:
55 fp.write(data)
56 return manifest_xml.XmlManifest(self.repodir, self.manifest_file)
57
41 def test_superproject_get_project_shas_no_url(self): 58 def test_superproject_get_project_shas_no_url(self):
42 """Test with no url.""" 59 """Test with no url."""
43 with self.assertRaises(ValueError): 60 with self.assertRaises(ValueError):
44 self._superproject.GetAllProjectsSHAs(url=None) 61 self._superproject._GetAllProjectsSHAs(url=None)
45 62
46 def test_superproject_get_project_shas_invalid_url(self): 63 def test_superproject_get_project_shas_invalid_url(self):
47 """Test with an invalid url.""" 64 """Test with an invalid url."""
48 with self.assertRaises(GitError): 65 with self.assertRaises(GitError):
49 self._superproject.GetAllProjectsSHAs(url='localhost') 66 self._superproject._GetAllProjectsSHAs(url='localhost')
50 67
51 def test_superproject_get_project_shas_invalid_branch(self): 68 def test_superproject_get_project_shas_invalid_branch(self):
52 """Test with an invalid branch.""" 69 """Test with an invalid branch."""
53 with self.assertRaises(GitError): 70 with self.assertRaises(GitError):
54 self._superproject.GetAllProjectsSHAs( 71 self._superproject._GetAllProjectsSHAs(
55 url='sso://android/platform/superproject', 72 url='sso://android/platform/superproject',
56 branch='junk') 73 branch='junk')
57 74
@@ -59,14 +76,14 @@ class SuperprojectTestCase(unittest.TestCase):
59 """Test with _Clone failing.""" 76 """Test with _Clone failing."""
60 with self.assertRaises(GitError): 77 with self.assertRaises(GitError):
61 with mock.patch.object(self._superproject, '_Clone', return_value=False): 78 with mock.patch.object(self._superproject, '_Clone', return_value=False):
62 self._superproject.GetAllProjectsSHAs(url='localhost') 79 self._superproject._GetAllProjectsSHAs(url='localhost')
63 80
64 def test_superproject_get_project_shas_mock_pull(self): 81 def test_superproject_get_project_shas_mock_pull(self):
65 """Test with _Pull failing.""" 82 """Test with _Pull failing."""
66 with self.assertRaises(GitError): 83 with self.assertRaises(GitError):
67 with mock.patch.object(self._superproject, '_Clone', return_value=True): 84 with mock.patch.object(self._superproject, '_Clone', return_value=True):
68 with mock.patch.object(self._superproject, '_Pull', return_value=False): 85 with mock.patch.object(self._superproject, '_Pull', return_value=False):
69 self._superproject.GetAllProjectsSHAs(url='localhost') 86 self._superproject._GetAllProjectsSHAs(url='localhost')
70 87
71 def test_superproject_get_project_shas_mock_ls_tree(self): 88 def test_superproject_get_project_shas_mock_ls_tree(self):
72 """Test with LsTree being a mock.""" 89 """Test with LsTree being a mock."""
@@ -77,13 +94,71 @@ class SuperprojectTestCase(unittest.TestCase):
77 '160000 commit ade9b7a0d874e25fff4bf2552488825c6f111928\tbuild/bazel\x00') 94 '160000 commit ade9b7a0d874e25fff4bf2552488825c6f111928\tbuild/bazel\x00')
78 with mock.patch.object(self._superproject, '_Clone', return_value=True): 95 with mock.patch.object(self._superproject, '_Clone', return_value=True):
79 with mock.patch.object(self._superproject, '_LsTree', return_value=data): 96 with mock.patch.object(self._superproject, '_LsTree', return_value=data):
80 shas = self._superproject.GetAllProjectsSHAs(url='localhost', branch='junk') 97 shas = self._superproject._GetAllProjectsSHAs(url='localhost', branch='junk')
81 self.assertEqual(shas, { 98 self.assertEqual(shas, {
82 'art': '2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea', 99 'art': '2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea',
83 'bootable/recovery': 'e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06', 100 'bootable/recovery': 'e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06',
84 'build/bazel': 'ade9b7a0d874e25fff4bf2552488825c6f111928' 101 'build/bazel': 'ade9b7a0d874e25fff4bf2552488825c6f111928'
85 }) 102 })
86 103
104 def test_superproject_write_manifest_file(self):
105 """Test with writing manifest to a file after setting revisionId."""
106 manifest = self.getXmlManifest("""
107<manifest>
108 <remote name="default-remote" fetch="http://localhost" />
109 <default remote="default-remote" revision="refs/heads/main" />
110 <project name="test-name"/>
111</manifest>
112""")
113 self.assertEqual(len(manifest.projects), 1)
114 project = manifest.projects[0]
115 project.SetRevisionId('ABCDEF')
116 # Create temporary directory so that it can write the file.
117 os.mkdir(self._superproject._superproject_path)
118 manifest_path = self._superproject._WriteManfiestFile(manifest)
119 self.assertIsNotNone(manifest_path)
120 with open(manifest_path, 'r') as fp:
121 manifest_xml = fp.read()
122 self.assertEqual(
123 manifest_xml,
124 '<?xml version="1.0" ?><manifest>' +
125 '<remote name="default-remote" fetch="http://localhost"/>' +
126 '<default remote="default-remote" revision="refs/heads/main"/>' +
127 '<project name="test-name" revision="ABCDEF"/>' +
128 '</manifest>')
129
130 def test_superproject_update_project_revision_id(self):
131 """Test with LsTree being a mock."""
132 manifest = self.getXmlManifest("""
133<manifest>
134 <remote name="default-remote" fetch="http://localhost" />
135 <default remote="default-remote" revision="refs/heads/main" />
136 <project path="art" name="platform/art" />
137</manifest>
138""")
139 self.assertEqual(len(manifest.projects), 1)
140 projects = manifest.projects
141 data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00'
142 '160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tbootable/recovery\x00')
143 with mock.patch.object(self._superproject, '_Clone', return_value=True):
144 with mock.patch.object(self._superproject, '_Pull', return_value=True):
145 with mock.patch.object(self._superproject, '_LsTree', return_value=data):
146 # Create temporary directory so that it can write the file.
147 os.mkdir(self._superproject._superproject_path)
148 manifest_path = self._superproject.UpdateProjectsRevisionId(
149 manifest, projects, url='localhost')
150 self.assertIsNotNone(manifest_path)
151 with open(manifest_path, 'r') as fp:
152 manifest_xml = fp.read()
153 self.assertEqual(
154 manifest_xml,
155 '<?xml version="1.0" ?><manifest>' +
156 '<remote name="default-remote" fetch="http://localhost"/>' +
157 '<default remote="default-remote" revision="refs/heads/main"/>' +
158 '<project name="platform/art" path="art" ' +
159 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea"/>' +
160 '</manifest>')
161
87 162
88if __name__ == '__main__': 163if __name__ == '__main__':
89 unittest.main() 164 unittest.main()