summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/manifest_submodule.txt130
-rw-r--r--manifest_loader.py3
-rw-r--r--manifest_submodule.py474
-rw-r--r--subcmds/init.py9
-rw-r--r--subcmds/manifest.py4
5 files changed, 619 insertions, 1 deletions
diff --git a/docs/manifest_submodule.txt b/docs/manifest_submodule.txt
new file mode 100644
index 00000000..e7d1f643
--- /dev/null
+++ b/docs/manifest_submodule.txt
@@ -0,0 +1,130 @@
1repo Manifest Format (submodule)
2================================
3
4A repo manifest describes the structure of a repo client; that is
5the directories that are visible and where they should be obtained
6from with git.
7
8The basic structure of a manifest is a bare Git repository holding
9a 'gitmodules' file in the top level directory, and one or more
10gitlink references pointing at commits from the referenced projects.
11This is the same structure as used by 'git submodule'.
12
13Manifests are inherently version controlled, since they are kept
14within a Git repository. Updates to manifests are automatically
15obtained by clients during `repo sync`.
16
17.gitmodules
18===========
19
20The '.gitmodules' file, located in the top-level directory of the
21client's working tree (or manifest repository), is a text file with
22a syntax matching the requirements of 'git config'.
23
24This file contains one subsection per project (also called a
25submodule by git), and the subsection value is a unique name to
26describe the project. Each submodule section must contain the
27following required keys:
28
29 * path
30 * url
31
32submodule.<name>.path
33---------------------
34
35Defines the path, relative to the top-level directory of the client's
36working tree, where the project is expected to be checked out. The
37path name must not end with a '/'. All paths must be unique within
38the .gitmodules file.
39
40At the specified path within the manifest repository a gitlink
41tree entry (an entry with file mode 160000) must exist referencing
42a commit SHA-1 from the project. This tree entry specifies the
43exact version of the project that `repo sync` will synchronize the
44client's working tree to.
45
46submodule.<name>.url
47--------------------
48
49Defines a URL from where the project repository can be cloned.
50By default `repo sync` will clone from this URL whenever a user
51needs to access this project.
52
53submodule.<name>.revision
54-------------------------
55
56Name of the branch in the project repository that Gerrit Code Review
57should automatically refresh the project's gitlink entry from.
58
59If set, during submit of a change within the referenced project,
60Gerrit Code Review will automatically update the manifest
61repository's corresponding gitlink to the new commit SHA-1 of
62this branch.
63
64Valid values are a short branch name (e.g. 'master'), a full ref
65name (e.g. 'refs/heads/master'), or '.' to request using the same
66branch name as the manifest branch itself. Since '.' automatically
67uses the manifest branch, '.' is the recommended value.
68
69If this key is not set, Gerrit Code Review will NOT automatically
70update the gitlink. An unset key requires the manifest maintainer
71to manually update the gitlink when it is necessary to reference
72a different revision of the project.
73
74submodule.<name>.update
75-----------------------
76
77This key is not supported by repo. If set, it will be ignored.
78
79.review
80=======
81
82The optional '.review' file, located in the top-level directory of
83the client's working tree (or manifest repository), is a text file
84with a syntax matching the requirements of 'git config'.
85
86This file describes how `repo upload` should interact with the
87project's preferred code review system.
88
89review.url
90----------
91
92URL of the default Gerrit Code Review server. If a project does
93not have a specific URL in the '.review' file, this default URL
94will be used instead.
95
96review.<name>.url
97-----------------
98
99Project specific URL of the Gerrit Code Review server, for the
100submodule whose project name is <name>.
101
102Example
103=======
104
105 $ cat .gitmodules
106 [submodule "app/Clock"]
107 path = clock
108 url = git://vcs.example.com/ClockWidget.git
109 revision = .
110 [submodule "app/Browser"]
111 path = net/browser
112 url = git://netgroup.example.com/network/web/Browser.git
113 revision = .
114
115 $ cat .review
116 [review]
117 url = vcs-gerrit.example.com
118 [review "app/Browser"]
119 url = netgroup.example.com
120
121In the above example, the app/Clock project will send its code
122reviews to the default server, vcs-gerrit.example.com, while
123app/Browser will send its code reviews to netgroup.example.com.
124
125See Also
126========
127
128 * http://www.kernel.org/pub/software/scm/git/docs/gitmodules.html
129 * http://www.kernel.org/pub/software/scm/git/docs/git-config.html
130 * http://code.google.com/p/gerrit/
diff --git a/manifest_loader.py b/manifest_loader.py
index 1ce1c1f3..467cb42a 100644
--- a/manifest_loader.py
+++ b/manifest_loader.py
@@ -13,11 +13,14 @@
13# See the License for the specific language governing permissions and 13# See the License for the specific language governing permissions and
14# limitations under the License. 14# limitations under the License.
15 15
16from manifest_submodule import SubmoduleManifest
16from manifest_xml import XmlManifest 17from manifest_xml import XmlManifest
17 18
18def ParseManifest(repodir, type=None): 19def ParseManifest(repodir, type=None):
19 if type: 20 if type:
20 return type(repodir) 21 return type(repodir)
22 if SubmoduleManifest.Is(repodir):
23 return SubmoduleManifest(repodir)
21 return XmlManifest(repodir) 24 return XmlManifest(repodir)
22 25
23_manifest = None 26_manifest = None
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
16import sys
17import os
18import shutil
19
20from error import GitError
21from error import ManifestParseError
22from git_command import GitCommand
23from git_config import GitConfig
24from git_config import IsId
25from manifest import Manifest
26from progress import Progress
27from project import RemoteSpec
28from project import Project
29from project import MetaProject
30from project import R_HEADS
31from project import HEAD
32from project import _lwrite
33
34import manifest_xml
35
36GITLINK = '160000'
37
38def _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
46def _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
53def _has_gitmodules(d):
54 return os.path.exists(os.path.join(d, '.gitmodules'))
55
56class 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:]
diff --git a/subcmds/init.py b/subcmds/init.py
index b5207fbf..cdbbfdf7 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -21,6 +21,7 @@ from command import InteractiveCommand, MirrorSafeCommand
21from error import ManifestParseError 21from error import ManifestParseError
22from project import SyncBuffer 22from project import SyncBuffer
23from git_command import git_require, MIN_GIT_VERSION 23from git_command import git_require, MIN_GIT_VERSION
24from manifest_submodule import SubmoduleManifest
24from manifest_xml import XmlManifest 25from manifest_xml import XmlManifest
25from subcmds.sync import _ReloadManifest 26from subcmds.sync import _ReloadManifest
26 27
@@ -144,6 +145,14 @@ to update the working directory files.
144 print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url 145 print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url
145 sys.exit(1) 146 sys.exit(1)
146 147
148 if is_new and SubmoduleManifest.IsBare(m):
149 new = self.GetManifest(reparse=True, type=SubmoduleManifest)
150 if m.gitdir != new.manifestProject.gitdir:
151 os.rename(m.gitdir, new.manifestProject.gitdir)
152 new = self.GetManifest(reparse=True, type=SubmoduleManifest)
153 m = new.manifestProject
154 self._ApplyOptions(opt, is_new)
155
147 if not is_new: 156 if not is_new:
148 # Force the manifest to load if it exists, the old graph 157 # Force the manifest to load if it exists, the old graph
149 # may be needed inside of _ReloadManifest(). 158 # may be needed inside of _ReloadManifest().
diff --git a/subcmds/manifest.py b/subcmds/manifest.py
index 551b13bd..7a8b2ee8 100644
--- a/subcmds/manifest.py
+++ b/subcmds/manifest.py
@@ -22,7 +22,7 @@ from manifest_xml import XmlManifest
22def _doc(name): 22def _doc(name):
23 r = os.path.dirname(__file__) 23 r = os.path.dirname(__file__)
24 r = os.path.dirname(r) 24 r = os.path.dirname(r)
25 fd = open(os.path.join(r, 'docs', 'manifest_xml.txt')) 25 fd = open(os.path.join(r, 'docs', name))
26 try: 26 try:
27 return fd.read() 27 return fd.read()
28 finally: 28 finally:
@@ -48,6 +48,8 @@ in a Git repository for use during future 'repo init' invocations.
48 help = '' 48 help = ''
49 if isinstance(self.manifest, XmlManifest): 49 if isinstance(self.manifest, XmlManifest):
50 help += self._xmlHelp + '\n' + _doc('manifest_xml.txt') 50 help += self._xmlHelp + '\n' + _doc('manifest_xml.txt')
51 if isinstance(self.manifest, SubmoduleManifest):
52 help += _doc('manifest_submodule.txt')
51 return help 53 return help
52 54
53 def _Options(self, p): 55 def _Options(self, p):