summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChe-Liang Chiou <clchiou@google.com>2012-01-11 11:28:42 +0800
committerChe-Liang Chiou <clchiou@google.com>2012-11-19 10:45:21 -0800
commitb2bd91c99b9435cf950ecf8efbb8439f31d3fcbc (patch)
tree5d26d3943317c11c1cd913fc5640074a5bc7910b
parent3f5ea0b18207a81f58595b1a2e10e5ffb784b74f (diff)
downloadgit-repo-b2bd91c99b9435cf950ecf8efbb8439f31d3fcbc.tar.gz
Represent git-submodule as nested projects, take 2
(Previous submission of this change broke Android buildbot due to incorrect regular expression for parsing git-config output. During investigation, we also found that Android, which pulls Chromium, has a workaround for Chromium's submodules; its manifest includes Chromium's submodules. This new change, in addition to fixing the regex, also take this type of workarounds into consideration; it adds a new attribute that makes repo not fetch submodules unless submodules have a project element defined in the manifest, or this attribute is overridden by a parent project element or by the default element.) We need a representation of git-submodule in repo; otherwise repo will not sync submodules, and leave workspace in a broken state. Of course this will not be a problem if all projects are owned by the owner of the manifest file, who may simply choose not to use git-submodule in all projects. However, this is not possible in practice because manifest file owner is unlikely to own all upstream projects. As git submodules are simply git repositories, it is natural to treat them as plain repo projects that live inside a repo project. That is, we could use recursively declared projects to denote the is-submodule relation of git repositories. The behavior of repo remains the same to projects that do not have a sub-project within. As for parent projects, repo fetches them and their sub-projects as normal projects, and then checks out subprojects at the commit specified in parent's commit object. The sub-project is fetched at a path relative to parent project's working directory; so the path specified in manifest file should match that of .gitmodules file. If a submodule is not registered in repo manifest, repo will derive its properties from itself and its parent project, which might not always be correct. In such cases, the subproject is called a derived subproject. To a user, a sub-project is merely a git-submodule; so all tips of working with a git-submodule apply here, too. For example, you should not run `repo sync` in a parent repository if its submodule is dirty. Change-Id: I4b8344c1b9ccad2f58ad304573133e5d52e1faef
-rw-r--r--command.py73
-rw-r--r--docs/manifest-format.txt17
-rw-r--r--manifest_xml.py129
-rw-r--r--project.py177
-rw-r--r--subcmds/sync.py33
5 files changed, 372 insertions, 57 deletions
diff --git a/command.py b/command.py
index dc6052a7..96d7848f 100644
--- a/command.py
+++ b/command.py
@@ -100,7 +100,33 @@ class Command(object):
100 """ 100 """
101 raise NotImplementedError 101 raise NotImplementedError
102 102
103 def GetProjects(self, args, missing_ok=False): 103 def _ResetPathToProjectMap(self, projects):
104 self._by_path = dict((p.worktree, p) for p in projects)
105
106 def _UpdatePathToProjectMap(self, project):
107 self._by_path[project.worktree] = project
108
109 def _GetProjectByPath(self, path):
110 project = None
111 if os.path.exists(path):
112 oldpath = None
113 while path \
114 and path != oldpath \
115 and path != self.manifest.topdir:
116 try:
117 project = self._by_path[path]
118 break
119 except KeyError:
120 oldpath = path
121 path = os.path.dirname(path)
122 else:
123 try:
124 project = self._by_path[path]
125 except KeyError:
126 pass
127 return project
128
129 def GetProjects(self, args, missing_ok=False, submodules_ok=False):
104 """A list of projects that match the arguments. 130 """A list of projects that match the arguments.
105 """ 131 """
106 all_projects = self.manifest.projects 132 all_projects = self.manifest.projects
@@ -114,40 +140,37 @@ class Command(object):
114 groups = [x for x in re.split(r'[,\s]+', groups) if x] 140 groups = [x for x in re.split(r'[,\s]+', groups) if x]
115 141
116 if not args: 142 if not args:
117 for project in all_projects.values(): 143 all_projects_list = all_projects.values()
144 derived_projects = {}
145 for project in all_projects_list:
146 if submodules_ok or project.sync_s:
147 derived_projects.update((p.name, p)
148 for p in project.GetDerivedSubprojects())
149 all_projects_list.extend(derived_projects.values())
150 for project in all_projects_list:
118 if ((missing_ok or project.Exists) and 151 if ((missing_ok or project.Exists) and
119 project.MatchesGroups(groups)): 152 project.MatchesGroups(groups)):
120 result.append(project) 153 result.append(project)
121 else: 154 else:
122 by_path = None 155 self._ResetPathToProjectMap(all_projects.values())
123 156
124 for arg in args: 157 for arg in args:
125 project = all_projects.get(arg) 158 project = all_projects.get(arg)
126 159
127 if not project: 160 if not project:
128 path = os.path.abspath(arg).replace('\\', '/') 161 path = os.path.abspath(arg).replace('\\', '/')
129 162 project = self._GetProjectByPath(path)
130 if not by_path: 163
131 by_path = dict() 164 # If it's not a derived project, update path->project mapping and
132 for p in all_projects.values(): 165 # search again, as arg might actually point to a derived subproject.
133 by_path[p.worktree] = p 166 if (project and not project.Derived and
134 167 (submodules_ok or project.sync_s)):
135 if os.path.exists(path): 168 search_again = False
136 oldpath = None 169 for subproject in project.GetDerivedSubprojects():
137 while path \ 170 self._UpdatePathToProjectMap(subproject)
138 and path != oldpath \ 171 search_again = True
139 and path != self.manifest.topdir: 172 if search_again:
140 try: 173 project = self._GetProjectByPath(path) or project
141 project = by_path[path]
142 break
143 except KeyError:
144 oldpath = path
145 path = os.path.dirname(path)
146 else:
147 try:
148 project = by_path[path]
149 except KeyError:
150 pass
151 174
152 if not project: 175 if not project:
153 raise NoSuchProjectError(arg) 176 raise NoSuchProjectError(arg)
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt
index a54282c8..f6dba640 100644
--- a/docs/manifest-format.txt
+++ b/docs/manifest-format.txt
@@ -41,17 +41,20 @@ following DTD:
41 <!ATTLIST default revision CDATA #IMPLIED> 41 <!ATTLIST default revision CDATA #IMPLIED>
42 <!ATTLIST default sync-j CDATA #IMPLIED> 42 <!ATTLIST default sync-j CDATA #IMPLIED>
43 <!ATTLIST default sync-c CDATA #IMPLIED> 43 <!ATTLIST default sync-c CDATA #IMPLIED>
44 <!ATTLIST default sync-s CDATA #IMPLIED>
44 45
45 <!ELEMENT manifest-server (EMPTY)> 46 <!ELEMENT manifest-server (EMPTY)>
46 <!ATTLIST url CDATA #REQUIRED> 47 <!ATTLIST url CDATA #REQUIRED>
47 48
48 <!ELEMENT project (annotation?)> 49 <!ELEMENT project (annotation?,
50 project*)>
49 <!ATTLIST project name CDATA #REQUIRED> 51 <!ATTLIST project name CDATA #REQUIRED>
50 <!ATTLIST project path CDATA #IMPLIED> 52 <!ATTLIST project path CDATA #IMPLIED>
51 <!ATTLIST project remote IDREF #IMPLIED> 53 <!ATTLIST project remote IDREF #IMPLIED>
52 <!ATTLIST project revision CDATA #IMPLIED> 54 <!ATTLIST project revision CDATA #IMPLIED>
53 <!ATTLIST project groups CDATA #IMPLIED> 55 <!ATTLIST project groups CDATA #IMPLIED>
54 <!ATTLIST project sync-c CDATA #IMPLIED> 56 <!ATTLIST project sync-c CDATA #IMPLIED>
57 <!ATTLIST project sync-s CDATA #IMPLIED>
55 58
56 <!ELEMENT annotation (EMPTY)> 59 <!ELEMENT annotation (EMPTY)>
57 <!ATTLIST annotation name CDATA #REQUIRED> 60 <!ATTLIST annotation name CDATA #REQUIRED>
@@ -152,7 +155,10 @@ Element project
152 155
153One or more project elements may be specified. Each element 156One or more project elements may be specified. Each element
154describes a single Git repository to be cloned into the repo 157describes a single Git repository to be cloned into the repo
155client workspace. 158client workspace. You may specify Git-submodules by creating a
159nested project. Git-submodules will be automatically
160recognized and inherit their parent's attributes, but those
161may be overridden by an explicitly specified project element.
156 162
157Attribute `name`: A unique name for this project. The project's 163Attribute `name`: A unique name for this project. The project's
158name is appended onto its remote's fetch URL to generate the actual 164name is appended onto its remote's fetch URL to generate the actual
@@ -163,7 +169,8 @@ URL to configure the Git remote with. The URL gets formed as:
163where ${remote_fetch} is the remote's fetch attribute and 169where ${remote_fetch} is the remote's fetch attribute and
164${project_name} is the project's name attribute. The suffix ".git" 170${project_name} is the project's name attribute. The suffix ".git"
165is always appended as repo assumes the upstream is a forest of 171is always appended as repo assumes the upstream is a forest of
166bare Git repositories. 172bare Git repositories. If the project has a parent element, its
173name will be prefixed by the parent's.
167 174
168The project name must match the name Gerrit knows, if Gerrit is 175The project name must match the name Gerrit knows, if Gerrit is
169being used for code reviews. 176being used for code reviews.
@@ -171,6 +178,8 @@ being used for code reviews.
171Attribute `path`: An optional path relative to the top directory 178Attribute `path`: An optional path relative to the top directory
172of the repo client where the Git working directory for this project 179of the repo client where the Git working directory for this project
173should be placed. If not supplied the project name is used. 180should be placed. If not supplied the project name is used.
181If the project has a parent element, its path will be prefixed
182by the parent's.
174 183
175Attribute `remote`: Name of a previously defined remote element. 184Attribute `remote`: Name of a previously defined remote element.
176If not supplied the remote given by the default element is used. 185If not supplied the remote given by the default element is used.
@@ -190,6 +199,8 @@ its name:`name` and path:`path`. E.g. for
190definition is implicitly in the following manifest groups: 199definition is implicitly in the following manifest groups:
191default, name:monkeys, and path:barrel-of. If you place a project in the 200default, name:monkeys, and path:barrel-of. If you place a project in the
192group "notdefault", it will not be automatically downloaded by repo. 201group "notdefault", it will not be automatically downloaded by repo.
202If the project has a parent element, the `name` and `path` here
203are the prefixed ones.
193 204
194Element annotation 205Element annotation
195------------------ 206------------------
diff --git a/manifest_xml.py b/manifest_xml.py
index 122393cf..36f8ef87 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -40,6 +40,7 @@ class _Default(object):
40 remote = None 40 remote = None
41 sync_j = 1 41 sync_j = 1
42 sync_c = False 42 sync_c = False
43 sync_s = False
43 44
44class _XmlRemote(object): 45class _XmlRemote(object):
45 def __init__(self, 46 def __init__(self,
@@ -178,6 +179,9 @@ class XmlManifest(object):
178 if d.sync_c: 179 if d.sync_c:
179 have_default = True 180 have_default = True
180 e.setAttribute('sync-c', 'true') 181 e.setAttribute('sync-c', 'true')
182 if d.sync_s:
183 have_default = True
184 e.setAttribute('sync-s', 'true')
181 if have_default: 185 if have_default:
182 root.appendChild(e) 186 root.appendChild(e)
183 root.appendChild(doc.createTextNode('')) 187 root.appendChild(doc.createTextNode(''))
@@ -188,20 +192,25 @@ class XmlManifest(object):
188 root.appendChild(e) 192 root.appendChild(e)
189 root.appendChild(doc.createTextNode('')) 193 root.appendChild(doc.createTextNode(''))
190 194
191 sort_projects = list(self.projects.keys()) 195 def output_projects(parent, parent_node, projects):
192 sort_projects.sort() 196 for p in projects:
193 197 output_project(parent, parent_node, self.projects[p])
194 for p in sort_projects:
195 p = self.projects[p]
196 198
199 def output_project(parent, parent_node, p):
197 if not p.MatchesGroups(groups): 200 if not p.MatchesGroups(groups):
198 continue 201 return
202
203 name = p.name
204 relpath = p.relpath
205 if parent:
206 name = self._UnjoinName(parent.name, name)
207 relpath = self._UnjoinRelpath(parent.relpath, relpath)
199 208
200 e = doc.createElement('project') 209 e = doc.createElement('project')
201 root.appendChild(e) 210 parent_node.appendChild(e)
202 e.setAttribute('name', p.name) 211 e.setAttribute('name', name)
203 if p.relpath != p.name: 212 if relpath != name:
204 e.setAttribute('path', p.relpath) 213 e.setAttribute('path', relpath)
205 if not d.remote or p.remote.name != d.remote.name: 214 if not d.remote or p.remote.name != d.remote.name:
206 e.setAttribute('remote', p.remote.name) 215 e.setAttribute('remote', p.remote.name)
207 if peg_rev: 216 if peg_rev:
@@ -239,6 +248,19 @@ class XmlManifest(object):
239 if p.sync_c: 248 if p.sync_c:
240 e.setAttribute('sync-c', 'true') 249 e.setAttribute('sync-c', 'true')
241 250
251 if p.sync_s:
252 e.setAttribute('sync-s', 'true')
253
254 if p.subprojects:
255 sort_projects = [subp.name for subp in p.subprojects]
256 sort_projects.sort()
257 output_projects(p, e, sort_projects)
258
259 sort_projects = [key for key in self.projects.keys()
260 if not self.projects[key].parent]
261 sort_projects.sort()
262 output_projects(None, root, sort_projects)
263
242 if self._repo_hooks_project: 264 if self._repo_hooks_project:
243 root.appendChild(doc.createTextNode('')) 265 root.appendChild(doc.createTextNode(''))
244 e = doc.createElement('repo-hooks') 266 e = doc.createElement('repo-hooks')
@@ -409,14 +431,19 @@ class XmlManifest(object):
409 (self.manifestFile)) 431 (self.manifestFile))
410 self._manifest_server = url 432 self._manifest_server = url
411 433
434 def recursively_add_projects(project):
435 if self._projects.get(project.name):
436 raise ManifestParseError(
437 'duplicate project %s in %s' %
438 (project.name, self.manifestFile))
439 self._projects[project.name] = project
440 for subproject in project.subprojects:
441 recursively_add_projects(subproject)
442
412 for node in itertools.chain(*node_list): 443 for node in itertools.chain(*node_list):
413 if node.nodeName == 'project': 444 if node.nodeName == 'project':
414 project = self._ParseProject(node) 445 project = self._ParseProject(node)
415 if self._projects.get(project.name): 446 recursively_add_projects(project)
416 raise ManifestParseError(
417 'duplicate project %s in %s' %
418 (project.name, self.manifestFile))
419 self._projects[project.name] = project
420 if node.nodeName == 'repo-hooks': 447 if node.nodeName == 'repo-hooks':
421 # Get the name of the project and the (space-separated) list of enabled. 448 # Get the name of the project and the (space-separated) list of enabled.
422 repo_hooks_project = self._reqatt(node, 'in-project') 449 repo_hooks_project = self._reqatt(node, 'in-project')
@@ -524,6 +551,12 @@ class XmlManifest(object):
524 d.sync_c = False 551 d.sync_c = False
525 else: 552 else:
526 d.sync_c = sync_c.lower() in ("yes", "true", "1") 553 d.sync_c = sync_c.lower() in ("yes", "true", "1")
554
555 sync_s = node.getAttribute('sync-s')
556 if not sync_s:
557 d.sync_s = False
558 else:
559 d.sync_s = sync_s.lower() in ("yes", "true", "1")
527 return d 560 return d
528 561
529 def _ParseNotice(self, node): 562 def _ParseNotice(self, node):
@@ -565,11 +598,19 @@ class XmlManifest(object):
565 598
566 return '\n'.join(cleanLines) 599 return '\n'.join(cleanLines)
567 600
568 def _ParseProject(self, node): 601 def _JoinName(self, parent_name, name):
602 return os.path.join(parent_name, name)
603
604 def _UnjoinName(self, parent_name, name):
605 return os.path.relpath(name, parent_name)
606
607 def _ParseProject(self, node, parent = None):
569 """ 608 """
570 reads a <project> element from the manifest file 609 reads a <project> element from the manifest file
571 """ 610 """
572 name = self._reqatt(node, 'name') 611 name = self._reqatt(node, 'name')
612 if parent:
613 name = self._JoinName(parent.name, name)
573 614
574 remote = self._get_remote(node) 615 remote = self._get_remote(node)
575 if remote is None: 616 if remote is None:
@@ -607,6 +648,12 @@ class XmlManifest(object):
607 else: 648 else:
608 sync_c = sync_c.lower() in ("yes", "true", "1") 649 sync_c = sync_c.lower() in ("yes", "true", "1")
609 650
651 sync_s = node.getAttribute('sync-s')
652 if not sync_s:
653 sync_s = self._default.sync_s
654 else:
655 sync_s = sync_s.lower() in ("yes", "true", "1")
656
610 upstream = node.getAttribute('upstream') 657 upstream = node.getAttribute('upstream')
611 658
612 groups = '' 659 groups = ''
@@ -614,37 +661,67 @@ class XmlManifest(object):
614 groups = node.getAttribute('groups') 661 groups = node.getAttribute('groups')
615 groups = [x for x in re.split(r'[,\s]+', groups) if x] 662 groups = [x for x in re.split(r'[,\s]+', groups) if x]
616 663
617 default_groups = ['all', 'name:%s' % name, 'path:%s' % path] 664 if parent is None:
618 groups.extend(set(default_groups).difference(groups)) 665 relpath, worktree, gitdir = self.GetProjectPaths(name, path)
619
620 if self.IsMirror:
621 worktree = None
622 gitdir = os.path.join(self.topdir, '%s.git' % name)
623 else: 666 else:
624 worktree = os.path.join(self.topdir, path).replace('\\', '/') 667 relpath, worktree, gitdir = self.GetSubprojectPaths(parent, path)
625 gitdir = os.path.join(self.repodir, 'projects/%s.git' % path) 668
669 default_groups = ['all', 'name:%s' % name, 'path:%s' % relpath]
670 groups.extend(set(default_groups).difference(groups))
626 671
627 project = Project(manifest = self, 672 project = Project(manifest = self,
628 name = name, 673 name = name,
629 remote = remote.ToRemoteSpec(name), 674 remote = remote.ToRemoteSpec(name),
630 gitdir = gitdir, 675 gitdir = gitdir,
631 worktree = worktree, 676 worktree = worktree,
632 relpath = path, 677 relpath = relpath,
633 revisionExpr = revisionExpr, 678 revisionExpr = revisionExpr,
634 revisionId = None, 679 revisionId = None,
635 rebase = rebase, 680 rebase = rebase,
636 groups = groups, 681 groups = groups,
637 sync_c = sync_c, 682 sync_c = sync_c,
638 upstream = upstream) 683 sync_s = sync_s,
684 upstream = upstream,
685 parent = parent)
639 686
640 for n in node.childNodes: 687 for n in node.childNodes:
641 if n.nodeName == 'copyfile': 688 if n.nodeName == 'copyfile':
642 self._ParseCopyFile(project, n) 689 self._ParseCopyFile(project, n)
643 if n.nodeName == 'annotation': 690 if n.nodeName == 'annotation':
644 self._ParseAnnotation(project, n) 691 self._ParseAnnotation(project, n)
692 if n.nodeName == 'project':
693 project.subprojects.append(self._ParseProject(n, parent = project))
645 694
646 return project 695 return project
647 696
697 def GetProjectPaths(self, name, path):
698 relpath = path
699 if self.IsMirror:
700 worktree = None
701 gitdir = os.path.join(self.topdir, '%s.git' % name)
702 else:
703 worktree = os.path.join(self.topdir, path).replace('\\', '/')
704 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path)
705 return relpath, worktree, gitdir
706
707 def GetSubprojectName(self, parent, submodule_path):
708 return os.path.join(parent.name, submodule_path)
709
710 def _JoinRelpath(self, parent_relpath, relpath):
711 return os.path.join(parent_relpath, relpath)
712
713 def _UnjoinRelpath(self, parent_relpath, relpath):
714 return os.path.relpath(relpath, parent_relpath)
715
716 def GetSubprojectPaths(self, parent, path):
717 relpath = self._JoinRelpath(parent.relpath, path)
718 gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path)
719 if self.IsMirror:
720 worktree = None
721 else:
722 worktree = os.path.join(parent.worktree, path).replace('\\', '/')
723 return relpath, worktree, gitdir
724
648 def _ParseCopyFile(self, project, node): 725 def _ParseCopyFile(self, project, node):
649 src = self._reqatt(node, 'src') 726 src = self._reqatt(node, 'src')
650 dest = self._reqatt(node, 'dest') 727 dest = self._reqatt(node, 'dest')
diff --git a/project.py b/project.py
index 08b27710..46b76118 100644
--- a/project.py
+++ b/project.py
@@ -23,6 +23,7 @@ import shutil
23import stat 23import stat
24import subprocess 24import subprocess
25import sys 25import sys
26import tempfile
26import time 27import time
27 28
28from color import Coloring 29from color import Coloring
@@ -486,7 +487,30 @@ class Project(object):
486 rebase = True, 487 rebase = True,
487 groups = None, 488 groups = None,
488 sync_c = False, 489 sync_c = False,
489 upstream = None): 490 sync_s = False,
491 upstream = None,
492 parent = None,
493 is_derived = False):
494 """Init a Project object.
495
496 Args:
497 manifest: The XmlManifest object.
498 name: The `name` attribute of manifest.xml's project element.
499 remote: RemoteSpec object specifying its remote's properties.
500 gitdir: Absolute path of git directory.
501 worktree: Absolute path of git working tree.
502 relpath: Relative path of git working tree to repo's top directory.
503 revisionExpr: The `revision` attribute of manifest.xml's project element.
504 revisionId: git commit id for checking out.
505 rebase: The `rebase` attribute of manifest.xml's project element.
506 groups: The `groups` attribute of manifest.xml's project element.
507 sync_c: The `sync-c` attribute of manifest.xml's project element.
508 sync_s: The `sync-s` attribute of manifest.xml's project element.
509 upstream: The `upstream` attribute of manifest.xml's project element.
510 parent: The parent Project object.
511 is_derived: False if the project was explicitly defined in the manifest;
512 True if the project is a discovered submodule.
513 """
490 self.manifest = manifest 514 self.manifest = manifest
491 self.name = name 515 self.name = name
492 self.remote = remote 516 self.remote = remote
@@ -508,7 +532,11 @@ class Project(object):
508 self.rebase = rebase 532 self.rebase = rebase
509 self.groups = groups 533 self.groups = groups
510 self.sync_c = sync_c 534 self.sync_c = sync_c
535 self.sync_s = sync_s
511 self.upstream = upstream 536 self.upstream = upstream
537 self.parent = parent
538 self.is_derived = is_derived
539 self.subprojects = []
512 540
513 self.snapshots = {} 541 self.snapshots = {}
514 self.copyfiles = [] 542 self.copyfiles = []
@@ -529,6 +557,10 @@ class Project(object):
529 self.enabled_repo_hooks = [] 557 self.enabled_repo_hooks = []
530 558
531 @property 559 @property
560 def Derived(self):
561 return self.is_derived
562
563 @property
532 def Exists(self): 564 def Exists(self):
533 return os.path.isdir(self.gitdir) 565 return os.path.isdir(self.gitdir)
534 566
@@ -1370,6 +1402,149 @@ class Project(object):
1370 return kept 1402 return kept
1371 1403
1372 1404
1405## Submodule Management ##
1406
1407 def GetRegisteredSubprojects(self):
1408 result = []
1409 def rec(subprojects):
1410 if not subprojects:
1411 return
1412 result.extend(subprojects)
1413 for p in subprojects:
1414 rec(p.subprojects)
1415 rec(self.subprojects)
1416 return result
1417
1418 def _GetSubmodules(self):
1419 # Unfortunately we cannot call `git submodule status --recursive` here
1420 # because the working tree might not exist yet, and it cannot be used
1421 # without a working tree in its current implementation.
1422
1423 def get_submodules(gitdir, rev):
1424 # Parse .gitmodules for submodule sub_paths and sub_urls
1425 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
1426 if not sub_paths:
1427 return []
1428 # Run `git ls-tree` to read SHAs of submodule object, which happen to be
1429 # revision of submodule repository
1430 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
1431 submodules = []
1432 for sub_path, sub_url in zip(sub_paths, sub_urls):
1433 try:
1434 sub_rev = sub_revs[sub_path]
1435 except KeyError:
1436 # Ignore non-exist submodules
1437 continue
1438 submodules.append((sub_rev, sub_path, sub_url))
1439 return submodules
1440
1441 re_path = re.compile(r'^submodule\.([^.]+)\.path=(.*)$')
1442 re_url = re.compile(r'^submodule\.([^.]+)\.url=(.*)$')
1443 def parse_gitmodules(gitdir, rev):
1444 cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev]
1445 try:
1446 p = GitCommand(None, cmd, capture_stdout = True, capture_stderr = True,
1447 bare = True, gitdir = gitdir)
1448 except GitError:
1449 return [], []
1450 if p.Wait() != 0:
1451 return [], []
1452
1453 gitmodules_lines = []
1454 fd, temp_gitmodules_path = tempfile.mkstemp()
1455 try:
1456 os.write(fd, p.stdout)
1457 os.close(fd)
1458 cmd = ['config', '--file', temp_gitmodules_path, '--list']
1459 p = GitCommand(None, cmd, capture_stdout = True, capture_stderr = True,
1460 bare = True, gitdir = gitdir)
1461 if p.Wait() != 0:
1462 return [], []
1463 gitmodules_lines = p.stdout.split('\n')
1464 except GitError:
1465 return [], []
1466 finally:
1467 os.remove(temp_gitmodules_path)
1468
1469 names = set()
1470 paths = {}
1471 urls = {}
1472 for line in gitmodules_lines:
1473 if not line:
1474 continue
1475 m = re_path.match(line)
1476 if m:
1477 names.add(m.group(1))
1478 paths[m.group(1)] = m.group(2)
1479 continue
1480 m = re_url.match(line)
1481 if m:
1482 names.add(m.group(1))
1483 urls[m.group(1)] = m.group(2)
1484 continue
1485 names = sorted(names)
1486 return ([paths.get(name, '') for name in names],
1487 [urls.get(name, '') for name in names])
1488
1489 def git_ls_tree(gitdir, rev, paths):
1490 cmd = ['ls-tree', rev, '--']
1491 cmd.extend(paths)
1492 try:
1493 p = GitCommand(None, cmd, capture_stdout = True, capture_stderr = True,
1494 bare = True, gitdir = gitdir)
1495 except GitError:
1496 return []
1497 if p.Wait() != 0:
1498 return []
1499 objects = {}
1500 for line in p.stdout.split('\n'):
1501 if not line.strip():
1502 continue
1503 object_rev, object_path = line.split()[2:4]
1504 objects[object_path] = object_rev
1505 return objects
1506
1507 try:
1508 rev = self.GetRevisionId()
1509 except GitError:
1510 return []
1511 return get_submodules(self.gitdir, rev)
1512
1513 def GetDerivedSubprojects(self):
1514 result = []
1515 if not self.Exists:
1516 # If git repo does not exist yet, querying its submodules will
1517 # mess up its states; so return here.
1518 return result
1519 for rev, path, url in self._GetSubmodules():
1520 name = self.manifest.GetSubprojectName(self, path)
1521 project = self.manifest.projects.get(name)
1522 if project:
1523 result.extend(project.GetDerivedSubprojects())
1524 continue
1525 relpath, worktree, gitdir = self.manifest.GetSubprojectPaths(self, path)
1526 remote = RemoteSpec(self.remote.name,
1527 url = url,
1528 review = self.remote.review)
1529 subproject = Project(manifest = self.manifest,
1530 name = name,
1531 remote = remote,
1532 gitdir = gitdir,
1533 worktree = worktree,
1534 relpath = relpath,
1535 revisionExpr = self.revisionExpr,
1536 revisionId = rev,
1537 rebase = self.rebase,
1538 groups = self.groups,
1539 sync_c = self.sync_c,
1540 sync_s = self.sync_s,
1541 parent = self,
1542 is_derived = True)
1543 result.append(subproject)
1544 result.extend(subproject.GetDerivedSubprojects())
1545 return result
1546
1547
1373## Direct Git Commands ## 1548## Direct Git Commands ##
1374 1549
1375 def _RemoteFetch(self, name=None, 1550 def _RemoteFetch(self, name=None,
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 5b3dca78..f8094738 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -114,6 +114,9 @@ resumeable bundle file on a content delivery network. This
114may be necessary if there are problems with the local Python 114may be necessary if there are problems with the local Python
115HTTP client or proxy configuration, but the Git binary works. 115HTTP client or proxy configuration, but the Git binary works.
116 116
117The --fetch-submodules option enables fetching Git submodules
118of a project from server.
119
117SSH Connections 120SSH Connections
118--------------- 121---------------
119 122
@@ -180,6 +183,9 @@ later is required to fix a server side protocol bug.
180 p.add_option('-p', '--manifest-server-password', action='store', 183 p.add_option('-p', '--manifest-server-password', action='store',
181 dest='manifest_server_password', 184 dest='manifest_server_password',
182 help='password to authenticate with the manifest server') 185 help='password to authenticate with the manifest server')
186 p.add_option('--fetch-submodules',
187 dest='fetch_submodules', action='store_true',
188 help='fetch submodules from server')
183 if show_smart: 189 if show_smart:
184 p.add_option('-s', '--smart-sync', 190 p.add_option('-s', '--smart-sync',
185 dest='smart_sync', action='store_true', 191 dest='smart_sync', action='store_true',
@@ -559,7 +565,9 @@ later is required to fix a server side protocol bug.
559 self.manifest._Unload() 565 self.manifest._Unload()
560 if opt.jobs is None: 566 if opt.jobs is None:
561 self.jobs = self.manifest.default.sync_j 567 self.jobs = self.manifest.default.sync_j
562 all_projects = self.GetProjects(args, missing_ok=True) 568 all_projects = self.GetProjects(args,
569 missing_ok=True,
570 submodules_ok=opt.fetch_submodules)
563 571
564 self._fetch_times = _FetchTimes(self.manifest) 572 self._fetch_times = _FetchTimes(self.manifest)
565 if not opt.local_only: 573 if not opt.local_only:
@@ -570,12 +578,33 @@ later is required to fix a server side protocol bug.
570 to_fetch.extend(all_projects) 578 to_fetch.extend(all_projects)
571 to_fetch.sort(key=self._fetch_times.Get, reverse=True) 579 to_fetch.sort(key=self._fetch_times.Get, reverse=True)
572 580
573 self._Fetch(to_fetch, opt) 581 fetched = self._Fetch(to_fetch, opt)
574 _PostRepoFetch(rp, opt.no_repo_verify) 582 _PostRepoFetch(rp, opt.no_repo_verify)
575 if opt.network_only: 583 if opt.network_only:
576 # bail out now; the rest touches the working tree 584 # bail out now; the rest touches the working tree
577 return 585 return
578 586
587 # Iteratively fetch missing and/or nested unregistered submodules
588 previously_missing_set = set()
589 while True:
590 self.manifest._Unload()
591 all_projects = self.GetProjects(args,
592 missing_ok=True,
593 submodules_ok=opt.fetch_submodules)
594 missing = []
595 for project in all_projects:
596 if project.gitdir not in fetched:
597 missing.append(project)
598 if not missing:
599 break
600 # Stop us from non-stopped fetching actually-missing repos: If set of
601 # missing repos has not been changed from last fetch, we break.
602 missing_set = set(p.name for p in missing)
603 if previously_missing_set == missing_set:
604 break
605 previously_missing_set = missing_set
606 fetched.update(self._Fetch(missing, opt))
607
579 if self.manifest.IsMirror: 608 if self.manifest.IsMirror:
580 # bail out now, we have no working tree 609 # bail out now, we have no working tree
581 return 610 return