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-10-23 16:08:58 -0700
commit69998b0c6ff724bf620480140ccce648fec7d6a9 (patch)
treeb6f9c4c00b04a0f140074c4c2dba91ed4f055b11
parent5c6eeac8f0350fd6b14cf226ffcff655f1dd9582 (diff)
downloadgit-repo-69998b0c6ff724bf620480140ccce648fec7d6a9.tar.gz
Represent git-submodule as nested projects
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: I541e9e2ac1a70304272dbe09724572aa1004eb5c
-rw-r--r--command.py72
-rw-r--r--docs/manifest-format.txt15
-rw-r--r--manifest_xml.py108
-rw-r--r--project.py180
-rw-r--r--subcmds/sync.py21
5 files changed, 341 insertions, 55 deletions
diff --git a/command.py b/command.py
index 5a5f468f..51c0cb48 100644
--- a/command.py
+++ b/command.py
@@ -60,6 +60,32 @@ class Command(object):
60 """ 60 """
61 raise NotImplementedError 61 raise NotImplementedError
62 62
63 def _ResetPathToProjectMap(self, projects):
64 self._by_path = dict((p.worktree, p) for p in projects)
65
66 def _UpdatePathToProjectMap(self, project):
67 self._by_path[project.worktree] = project
68
69 def _GetProjectByPath(self, path):
70 project = None
71 if os.path.exists(path):
72 oldpath = None
73 while path \
74 and path != oldpath \
75 and path != self.manifest.topdir:
76 try:
77 project = self._by_path[path]
78 break
79 except KeyError:
80 oldpath = path
81 path = os.path.dirname(path)
82 else:
83 try:
84 project = self._by_path[path]
85 except KeyError:
86 pass
87 return project
88
63 def GetProjects(self, args, missing_ok=False): 89 def GetProjects(self, args, missing_ok=False):
64 """A list of projects that match the arguments. 90 """A list of projects that match the arguments.
65 """ 91 """
@@ -74,40 +100,38 @@ class Command(object):
74 groups = [x for x in re.split('[,\s]+', groups) if x] 100 groups = [x for x in re.split('[,\s]+', groups) if x]
75 101
76 if not args: 102 if not args:
77 for project in all_projects.values(): 103 all_projects_list = all_projects.values()
104 derived_projects = []
105 for project in all_projects_list:
106 if project.Registered:
107 # Do not search registered subproject for derived projects
108 # since its parent has been searched already
109 continue
110 derived_projects.extend(project.GetDerivedSubprojects())
111 all_projects_list.extend(derived_projects)
112 for project in all_projects_list:
78 if ((missing_ok or project.Exists) and 113 if ((missing_ok or project.Exists) and
79 project.MatchesGroups(groups)): 114 project.MatchesGroups(groups)):
80 result.append(project) 115 result.append(project)
81 else: 116 else:
82 by_path = None 117 self._ResetPathToProjectMap(all_projects.values())
83 118
84 for arg in args: 119 for arg in args:
85 project = all_projects.get(arg) 120 project = all_projects.get(arg)
86 121
87 if not project: 122 if not project:
88 path = os.path.abspath(arg).replace('\\', '/') 123 path = os.path.abspath(arg).replace('\\', '/')
89 124 project = self._GetProjectByPath(path)
90 if not by_path: 125
91 by_path = dict() 126 # If it's not a derived project, update path->project mapping and
92 for p in all_projects.values(): 127 # search again, as arg might actually point to a derived subproject.
93 by_path[p.worktree] = p 128 if project and not project.Derived:
94 129 search_again = False
95 if os.path.exists(path): 130 for subproject in project.GetDerivedSubprojects():
96 oldpath = None 131 self._UpdatePathToProjectMap(subproject)
97 while path \ 132 search_again = True
98 and path != oldpath \ 133 if search_again:
99 and path != self.manifest.topdir: 134 project = self._GetProjectByPath(path) or project
100 try:
101 project = by_path[path]
102 break
103 except KeyError:
104 oldpath = path
105 path = os.path.dirname(path)
106 else:
107 try:
108 project = by_path[path]
109 except KeyError:
110 pass
111 135
112 if not project: 136 if not project:
113 raise NoSuchProjectError(arg) 137 raise NoSuchProjectError(arg)
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt
index f499868c..a36af67c 100644
--- a/docs/manifest-format.txt
+++ b/docs/manifest-format.txt
@@ -45,7 +45,8 @@ following DTD:
45 <!ELEMENT manifest-server (EMPTY)> 45 <!ELEMENT manifest-server (EMPTY)>
46 <!ATTLIST url CDATA #REQUIRED> 46 <!ATTLIST url CDATA #REQUIRED>
47 47
48 <!ELEMENT project (annotation?)> 48 <!ELEMENT project (annotation?,
49 project*)>
49 <!ATTLIST project name CDATA #REQUIRED> 50 <!ATTLIST project name CDATA #REQUIRED>
50 <!ATTLIST project path CDATA #IMPLIED> 51 <!ATTLIST project path CDATA #IMPLIED>
51 <!ATTLIST project remote IDREF #IMPLIED> 52 <!ATTLIST project remote IDREF #IMPLIED>
@@ -152,7 +153,10 @@ Element project
152 153
153One or more project elements may be specified. Each element 154One or more project elements may be specified. Each element
154describes a single Git repository to be cloned into the repo 155describes a single Git repository to be cloned into the repo
155client workspace. 156client workspace. You may specify Git-submodules by creating a
157nested project. Git-submodules will be automatically
158recognized and inherit their parent's attributes, but those
159may be overridden by an explicitly specified project element.
156 160
157Attribute `name`: A unique name for this project. The project's 161Attribute `name`: A unique name for this project. The project's
158name is appended onto its remote's fetch URL to generate the actual 162name is appended onto its remote's fetch URL to generate the actual
@@ -163,7 +167,8 @@ URL to configure the Git remote with. The URL gets formed as:
163where ${remote_fetch} is the remote's fetch attribute and 167where ${remote_fetch} is the remote's fetch attribute and
164${project_name} is the project's name attribute. The suffix ".git" 168${project_name} is the project's name attribute. The suffix ".git"
165is always appended as repo assumes the upstream is a forest of 169is always appended as repo assumes the upstream is a forest of
166bare Git repositories. 170bare Git repositories. If the project has a parent element, its
171name will be prefixed by the parent's.
167 172
168The project name must match the name Gerrit knows, if Gerrit is 173The project name must match the name Gerrit knows, if Gerrit is
169being used for code reviews. 174being used for code reviews.
@@ -171,6 +176,8 @@ being used for code reviews.
171Attribute `path`: An optional path relative to the top directory 176Attribute `path`: An optional path relative to the top directory
172of the repo client where the Git working directory for this project 177of the repo client where the Git working directory for this project
173should be placed. If not supplied the project name is used. 178should be placed. If not supplied the project name is used.
179If the project has a parent element, its path will be prefixed
180by the parent's.
174 181
175Attribute `remote`: Name of a previously defined remote element. 182Attribute `remote`: Name of a previously defined remote element.
176If not supplied the remote given by the default element is used. 183If not supplied the remote given by the default element is used.
@@ -190,6 +197,8 @@ its name:`name` and path:`path`. E.g. for
190definition is implicitly in the following manifest groups: 197definition is implicitly in the following manifest groups:
191default, name:monkeys, and path:barrel-of. If you place a project in the 198default, name:monkeys, and path:barrel-of. If you place a project in the
192group "notdefault", it will not be automatically downloaded by repo. 199group "notdefault", it will not be automatically downloaded by repo.
200If the project has a parent element, the `name` and `path` here
201are the prefixed ones.
193 202
194Element annotation 203Element annotation
195------------------ 204------------------
diff --git a/manifest_xml.py b/manifest_xml.py
index 04cabaad..a2a56e92 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -180,20 +180,25 @@ class XmlManifest(object):
180 root.appendChild(e) 180 root.appendChild(e)
181 root.appendChild(doc.createTextNode('')) 181 root.appendChild(doc.createTextNode(''))
182 182
183 sort_projects = list(self.projects.keys()) 183 def output_projects(parent, parent_node, projects):
184 sort_projects.sort() 184 for p in projects:
185 185 output_project(parent, parent_node, self.projects[p])
186 for p in sort_projects:
187 p = self.projects[p]
188 186
187 def output_project(parent, parent_node, p):
189 if not p.MatchesGroups(groups): 188 if not p.MatchesGroups(groups):
190 continue 189 return
190
191 name = p.name
192 relpath = p.relpath
193 if parent:
194 name = self._UnjoinName(parent.name, name)
195 relpath = self._UnjoinRelpath(parent.relpath, relpath)
191 196
192 e = doc.createElement('project') 197 e = doc.createElement('project')
193 root.appendChild(e) 198 parent_node.appendChild(e)
194 e.setAttribute('name', p.name) 199 e.setAttribute('name', name)
195 if p.relpath != p.name: 200 if relpath != name:
196 e.setAttribute('path', p.relpath) 201 e.setAttribute('path', relpath)
197 if not d.remote or p.remote.name != d.remote.name: 202 if not d.remote or p.remote.name != d.remote.name:
198 e.setAttribute('remote', p.remote.name) 203 e.setAttribute('remote', p.remote.name)
199 if peg_rev: 204 if peg_rev:
@@ -231,6 +236,16 @@ class XmlManifest(object):
231 if p.sync_c: 236 if p.sync_c:
232 e.setAttribute('sync-c', 'true') 237 e.setAttribute('sync-c', 'true')
233 238
239 if p.subprojects:
240 sort_projects = [subp.name for subp in p.subprojects]
241 sort_projects.sort()
242 output_projects(p, e, sort_projects)
243
244 sort_projects = [key for key in self.projects.keys()
245 if not self.projects[key].parent]
246 sort_projects.sort()
247 output_projects(None, root, sort_projects)
248
234 if self._repo_hooks_project: 249 if self._repo_hooks_project:
235 root.appendChild(doc.createTextNode('')) 250 root.appendChild(doc.createTextNode(''))
236 e = doc.createElement('repo-hooks') 251 e = doc.createElement('repo-hooks')
@@ -383,11 +398,15 @@ class XmlManifest(object):
383 for node in itertools.chain(*node_list): 398 for node in itertools.chain(*node_list):
384 if node.nodeName == 'project': 399 if node.nodeName == 'project':
385 project = self._ParseProject(node) 400 project = self._ParseProject(node)
386 if self._projects.get(project.name): 401 def recursively_add_projects(project):
387 raise ManifestParseError( 402 if self._projects.get(project.name):
388 'duplicate project %s in %s' % 403 raise ManifestParseError(
389 (project.name, self.manifestFile)) 404 'duplicate project %s in %s' %
390 self._projects[project.name] = project 405 (project.name, self.manifestFile))
406 self._projects[project.name] = project
407 for subproject in project.subprojects:
408 recursively_add_projects(subproject)
409 recursively_add_projects(project)
391 if node.nodeName == 'repo-hooks': 410 if node.nodeName == 'repo-hooks':
392 # Get the name of the project and the (space-separated) list of enabled. 411 # Get the name of the project and the (space-separated) list of enabled.
393 repo_hooks_project = self._reqatt(node, 'in-project') 412 repo_hooks_project = self._reqatt(node, 'in-project')
@@ -537,11 +556,19 @@ class XmlManifest(object):
537 556
538 return '\n'.join(cleanLines) 557 return '\n'.join(cleanLines)
539 558
540 def _ParseProject(self, node): 559 def _JoinName(self, parent_name, name):
560 return os.path.join(parent_name, name)
561
562 def _UnjoinName(self, parent_name, name):
563 return os.path.relpath(name, parent_name)
564
565 def _ParseProject(self, node, parent = None):
541 """ 566 """
542 reads a <project> element from the manifest file 567 reads a <project> element from the manifest file
543 """ 568 """
544 name = self._reqatt(node, 'name') 569 name = self._reqatt(node, 'name')
570 if parent:
571 name = self._JoinName(parent.name, name)
545 572
546 remote = self._get_remote(node) 573 remote = self._get_remote(node)
547 if remote is None: 574 if remote is None:
@@ -586,37 +613,66 @@ class XmlManifest(object):
586 groups = node.getAttribute('groups') 613 groups = node.getAttribute('groups')
587 groups = [x for x in re.split('[,\s]+', groups) if x] 614 groups = [x for x in re.split('[,\s]+', groups) if x]
588 615
589 default_groups = ['all', 'name:%s' % name, 'path:%s' % path] 616 if parent is None:
590 groups.extend(set(default_groups).difference(groups)) 617 relpath, worktree, gitdir = self.GetProjectPaths(name, path)
591
592 if self.IsMirror:
593 worktree = None
594 gitdir = os.path.join(self.topdir, '%s.git' % name)
595 else: 618 else:
596 worktree = os.path.join(self.topdir, path).replace('\\', '/') 619 relpath, worktree, gitdir = self.GetSubprojectPaths(parent, path)
597 gitdir = os.path.join(self.repodir, 'projects/%s.git' % path) 620
621 default_groups = ['all', 'name:%s' % name, 'path:%s' % relpath]
622 groups.extend(set(default_groups).difference(groups))
598 623
599 project = Project(manifest = self, 624 project = Project(manifest = self,
600 name = name, 625 name = name,
601 remote = remote.ToRemoteSpec(name), 626 remote = remote.ToRemoteSpec(name),
602 gitdir = gitdir, 627 gitdir = gitdir,
603 worktree = worktree, 628 worktree = worktree,
604 relpath = path, 629 relpath = relpath,
605 revisionExpr = revisionExpr, 630 revisionExpr = revisionExpr,
606 revisionId = None, 631 revisionId = None,
607 rebase = rebase, 632 rebase = rebase,
608 groups = groups, 633 groups = groups,
609 sync_c = sync_c, 634 sync_c = sync_c,
610 upstream = upstream) 635 upstream = upstream,
636 parent = parent)
611 637
612 for n in node.childNodes: 638 for n in node.childNodes:
613 if n.nodeName == 'copyfile': 639 if n.nodeName == 'copyfile':
614 self._ParseCopyFile(project, n) 640 self._ParseCopyFile(project, n)
615 if n.nodeName == 'annotation': 641 if n.nodeName == 'annotation':
616 self._ParseAnnotation(project, n) 642 self._ParseAnnotation(project, n)
643 if n.nodeName == 'project':
644 project.subprojects.append(self._ParseProject(n, parent = project))
617 645
618 return project 646 return project
619 647
648 def GetProjectPaths(self, name, path):
649 relpath = path
650 if self.IsMirror:
651 worktree = None
652 gitdir = os.path.join(self.topdir, '%s.git' % name)
653 else:
654 worktree = os.path.join(self.topdir, path).replace('\\', '/')
655 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path)
656 return relpath, worktree, gitdir
657
658 def GetSubprojectName(self, parent, submodule_path):
659 return os.path.join(parent.name, submodule_path)
660
661 def _JoinRelpath(self, parent_relpath, relpath):
662 return os.path.join(parent_relpath, relpath)
663
664 def _UnjoinRelpath(self, parent_relpath, relpath):
665 return os.path.relpath(relpath, parent_relpath)
666
667 def GetSubprojectPaths(self, parent, path):
668 relpath = self._JoinRelpath(parent.relpath, path)
669 gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path)
670 if self.IsMirror:
671 worktree = None
672 else:
673 worktree = os.path.join(parent.worktree, path).replace('\\', '/')
674 return relpath, worktree, gitdir
675
620 def _ParseCopyFile(self, project, node): 676 def _ParseCopyFile(self, project, node):
621 src = self._reqatt(node, 'src') 677 src = self._reqatt(node, 'src')
622 dest = self._reqatt(node, 'dest') 678 dest = self._reqatt(node, 'dest')
diff --git a/project.py b/project.py
index 472b1d32..2989d380 100644
--- a/project.py
+++ b/project.py
@@ -22,6 +22,7 @@ import shutil
22import stat 22import stat
23import subprocess 23import subprocess
24import sys 24import sys
25import tempfile
25import time 26import time
26 27
27from color import Coloring 28from color import Coloring
@@ -484,7 +485,28 @@ class Project(object):
484 rebase = True, 485 rebase = True,
485 groups = None, 486 groups = None,
486 sync_c = False, 487 sync_c = False,
487 upstream = None): 488 upstream = None,
489 parent = None,
490 is_derived = False):
491 """Init a Project object.
492
493 Args:
494 manifest: The XmlManifest object.
495 name: The `name` attribute of manifest.xml's project element.
496 remote: RemoteSpec object specifying its remote's properties.
497 gitdir: Absolute path of git directory.
498 worktree: Absolute path of git working tree.
499 relpath: Relative path of git working tree to repo's top directory.
500 revisionExpr: The `revision` attribute of manifest.xml's project element.
501 revisionId: git commit id for checking out.
502 rebase: The `rebase` attribute of manifest.xml's project element.
503 groups: The `groups` attribute of manifest.xml's project element.
504 sync_c: The `sync-c` attribute of manifest.xml's project element.
505 upstream: The `upstream` attribute of manifest.xml's project element.
506 parent: The parent Project object.
507 is_derived: False if the project was explicitly defined in the manifest;
508 True if the project is a discovered submodule.
509 """
488 self.manifest = manifest 510 self.manifest = manifest
489 self.name = name 511 self.name = name
490 self.remote = remote 512 self.remote = remote
@@ -507,6 +529,9 @@ class Project(object):
507 self.groups = groups 529 self.groups = groups
508 self.sync_c = sync_c 530 self.sync_c = sync_c
509 self.upstream = upstream 531 self.upstream = upstream
532 self.parent = parent
533 self.is_derived = is_derived
534 self.subprojects = []
510 535
511 self.snapshots = {} 536 self.snapshots = {}
512 self.copyfiles = [] 537 self.copyfiles = []
@@ -527,6 +552,14 @@ class Project(object):
527 self.enabled_repo_hooks = [] 552 self.enabled_repo_hooks = []
528 553
529 @property 554 @property
555 def Registered(self):
556 return self.parent and not self.is_derived
557
558 @property
559 def Derived(self):
560 return self.is_derived
561
562 @property
530 def Exists(self): 563 def Exists(self):
531 return os.path.isdir(self.gitdir) 564 return os.path.isdir(self.gitdir)
532 565
@@ -1370,6 +1403,151 @@ class Project(object):
1370 return kept 1403 return kept
1371 1404
1372 1405
1406## Submodule Management ##
1407
1408 def GetRegisteredSubprojects(self):
1409 result = []
1410 def rec(subprojects):
1411 if not subprojects:
1412 return
1413 result.extend(subprojects)
1414 for p in subprojects:
1415 rec(p.subprojects)
1416 rec(self.subprojects)
1417 return result
1418
1419 def _GetSubmodules(self):
1420 # Unfortunately we cannot call `git submodule status --recursive` here
1421 # because the working tree might not exist yet, and it cannot be used
1422 # without a working tree in its current implementation.
1423
1424 def get_submodules(gitdir, rev, path):
1425 # Parse .gitmodules for submodule sub_paths and sub_urls
1426 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
1427 if not sub_paths:
1428 return []
1429 # Run `git ls-tree` to read SHAs of submodule object, which happen to be
1430 # revision of submodule repository
1431 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
1432 submodules = []
1433 for sub_path, sub_url in zip(sub_paths, sub_urls):
1434 try:
1435 sub_rev = sub_revs[sub_path]
1436 except KeyError:
1437 # Ignore non-exist submodules
1438 continue
1439 sub_gitdir = self.manifest.GetSubprojectPaths(self, sub_path)[2]
1440 submodules.append((sub_rev, sub_path, sub_url))
1441 return submodules
1442
1443 re_path = re.compile(r'submodule.(\w+).path')
1444 re_url = re.compile(r'submodule.(\w+).url')
1445 def parse_gitmodules(gitdir, rev):
1446 cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev]
1447 try:
1448 p = GitCommand(None, cmd, capture_stdout = True, capture_stderr = True,
1449 bare = True, gitdir = gitdir)
1450 except GitError as e:
1451 return [], []
1452 if p.Wait() != 0:
1453 return [], []
1454
1455 gitmodules_lines = []
1456 fd, temp_gitmodules_path = tempfile.mkstemp()
1457 try:
1458 os.write(fd, p.stdout)
1459 os.close(fd)
1460 cmd = ['config', '--file', temp_gitmodules_path, '--list']
1461 p = GitCommand(None, cmd, capture_stdout = True, capture_stderr = True,
1462 bare = True, gitdir = gitdir)
1463 if p.Wait() != 0:
1464 return [], []
1465 gitmodules_lines = p.stdout.split('\n')
1466 except GitError as e:
1467 return [], []
1468 finally:
1469 os.remove(temp_gitmodules_path)
1470
1471 names = set()
1472 paths = {}
1473 urls = {}
1474 for line in gitmodules_lines:
1475 if not line:
1476 continue
1477 key, value = line.split('=')
1478 m = re_path.match(key)
1479 if m:
1480 names.add(m.group(1))
1481 paths[m.group(1)] = value
1482 continue
1483 m = re_url.match(key)
1484 if m:
1485 names.add(m.group(1))
1486 urls[m.group(1)] = value
1487 continue
1488 names = sorted(names)
1489 return [paths[name] for name in names], [urls[name] for name in names]
1490
1491 def git_ls_tree(gitdir, rev, paths):
1492 cmd = ['ls-tree', rev, '--']
1493 cmd.extend(paths)
1494 try:
1495 p = GitCommand(None, cmd, capture_stdout = True, capture_stderr = True,
1496 bare = True, gitdir = gitdir)
1497 except GitError:
1498 return []
1499 if p.Wait() != 0:
1500 return []
1501 objects = {}
1502 for line in p.stdout.split('\n'):
1503 if not line.strip():
1504 continue
1505 object_rev, object_path = line.split()[2:4]
1506 objects[object_path] = object_rev
1507 return objects
1508
1509 try:
1510 rev = self.GetRevisionId()
1511 except GitError:
1512 return []
1513 return get_submodules(self.gitdir, rev, '')
1514
1515 def GetDerivedSubprojects(self):
1516 result = []
1517 if not self.Exists:
1518 # If git repo does not exist yet, querying its submodules will
1519 # mess up its states; so return here.
1520 return result
1521 for rev, path, url in self._GetSubmodules():
1522 name = self.manifest.GetSubprojectName(self, path)
1523 project = self.manifest.projects.get(name)
1524 if project and project.Registered:
1525 # If it has been registered, skip it because we are searching
1526 # derived subprojects, but search for its derived subprojects.
1527 result.extend(project.GetDerivedSubprojects())
1528 continue
1529 relpath, worktree, gitdir = self.manifest.GetSubprojectPaths(self, path)
1530 remote = RemoteSpec(self.remote.name,
1531 url = url,
1532 review = self.remote.review)
1533 subproject = Project(manifest = self.manifest,
1534 name = name,
1535 remote = remote,
1536 gitdir = gitdir,
1537 worktree = worktree,
1538 relpath = relpath,
1539 revisionExpr = self.revisionExpr,
1540 revisionId = rev,
1541 rebase = self.rebase,
1542 groups = self.groups,
1543 sync_c = self.sync_c,
1544 parent = self,
1545 is_derived = True)
1546 result.append(subproject)
1547 result.extend(subproject.GetDerivedSubprojects())
1548 return result
1549
1550
1373## Direct Git Commands ## 1551## Direct Git Commands ##
1374 1552
1375 def _RemoteFetch(self, name=None, 1553 def _RemoteFetch(self, name=None,
diff --git a/subcmds/sync.py b/subcmds/sync.py
index e68a025e..90e2908f 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -503,12 +503,31 @@ uncommitted changes are present' % project.relpath
503 to_fetch.append(rp) 503 to_fetch.append(rp)
504 to_fetch.extend(all_projects) 504 to_fetch.extend(all_projects)
505 505
506 self._Fetch(to_fetch, opt) 506 fetched = self._Fetch(to_fetch, opt)
507 _PostRepoFetch(rp, opt.no_repo_verify) 507 _PostRepoFetch(rp, opt.no_repo_verify)
508 if opt.network_only: 508 if opt.network_only:
509 # bail out now; the rest touches the working tree 509 # bail out now; the rest touches the working tree
510 return 510 return
511 511
512 # Iteratively fetch missing and/or nested unregistered submodules
513 previously_missing_set = set()
514 while True:
515 self.manifest._Unload()
516 all = self.GetProjects(args, missing_ok=True)
517 missing = []
518 for project in all:
519 if project.gitdir not in fetched:
520 missing.append(project)
521 if not missing:
522 break
523 # Stop us from non-stopped fetching actually-missing repos: If set of
524 # missing repos has not been changed from last fetch, we break.
525 missing_set = set(p.name for p in missing)
526 if previously_missing_set == missing_set:
527 break
528 previously_missing_set = missing_set
529 fetched.update(self._Fetch(missing, opt))
530
512 if self.manifest.IsMirror: 531 if self.manifest.IsMirror:
513 # bail out now, we have no working tree 532 # bail out now, we have no working tree
514 return 533 return