diff options
author | Shawn O. Pearce <sop@google.com> | 2009-07-03 18:05:23 -0700 |
---|---|---|
committer | Shawn O. Pearce <sop@google.com> | 2009-07-03 20:50:52 -0700 |
commit | 0125ae2fda18deee89dc94b32a2daa1b37a8a361 (patch) | |
tree | db0d0af58d10cb0cdb709fc604732f2454f0ab78 /manifest_submodule.py | |
parent | a7ce096047a7707edc572de375b700d161b9520b (diff) | |
download | git-repo-0125ae2fda18deee89dc94b32a2daa1b37a8a361.tar.gz |
Introduce manifest format using git submodules
If a manifest top level directory contains '.gitmodules' we now
assume this is a git module format manifest and switch to using
that code, rather than the legacy XML based manifest.
At the same time, we move the bare repository for a project from
$TOP/.repo/projects/$REPO_PATH.git to be $REPO_NAME.git instead.
This makes it easier for us to later support a repo init from an
existing work tree, as we can more accurately predict the path of
the project's repository in the workspace. It also means that the
$TOP/.repo/projects/ directory is layed out like a mirror would be.
Signed-off-by: Shawn O. Pearce <sop@google.com>
Diffstat (limited to 'manifest_submodule.py')
-rw-r--r-- | manifest_submodule.py | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/manifest_submodule.py b/manifest_submodule.py new file mode 100644 index 00000000..92f187a0 --- /dev/null +++ b/manifest_submodule.py | |||
@@ -0,0 +1,474 @@ | |||
1 | # | ||
2 | # Copyright (C) 2009 The Android Open Source Project | ||
3 | # | ||
4 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | # you may not use this file except in compliance with the License. | ||
6 | # You may obtain a copy of the License at | ||
7 | # | ||
8 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | # | ||
10 | # Unless required by applicable law or agreed to in writing, software | ||
11 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | # See the License for the specific language governing permissions and | ||
14 | # limitations under the License. | ||
15 | |||
16 | import sys | ||
17 | import os | ||
18 | import shutil | ||
19 | |||
20 | from error import GitError | ||
21 | from error import ManifestParseError | ||
22 | from git_command import GitCommand | ||
23 | from git_config import GitConfig | ||
24 | from git_config import IsId | ||
25 | from manifest import Manifest | ||
26 | from progress import Progress | ||
27 | from project import RemoteSpec | ||
28 | from project import Project | ||
29 | from project import MetaProject | ||
30 | from project import R_HEADS | ||
31 | from project import HEAD | ||
32 | from project import _lwrite | ||
33 | |||
34 | import manifest_xml | ||
35 | |||
36 | GITLINK = '160000' | ||
37 | |||
38 | def _rmdir(dir, top): | ||
39 | while dir != top: | ||
40 | try: | ||
41 | os.rmdir(dir) | ||
42 | except OSError: | ||
43 | break | ||
44 | dir = os.path.dirname(dir) | ||
45 | |||
46 | def _rmref(gitdir, ref): | ||
47 | os.remove(os.path.join(gitdir, ref)) | ||
48 | log = os.path.join(gitdir, 'logs', ref) | ||
49 | if os.path.exists(log): | ||
50 | os.remove(log) | ||
51 | _rmdir(os.path.dirname(log), gitdir) | ||
52 | |||
53 | def _has_gitmodules(d): | ||
54 | return os.path.exists(os.path.join(d, '.gitmodules')) | ||
55 | |||
56 | class SubmoduleManifest(Manifest): | ||
57 | """manifest from .gitmodules file""" | ||
58 | |||
59 | @classmethod | ||
60 | def Is(cls, repodir): | ||
61 | return _has_gitmodules(os.path.dirname(repodir)) \ | ||
62 | or _has_gitmodules(os.path.join(repodir, 'manifest')) \ | ||
63 | or _has_gitmodules(os.path.join(repodir, 'manifests')) | ||
64 | |||
65 | @classmethod | ||
66 | def IsBare(cls, p): | ||
67 | try: | ||
68 | p.bare_git.cat_file('-e', '%s:.gitmodules' % p.GetRevisionId()) | ||
69 | except GitError: | ||
70 | return False | ||
71 | return True | ||
72 | |||
73 | def __init__(self, repodir): | ||
74 | Manifest.__init__(self, repodir) | ||
75 | |||
76 | gitdir = os.path.join(repodir, 'manifest.git') | ||
77 | config = GitConfig.ForRepository(gitdir = gitdir) | ||
78 | |||
79 | if config.GetBoolean('repo.mirror'): | ||
80 | worktree = os.path.join(repodir, 'manifest') | ||
81 | relpath = None | ||
82 | else: | ||
83 | worktree = self.topdir | ||
84 | relpath = '.' | ||
85 | |||
86 | self.manifestProject = MetaProject(self, '__manifest__', | ||
87 | gitdir = gitdir, | ||
88 | worktree = worktree, | ||
89 | relpath = relpath) | ||
90 | self._modules = GitConfig(os.path.join(worktree, '.gitmodules'), | ||
91 | pickleFile = os.path.join( | ||
92 | repodir, '.repopickle_gitmodules' | ||
93 | )) | ||
94 | self._review = GitConfig(os.path.join(worktree, '.review'), | ||
95 | pickleFile = os.path.join( | ||
96 | repodir, '.repopickle_review' | ||
97 | )) | ||
98 | self._Unload() | ||
99 | |||
100 | @property | ||
101 | def projects(self): | ||
102 | self._Load() | ||
103 | return self._projects | ||
104 | |||
105 | def InitBranch(self): | ||
106 | m = self.manifestProject | ||
107 | if m.CurrentBranch is None: | ||
108 | b = m.revisionExpr | ||
109 | if b.startswith(R_HEADS): | ||
110 | b = b[len(R_HEADS):] | ||
111 | return m.StartBranch(b) | ||
112 | return True | ||
113 | |||
114 | def SetMRefs(self, project): | ||
115 | if project.revisionId is None: | ||
116 | # Special project, e.g. the manifest or repo executable. | ||
117 | # | ||
118 | return | ||
119 | |||
120 | ref = 'refs/remotes/m' | ||
121 | cur = project.bare_ref.get(ref) | ||
122 | exp = project.revisionId | ||
123 | if cur != exp: | ||
124 | msg = 'manifest set to %s' % exp | ||
125 | project.bare_git.UpdateRef(ref, exp, message = msg, detach = True) | ||
126 | |||
127 | ref = 'refs/remotes/m-revision' | ||
128 | cur = project.bare_ref.symref(ref) | ||
129 | exp = project.revisionExpr | ||
130 | if exp is None: | ||
131 | if cur: | ||
132 | _rmref(project.gitdir, ref) | ||
133 | elif cur != exp: | ||
134 | remote = project.GetRemote(project.remote.name) | ||
135 | dst = remote.ToLocal(exp) | ||
136 | msg = 'manifest set to %s (%s)' % (exp, dst) | ||
137 | project.bare_git.symbolic_ref('-m', msg, ref, dst) | ||
138 | |||
139 | def Upgrade_Local(self, old): | ||
140 | if isinstance(old, manifest_xml.XmlManifest): | ||
141 | self.FromXml_Local_1(old, checkout=True) | ||
142 | self.FromXml_Local_2(old) | ||
143 | else: | ||
144 | raise ManifestParseError, 'cannot upgrade manifest' | ||
145 | |||
146 | def FromXml_Local_1(self, old, checkout): | ||
147 | os.rename(old.manifestProject.gitdir, | ||
148 | os.path.join(old.repodir, 'manifest.git')) | ||
149 | |||
150 | oldmp = old.manifestProject | ||
151 | oldBranch = oldmp.CurrentBranch | ||
152 | b = oldmp.GetBranch(oldBranch).merge | ||
153 | if not b: | ||
154 | raise ManifestParseError, 'cannot upgrade manifest' | ||
155 | if b.startswith(R_HEADS): | ||
156 | b = b[len(R_HEADS):] | ||
157 | |||
158 | newmp = self.manifestProject | ||
159 | self._CleanOldMRefs(newmp) | ||
160 | if oldBranch != b: | ||
161 | newmp.bare_git.branch('-m', oldBranch, b) | ||
162 | newmp.config.ClearCache() | ||
163 | |||
164 | old_remote = newmp.GetBranch(b).remote.name | ||
165 | act_remote = self._GuessRemoteName(old) | ||
166 | if old_remote != act_remote: | ||
167 | newmp.bare_git.remote('rename', old_remote, act_remote) | ||
168 | newmp.config.ClearCache() | ||
169 | newmp.remote.name = act_remote | ||
170 | print >>sys.stderr, "Assuming remote named '%s'" % act_remote | ||
171 | |||
172 | if checkout: | ||
173 | for p in old.projects.values(): | ||
174 | for c in p.copyfiles: | ||
175 | if os.path.exists(c.abs_dest): | ||
176 | os.remove(c.abs_dest) | ||
177 | newmp._InitWorkTree() | ||
178 | else: | ||
179 | newmp._LinkWorkTree() | ||
180 | |||
181 | _lwrite(os.path.join(newmp.worktree,'.git',HEAD), | ||
182 | 'ref: refs/heads/%s\n' % b) | ||
183 | |||
184 | def _GuessRemoteName(self, old): | ||
185 | used = {} | ||
186 | for p in old.projects.values(): | ||
187 | n = p.remote.name | ||
188 | used[n] = used.get(n, 0) + 1 | ||
189 | |||
190 | remote_name = 'origin' | ||
191 | remote_used = 0 | ||
192 | for n in used.keys(): | ||
193 | if remote_used < used[n]: | ||
194 | remote_used = used[n] | ||
195 | remote_name = n | ||
196 | return remote_name | ||
197 | |||
198 | def FromXml_Local_2(self, old): | ||
199 | shutil.rmtree(old.manifestProject.worktree) | ||
200 | os.remove(old._manifestFile) | ||
201 | |||
202 | my_remote = self._Remote().name | ||
203 | new_base = os.path.join(self.repodir, 'projects') | ||
204 | old_base = os.path.join(self.repodir, 'projects.old') | ||
205 | os.rename(new_base, old_base) | ||
206 | os.makedirs(new_base) | ||
207 | |||
208 | info = [] | ||
209 | pm = Progress('Converting projects', len(self.projects)) | ||
210 | for p in self.projects.values(): | ||
211 | pm.update() | ||
212 | |||
213 | old_p = old.projects.get(p.name) | ||
214 | old_gitdir = os.path.join(old_base, '%s.git' % p.relpath) | ||
215 | if not os.path.isdir(old_gitdir): | ||
216 | continue | ||
217 | |||
218 | parent = os.path.dirname(p.gitdir) | ||
219 | if not os.path.isdir(parent): | ||
220 | os.makedirs(parent) | ||
221 | os.rename(old_gitdir, p.gitdir) | ||
222 | _rmdir(os.path.dirname(old_gitdir), self.repodir) | ||
223 | |||
224 | if not os.path.isdir(p.worktree): | ||
225 | os.makedirs(p.worktree) | ||
226 | |||
227 | if os.path.isdir(os.path.join(p.worktree, '.git')): | ||
228 | p._LinkWorkTree(relink=True) | ||
229 | |||
230 | self._CleanOldMRefs(p) | ||
231 | if old_p and old_p.remote.name != my_remote: | ||
232 | info.append("%s/: renamed remote '%s' to '%s'" \ | ||
233 | % (p.relpath, old_p.remote.name, my_remote)) | ||
234 | p.bare_git.remote('rename', old_p.remote.name, my_remote) | ||
235 | p.config.ClearCache() | ||
236 | |||
237 | self.SetMRefs(p) | ||
238 | pm.end() | ||
239 | for i in info: | ||
240 | print >>sys.stderr, i | ||
241 | |||
242 | def _CleanOldMRefs(self, p): | ||
243 | all_refs = p._allrefs | ||
244 | for ref in all_refs.keys(): | ||
245 | if ref.startswith(manifest_xml.R_M): | ||
246 | if p.bare_ref.symref(ref) != '': | ||
247 | _rmref(p.gitdir, ref) | ||
248 | else: | ||
249 | p.bare_git.DeleteRef(ref, all_refs[ref]) | ||
250 | |||
251 | def FromXml_Definition(self, old): | ||
252 | """Convert another manifest representation to this one. | ||
253 | """ | ||
254 | mp = self.manifestProject | ||
255 | gm = self._modules | ||
256 | gr = self._review | ||
257 | |||
258 | fd = open(os.path.join(mp.worktree, '.gitignore'), 'ab') | ||
259 | fd.write('/.repo\n') | ||
260 | fd.close() | ||
261 | |||
262 | sort_projects = list(old.projects.keys()) | ||
263 | sort_projects.sort() | ||
264 | |||
265 | b = mp.GetBranch(mp.CurrentBranch).merge | ||
266 | if b.startswith(R_HEADS): | ||
267 | b = b[len(R_HEADS):] | ||
268 | |||
269 | info = [] | ||
270 | pm = Progress('Converting manifest', len(sort_projects)) | ||
271 | for p in sort_projects: | ||
272 | pm.update() | ||
273 | p = old.projects[p] | ||
274 | |||
275 | gm.SetString('submodule.%s.path' % p.name, p.relpath) | ||
276 | gm.SetString('submodule.%s.url' % p.name, p.remote.url) | ||
277 | |||
278 | if gr.GetString('review.url') is None: | ||
279 | gr.SetString('review.url', p.remote.review) | ||
280 | elif gr.GetString('review.url') != p.remote.review: | ||
281 | gr.SetString('review.%s.url' % p.name, p.remote.review) | ||
282 | |||
283 | r = p.revisionExpr | ||
284 | if r and not IsId(r): | ||
285 | if r.startswith(R_HEADS): | ||
286 | r = r[len(R_HEADS):] | ||
287 | if r == b: | ||
288 | r = '.' | ||
289 | gm.SetString('submodule.%s.revision' % p.name, r) | ||
290 | |||
291 | for c in p.copyfiles: | ||
292 | info.append('Moved %s out of %s' % (c.src, p.relpath)) | ||
293 | c._Copy() | ||
294 | p.work_git.rm(c.src) | ||
295 | mp.work_git.add(c.dest) | ||
296 | |||
297 | self.SetRevisionId(p.relpath, p.GetRevisionId()) | ||
298 | mp.work_git.add('.gitignore', '.gitmodules', '.review') | ||
299 | pm.end() | ||
300 | for i in info: | ||
301 | print >>sys.stderr, i | ||
302 | |||
303 | def _Unload(self): | ||
304 | self._loaded = False | ||
305 | self._projects = {} | ||
306 | self._revisionIds = None | ||
307 | self.branch = None | ||
308 | |||
309 | def _Load(self): | ||
310 | if not self._loaded: | ||
311 | f = os.path.join(self.repodir, manifest_xml.LOCAL_MANIFEST_NAME) | ||
312 | if os.path.exists(f): | ||
313 | print >>sys.stderr, 'warning: ignoring %s' % f | ||
314 | |||
315 | m = self.manifestProject | ||
316 | b = m.CurrentBranch | ||
317 | if not b: | ||
318 | raise ManifestParseError, 'manifest cannot be on detached HEAD' | ||
319 | b = m.GetBranch(b).merge | ||
320 | if b.startswith(R_HEADS): | ||
321 | b = b[len(R_HEADS):] | ||
322 | self.branch = b | ||
323 | m.remote.name = self._Remote().name | ||
324 | |||
325 | self._ParseModules() | ||
326 | |||
327 | if self.IsMirror: | ||
328 | self._AddMetaProjectMirror(self.repoProject) | ||
329 | self._AddMetaProjectMirror(self.manifestProject) | ||
330 | |||
331 | self._loaded = True | ||
332 | |||
333 | def _ParseModules(self): | ||
334 | byPath = dict() | ||
335 | for name in self._modules.GetSubSections('submodule'): | ||
336 | p = self._ParseProject(name) | ||
337 | if self._projects.get(p.name): | ||
338 | raise ManifestParseError, 'duplicate project "%s"' % p.name | ||
339 | if byPath.get(p.relpath): | ||
340 | raise ManifestParseError, 'duplicate path "%s"' % p.relpath | ||
341 | self._projects[p.name] = p | ||
342 | byPath[p.relpath] = p | ||
343 | |||
344 | for relpath in self._allRevisionIds.keys(): | ||
345 | if relpath not in byPath: | ||
346 | raise ManifestParseError, \ | ||
347 | 'project "%s" not in .gitmodules' \ | ||
348 | % relpath | ||
349 | |||
350 | def _Remote(self): | ||
351 | m = self.manifestProject | ||
352 | b = m.GetBranch(m.CurrentBranch) | ||
353 | return b.remote | ||
354 | |||
355 | def _ResolveUrl(self, url): | ||
356 | if url.startswith('./') or url.startswith('../'): | ||
357 | base = self._Remote().url | ||
358 | try: | ||
359 | base = base[:base.rindex('/')+1] | ||
360 | except ValueError: | ||
361 | base = base[:base.rindex(':')+1] | ||
362 | if url.startswith('./'): | ||
363 | url = url[2:] | ||
364 | while '/' in base and url.startswith('../'): | ||
365 | base = base[:base.rindex('/')+1] | ||
366 | url = url[3:] | ||
367 | return base + url | ||
368 | return url | ||
369 | |||
370 | def _GetRevisionId(self, path): | ||
371 | return self._allRevisionIds.get(path) | ||
372 | |||
373 | @property | ||
374 | def _allRevisionIds(self): | ||
375 | if self._revisionIds is None: | ||
376 | a = dict() | ||
377 | p = GitCommand(self.manifestProject, | ||
378 | ['ls-files','-z','--stage'], | ||
379 | capture_stdout = True) | ||
380 | for line in p.process.stdout.read().split('\0')[:-1]: | ||
381 | l_info, l_path = line.split('\t', 2) | ||
382 | l_mode, l_id, l_stage = l_info.split(' ', 2) | ||
383 | if l_mode == GITLINK and l_stage == '0': | ||
384 | a[l_path] = l_id | ||
385 | p.Wait() | ||
386 | self._revisionIds = a | ||
387 | return self._revisionIds | ||
388 | |||
389 | def SetRevisionId(self, path, id): | ||
390 | self.manifestProject.work_git.update_index( | ||
391 | '--add','--cacheinfo', GITLINK, id, path) | ||
392 | |||
393 | def _ParseProject(self, name): | ||
394 | gm = self._modules | ||
395 | gr = self._review | ||
396 | |||
397 | path = gm.GetString('submodule.%s.path' % name) | ||
398 | if not path: | ||
399 | path = name | ||
400 | |||
401 | revId = self._GetRevisionId(path) | ||
402 | if not revId: | ||
403 | raise ManifestParseError( | ||
404 | 'submodule "%s" has no revision at "%s"' \ | ||
405 | % (name, path)) | ||
406 | |||
407 | url = gm.GetString('submodule.%s.url' % name) | ||
408 | if not url: | ||
409 | url = name | ||
410 | url = self._ResolveUrl(url) | ||
411 | |||
412 | review = gr.GetString('review.%s.url' % name) | ||
413 | if not review: | ||
414 | review = gr.GetString('review.url') | ||
415 | if not review: | ||
416 | review = self._Remote().review | ||
417 | |||
418 | remote = RemoteSpec(self._Remote().name, url, review) | ||
419 | revExpr = gm.GetString('submodule.%s.revision' % name) | ||
420 | if revExpr == '.': | ||
421 | revExpr = self.branch | ||
422 | |||
423 | if self.IsMirror: | ||
424 | relpath = None | ||
425 | worktree = None | ||
426 | gitdir = os.path.join(self.topdir, '%s.git' % name) | ||
427 | else: | ||
428 | worktree = os.path.join(self.topdir, path) | ||
429 | gitdir = os.path.join(self.repodir, 'projects/%s.git' % name) | ||
430 | |||
431 | return Project(manifest = self, | ||
432 | name = name, | ||
433 | remote = remote, | ||
434 | gitdir = gitdir, | ||
435 | worktree = worktree, | ||
436 | relpath = path, | ||
437 | revisionExpr = revExpr, | ||
438 | revisionId = revId) | ||
439 | |||
440 | def _AddMetaProjectMirror(self, m): | ||
441 | m_url = m.GetRemote(m.remote.name).url | ||
442 | if m_url.endswith('/.git'): | ||
443 | raise ManifestParseError, 'refusing to mirror %s' % m_url | ||
444 | |||
445 | name = self._GuessMetaName(m_url) | ||
446 | if name.endswith('.git'): | ||
447 | name = name[:-4] | ||
448 | |||
449 | if name not in self._projects: | ||
450 | m.PreSync() | ||
451 | gitdir = os.path.join(self.topdir, '%s.git' % name) | ||
452 | project = Project(manifest = self, | ||
453 | name = name, | ||
454 | remote = RemoteSpec(self._Remote().name, m_url), | ||
455 | gitdir = gitdir, | ||
456 | worktree = None, | ||
457 | relpath = None, | ||
458 | revisionExpr = m.revisionExpr, | ||
459 | revisionId = None) | ||
460 | self._projects[project.name] = project | ||
461 | |||
462 | def _GuessMetaName(self, m_url): | ||
463 | parts = m_url.split('/') | ||
464 | name = parts[-1] | ||
465 | parts = parts[0:-1] | ||
466 | s = len(parts) - 1 | ||
467 | while s > 0: | ||
468 | l = '/'.join(parts[0:s]) + '/' | ||
469 | r = '/'.join(parts[s:]) + '/' | ||
470 | for p in self._projects.values(): | ||
471 | if p.name.startswith(r) and p.remote.url.startswith(l): | ||
472 | return r + name | ||
473 | s -= 1 | ||
474 | return m_url[m_url.rindex('/') + 1:] | ||