summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--command.py73
-rw-r--r--docs/manifest-format.txt17
-rw-r--r--manifest_xml.py154
-rw-r--r--project.py180
-rw-r--r--subcmds/sync.py40
5 files changed, 399 insertions, 65 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 6606575c..53f33537 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,
@@ -64,12 +65,19 @@ class _XmlRemote(object):
64 def _resolveFetchUrl(self): 65 def _resolveFetchUrl(self):
65 url = self.fetchUrl.rstrip('/') 66 url = self.fetchUrl.rstrip('/')
66 manifestUrl = self.manifestUrl.rstrip('/') 67 manifestUrl = self.manifestUrl.rstrip('/')
68 p = manifestUrl.startswith('persistent-http')
69 if p:
70 manifestUrl = manifestUrl[len('persistent-'):]
71
67 # urljoin will get confused if there is no scheme in the base url 72 # urljoin will get confused if there is no scheme in the base url
68 # ie, if manifestUrl is of the form <hostname:port> 73 # ie, if manifestUrl is of the form <hostname:port>
69 if manifestUrl.find(':') != manifestUrl.find('/') - 1: 74 if manifestUrl.find(':') != manifestUrl.find('/') - 1:
70 manifestUrl = 'gopher://' + manifestUrl 75 manifestUrl = 'gopher://' + manifestUrl
71 url = urlparse.urljoin(manifestUrl, url) 76 url = urlparse.urljoin(manifestUrl, url)
72 return re.sub(r'^gopher://', '', url) 77 url = re.sub(r'^gopher://', '', url)
78 if p:
79 url = 'persistent-' + url
80 return url
73 81
74 def ToRemoteSpec(self, projectName): 82 def ToRemoteSpec(self, projectName):
75 url = self.resolvedFetchUrl.rstrip('/') + '/' + projectName 83 url = self.resolvedFetchUrl.rstrip('/') + '/' + projectName
@@ -138,9 +146,8 @@ class XmlManifest(object):
138 mp = self.manifestProject 146 mp = self.manifestProject
139 147
140 groups = mp.config.GetString('manifest.groups') 148 groups = mp.config.GetString('manifest.groups')
141 if not groups: 149 if groups:
142 groups = 'all' 150 groups = [x for x in re.split(r'[,\s]+', groups) if x]
143 groups = [x for x in re.split(r'[,\s]+', groups) if x]
144 151
145 doc = xml.dom.minidom.Document() 152 doc = xml.dom.minidom.Document()
146 root = doc.createElement('manifest') 153 root = doc.createElement('manifest')
@@ -178,6 +185,9 @@ class XmlManifest(object):
178 if d.sync_c: 185 if d.sync_c:
179 have_default = True 186 have_default = True
180 e.setAttribute('sync-c', 'true') 187 e.setAttribute('sync-c', 'true')
188 if d.sync_s:
189 have_default = True
190 e.setAttribute('sync-s', 'true')
181 if have_default: 191 if have_default:
182 root.appendChild(e) 192 root.appendChild(e)
183 root.appendChild(doc.createTextNode('')) 193 root.appendChild(doc.createTextNode(''))
@@ -188,20 +198,25 @@ class XmlManifest(object):
188 root.appendChild(e) 198 root.appendChild(e)
189 root.appendChild(doc.createTextNode('')) 199 root.appendChild(doc.createTextNode(''))
190 200
191 sort_projects = list(self.projects.keys()) 201 def output_projects(parent, parent_node, projects):
192 sort_projects.sort() 202 for p in projects:
193 203 output_project(parent, parent_node, self.projects[p])
194 for p in sort_projects:
195 p = self.projects[p]
196 204
205 def output_project(parent, parent_node, p):
197 if not p.MatchesGroups(groups): 206 if not p.MatchesGroups(groups):
198 continue 207 return
208
209 name = p.name
210 relpath = p.relpath
211 if parent:
212 name = self._UnjoinName(parent.name, name)
213 relpath = self._UnjoinRelpath(parent.relpath, relpath)
199 214
200 e = doc.createElement('project') 215 e = doc.createElement('project')
201 root.appendChild(e) 216 parent_node.appendChild(e)
202 e.setAttribute('name', p.name) 217 e.setAttribute('name', name)
203 if p.relpath != p.name: 218 if relpath != name:
204 e.setAttribute('path', p.relpath) 219 e.setAttribute('path', relpath)
205 if not d.remote or p.remote.name != d.remote.name: 220 if not d.remote or p.remote.name != d.remote.name:
206 e.setAttribute('remote', p.remote.name) 221 e.setAttribute('remote', p.remote.name)
207 if peg_rev: 222 if peg_rev:
@@ -239,6 +254,19 @@ class XmlManifest(object):
239 if p.sync_c: 254 if p.sync_c:
240 e.setAttribute('sync-c', 'true') 255 e.setAttribute('sync-c', 'true')
241 256
257 if p.sync_s:
258 e.setAttribute('sync-s', 'true')
259
260 if p.subprojects:
261 sort_projects = [subp.name for subp in p.subprojects]
262 sort_projects.sort()
263 output_projects(p, e, sort_projects)
264
265 sort_projects = [key for key in self.projects.keys()
266 if not self.projects[key].parent]
267 sort_projects.sort()
268 output_projects(None, root, sort_projects)
269
242 if self._repo_hooks_project: 270 if self._repo_hooks_project:
243 root.appendChild(doc.createTextNode('')) 271 root.appendChild(doc.createTextNode(''))
244 e = doc.createElement('repo-hooks') 272 e = doc.createElement('repo-hooks')
@@ -317,13 +345,20 @@ class XmlManifest(object):
317 for local_file in sorted(os.listdir(local_dir)): 345 for local_file in sorted(os.listdir(local_dir)):
318 if local_file.endswith('.xml'): 346 if local_file.endswith('.xml'):
319 try: 347 try:
320 nodes.append(self._ParseManifestXml(local_file, self.repodir)) 348 local = os.path.join(local_dir, local_file)
349 nodes.append(self._ParseManifestXml(local, self.repodir))
321 except ManifestParseError as e: 350 except ManifestParseError as e:
322 print('%s' % str(e), file=sys.stderr) 351 print('%s' % str(e), file=sys.stderr)
323 except OSError: 352 except OSError:
324 pass 353 pass
325 354
326 self._ParseManifest(nodes) 355 try:
356 self._ParseManifest(nodes)
357 except ManifestParseError as e:
358 # There was a problem parsing, unload ourselves in case they catch
359 # this error and try again later, we will show the correct error
360 self._Unload()
361 raise e
327 362
328 if self.IsMirror: 363 if self.IsMirror:
329 self._AddMetaProjectMirror(self.repoProject) 364 self._AddMetaProjectMirror(self.repoProject)
@@ -409,14 +444,19 @@ class XmlManifest(object):
409 (self.manifestFile)) 444 (self.manifestFile))
410 self._manifest_server = url 445 self._manifest_server = url
411 446
447 def recursively_add_projects(project):
448 if self._projects.get(project.name):
449 raise ManifestParseError(
450 'duplicate project %s in %s' %
451 (project.name, self.manifestFile))
452 self._projects[project.name] = project
453 for subproject in project.subprojects:
454 recursively_add_projects(subproject)
455
412 for node in itertools.chain(*node_list): 456 for node in itertools.chain(*node_list):
413 if node.nodeName == 'project': 457 if node.nodeName == 'project':
414 project = self._ParseProject(node) 458 project = self._ParseProject(node)
415 if self._projects.get(project.name): 459 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': 460 if node.nodeName == 'repo-hooks':
421 # Get the name of the project and the (space-separated) list of enabled. 461 # Get the name of the project and the (space-separated) list of enabled.
422 repo_hooks_project = self._reqatt(node, 'in-project') 462 repo_hooks_project = self._reqatt(node, 'in-project')
@@ -524,6 +564,12 @@ class XmlManifest(object):
524 d.sync_c = False 564 d.sync_c = False
525 else: 565 else:
526 d.sync_c = sync_c.lower() in ("yes", "true", "1") 566 d.sync_c = sync_c.lower() in ("yes", "true", "1")
567
568 sync_s = node.getAttribute('sync-s')
569 if not sync_s:
570 d.sync_s = False
571 else:
572 d.sync_s = sync_s.lower() in ("yes", "true", "1")
527 return d 573 return d
528 574
529 def _ParseNotice(self, node): 575 def _ParseNotice(self, node):
@@ -565,11 +611,19 @@ class XmlManifest(object):
565 611
566 return '\n'.join(cleanLines) 612 return '\n'.join(cleanLines)
567 613
568 def _ParseProject(self, node): 614 def _JoinName(self, parent_name, name):
615 return os.path.join(parent_name, name)
616
617 def _UnjoinName(self, parent_name, name):
618 return os.path.relpath(name, parent_name)
619
620 def _ParseProject(self, node, parent = None):
569 """ 621 """
570 reads a <project> element from the manifest file 622 reads a <project> element from the manifest file
571 """ 623 """
572 name = self._reqatt(node, 'name') 624 name = self._reqatt(node, 'name')
625 if parent:
626 name = self._JoinName(parent.name, name)
573 627
574 remote = self._get_remote(node) 628 remote = self._get_remote(node)
575 if remote is None: 629 if remote is None:
@@ -607,6 +661,12 @@ class XmlManifest(object):
607 else: 661 else:
608 sync_c = sync_c.lower() in ("yes", "true", "1") 662 sync_c = sync_c.lower() in ("yes", "true", "1")
609 663
664 sync_s = node.getAttribute('sync-s')
665 if not sync_s:
666 sync_s = self._default.sync_s
667 else:
668 sync_s = sync_s.lower() in ("yes", "true", "1")
669
610 upstream = node.getAttribute('upstream') 670 upstream = node.getAttribute('upstream')
611 671
612 groups = '' 672 groups = ''
@@ -614,37 +674,67 @@ class XmlManifest(object):
614 groups = node.getAttribute('groups') 674 groups = node.getAttribute('groups')
615 groups = [x for x in re.split(r'[,\s]+', groups) if x] 675 groups = [x for x in re.split(r'[,\s]+', groups) if x]
616 676
617 default_groups = ['all', 'name:%s' % name, 'path:%s' % path] 677 if parent is None:
618 groups.extend(set(default_groups).difference(groups)) 678 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: 679 else:
624 worktree = os.path.join(self.topdir, path).replace('\\', '/') 680 relpath, worktree, gitdir = self.GetSubprojectPaths(parent, path)
625 gitdir = os.path.join(self.repodir, 'projects/%s.git' % path) 681
682 default_groups = ['all', 'name:%s' % name, 'path:%s' % relpath]
683 groups.extend(set(default_groups).difference(groups))
626 684
627 project = Project(manifest = self, 685 project = Project(manifest = self,
628 name = name, 686 name = name,
629 remote = remote.ToRemoteSpec(name), 687 remote = remote.ToRemoteSpec(name),
630 gitdir = gitdir, 688 gitdir = gitdir,
631 worktree = worktree, 689 worktree = worktree,
632 relpath = path, 690 relpath = relpath,
633 revisionExpr = revisionExpr, 691 revisionExpr = revisionExpr,
634 revisionId = None, 692 revisionId = None,
635 rebase = rebase, 693 rebase = rebase,
636 groups = groups, 694 groups = groups,
637 sync_c = sync_c, 695 sync_c = sync_c,
638 upstream = upstream) 696 sync_s = sync_s,
697 upstream = upstream,
698 parent = parent)
639 699
640 for n in node.childNodes: 700 for n in node.childNodes:
641 if n.nodeName == 'copyfile': 701 if n.nodeName == 'copyfile':
642 self._ParseCopyFile(project, n) 702 self._ParseCopyFile(project, n)
643 if n.nodeName == 'annotation': 703 if n.nodeName == 'annotation':
644 self._ParseAnnotation(project, n) 704 self._ParseAnnotation(project, n)
705 if n.nodeName == 'project':
706 project.subprojects.append(self._ParseProject(n, parent = project))
645 707
646 return project 708 return project
647 709
710 def GetProjectPaths(self, name, path):
711 relpath = path
712 if self.IsMirror:
713 worktree = None
714 gitdir = os.path.join(self.topdir, '%s.git' % name)
715 else:
716 worktree = os.path.join(self.topdir, path).replace('\\', '/')
717 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path)
718 return relpath, worktree, gitdir
719
720 def GetSubprojectName(self, parent, submodule_path):
721 return os.path.join(parent.name, submodule_path)
722
723 def _JoinRelpath(self, parent_relpath, relpath):
724 return os.path.join(parent_relpath, relpath)
725
726 def _UnjoinRelpath(self, parent_relpath, relpath):
727 return os.path.relpath(relpath, parent_relpath)
728
729 def GetSubprojectPaths(self, parent, path):
730 relpath = self._JoinRelpath(parent.relpath, path)
731 gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path)
732 if self.IsMirror:
733 worktree = None
734 else:
735 worktree = os.path.join(parent.worktree, path).replace('\\', '/')
736 return relpath, worktree, gitdir
737
648 def _ParseCopyFile(self, project, node): 738 def _ParseCopyFile(self, project, node):
649 src = self._reqatt(node, 'src') 739 src = self._reqatt(node, 'src')
650 dest = self._reqatt(node, 'dest') 740 dest = self._reqatt(node, 'dest')
diff --git a/project.py b/project.py
index 08b27710..ba7898ed 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,
@@ -1571,6 +1746,9 @@ class Project(object):
1571 os.remove(tmpPath) 1746 os.remove(tmpPath)
1572 if 'http_proxy' in os.environ and 'darwin' == sys.platform: 1747 if 'http_proxy' in os.environ and 'darwin' == sys.platform:
1573 cmd += ['--proxy', os.environ['http_proxy']] 1748 cmd += ['--proxy', os.environ['http_proxy']]
1749 cookiefile = GitConfig.ForUser().GetString('http.cookiefile')
1750 if cookiefile:
1751 cmd += ['--cookie', cookiefile]
1574 cmd += [srcUrl] 1752 cmd += [srcUrl]
1575 1753
1576 if IsTrace(): 1754 if IsTrace():
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 5b3dca78..228a279a 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -51,7 +51,7 @@ from main import WrapperModule
51from project import Project 51from project import Project
52from project import RemoteSpec 52from project import RemoteSpec
53from command import Command, MirrorSafeCommand 53from command import Command, MirrorSafeCommand
54from error import RepoChangedException, GitError 54from error import RepoChangedException, GitError, ManifestParseError
55from project import SyncBuffer 55from project import SyncBuffer
56from progress import Progress 56from progress import Progress
57 57
@@ -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
@@ -145,7 +148,10 @@ later is required to fix a server side protocol bug.
145""" 148"""
146 149
147 def _Options(self, p, show_smart=True): 150 def _Options(self, p, show_smart=True):
148 self.jobs = self.manifest.default.sync_j 151 try:
152 self.jobs = self.manifest.default.sync_j
153 except ManifestParseError:
154 self.jobs = 1
149 155
150 p.add_option('-f', '--force-broken', 156 p.add_option('-f', '--force-broken',
151 dest='force_broken', action='store_true', 157 dest='force_broken', action='store_true',
@@ -180,6 +186,9 @@ later is required to fix a server side protocol bug.
180 p.add_option('-p', '--manifest-server-password', action='store', 186 p.add_option('-p', '--manifest-server-password', action='store',
181 dest='manifest_server_password', 187 dest='manifest_server_password',
182 help='password to authenticate with the manifest server') 188 help='password to authenticate with the manifest server')
189 p.add_option('--fetch-submodules',
190 dest='fetch_submodules', action='store_true',
191 help='fetch submodules from server')
183 if show_smart: 192 if show_smart:
184 p.add_option('-s', '--smart-sync', 193 p.add_option('-s', '--smart-sync',
185 dest='smart_sync', action='store_true', 194 dest='smart_sync', action='store_true',
@@ -559,7 +568,9 @@ later is required to fix a server side protocol bug.
559 self.manifest._Unload() 568 self.manifest._Unload()
560 if opt.jobs is None: 569 if opt.jobs is None:
561 self.jobs = self.manifest.default.sync_j 570 self.jobs = self.manifest.default.sync_j
562 all_projects = self.GetProjects(args, missing_ok=True) 571 all_projects = self.GetProjects(args,
572 missing_ok=True,
573 submodules_ok=opt.fetch_submodules)
563 574
564 self._fetch_times = _FetchTimes(self.manifest) 575 self._fetch_times = _FetchTimes(self.manifest)
565 if not opt.local_only: 576 if not opt.local_only:
@@ -570,12 +581,33 @@ later is required to fix a server side protocol bug.
570 to_fetch.extend(all_projects) 581 to_fetch.extend(all_projects)
571 to_fetch.sort(key=self._fetch_times.Get, reverse=True) 582 to_fetch.sort(key=self._fetch_times.Get, reverse=True)
572 583
573 self._Fetch(to_fetch, opt) 584 fetched = self._Fetch(to_fetch, opt)
574 _PostRepoFetch(rp, opt.no_repo_verify) 585 _PostRepoFetch(rp, opt.no_repo_verify)
575 if opt.network_only: 586 if opt.network_only:
576 # bail out now; the rest touches the working tree 587 # bail out now; the rest touches the working tree
577 return 588 return
578 589
590 # Iteratively fetch missing and/or nested unregistered submodules
591 previously_missing_set = set()
592 while True:
593 self.manifest._Unload()
594 all_projects = self.GetProjects(args,
595 missing_ok=True,
596 submodules_ok=opt.fetch_submodules)
597 missing = []
598 for project in all_projects:
599 if project.gitdir not in fetched:
600 missing.append(project)
601 if not missing:
602 break
603 # Stop us from non-stopped fetching actually-missing repos: If set of
604 # missing repos has not been changed from last fetch, we break.
605 missing_set = set(p.name for p in missing)
606 if previously_missing_set == missing_set:
607 break
608 previously_missing_set = missing_set
609 fetched.update(self._Fetch(missing, opt))
610
579 if self.manifest.IsMirror: 611 if self.manifest.IsMirror:
580 # bail out now, we have no working tree 612 # bail out now, we have no working tree
581 return 613 return