summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--command.py24
-rw-r--r--git_config.py39
-rw-r--r--gitc_utils.py158
-rwxr-xr-xmain.py15
-rw-r--r--manifest_xml.py44
-rw-r--r--project.py85
-rwxr-xr-xrepo45
-rw-r--r--subcmds/gitc_init.py73
-rw-r--r--subcmds/help.py13
-rw-r--r--subcmds/start.py42
-rw-r--r--subcmds/sync.py163
11 files changed, 560 insertions, 141 deletions
diff --git a/command.py b/command.py
index 38cacd3b..997acec0 100644
--- a/command.py
+++ b/command.py
@@ -106,13 +106,13 @@ class Command(object):
106 def _UpdatePathToProjectMap(self, project): 106 def _UpdatePathToProjectMap(self, project):
107 self._by_path[project.worktree] = project 107 self._by_path[project.worktree] = project
108 108
109 def _GetProjectByPath(self, path): 109 def _GetProjectByPath(self, manifest, path):
110 project = None 110 project = None
111 if os.path.exists(path): 111 if os.path.exists(path):
112 oldpath = None 112 oldpath = None
113 while path \ 113 while path \
114 and path != oldpath \ 114 and path != oldpath \
115 and path != self.manifest.topdir: 115 and path != manifest.topdir:
116 try: 116 try:
117 project = self._by_path[path] 117 project = self._by_path[path]
118 break 118 break
@@ -126,13 +126,16 @@ class Command(object):
126 pass 126 pass
127 return project 127 return project
128 128
129 def GetProjects(self, args, groups='', missing_ok=False, submodules_ok=False): 129 def GetProjects(self, args, manifest=None, groups='', missing_ok=False,
130 submodules_ok=False):
130 """A list of projects that match the arguments. 131 """A list of projects that match the arguments.
131 """ 132 """
132 all_projects_list = self.manifest.projects 133 if not manifest:
134 manifest = self.manifest
135 all_projects_list = manifest.projects
133 result = [] 136 result = []
134 137
135 mp = self.manifest.manifestProject 138 mp = manifest.manifestProject
136 139
137 if not groups: 140 if not groups:
138 groups = mp.config.GetString('manifest.groups') 141 groups = mp.config.GetString('manifest.groups')
@@ -155,11 +158,11 @@ class Command(object):
155 self._ResetPathToProjectMap(all_projects_list) 158 self._ResetPathToProjectMap(all_projects_list)
156 159
157 for arg in args: 160 for arg in args:
158 projects = self.manifest.GetProjectsWithName(arg) 161 projects = manifest.GetProjectsWithName(arg)
159 162
160 if not projects: 163 if not projects:
161 path = os.path.abspath(arg).replace('\\', '/') 164 path = os.path.abspath(arg).replace('\\', '/')
162 project = self._GetProjectByPath(path) 165 project = self._GetProjectByPath(manifest, path)
163 166
164 # If it's not a derived project, update path->project mapping and 167 # If it's not a derived project, update path->project mapping and
165 # search again, as arg might actually point to a derived subproject. 168 # search again, as arg might actually point to a derived subproject.
@@ -170,7 +173,7 @@ class Command(object):
170 self._UpdatePathToProjectMap(subproject) 173 self._UpdatePathToProjectMap(subproject)
171 search_again = True 174 search_again = True
172 if search_again: 175 if search_again:
173 project = self._GetProjectByPath(path) or project 176 project = self._GetProjectByPath(manifest, path) or project
174 177
175 if project: 178 if project:
176 projects = [project] 179 projects = [project]
@@ -227,3 +230,8 @@ class MirrorSafeCommand(object):
227 """Command permits itself to run within a mirror, 230 """Command permits itself to run within a mirror,
228 and does not require a working directory. 231 and does not require a working directory.
229 """ 232 """
233
234class RequiresGitcCommand(object):
235 """Command that requires GITC to be available, but does
236 not require the local client to be a GITC client.
237 """
diff --git a/git_config.py b/git_config.py
index 8ded7c25..0379181a 100644
--- a/git_config.py
+++ b/git_config.py
@@ -15,6 +15,8 @@
15 15
16from __future__ import print_function 16from __future__ import print_function
17 17
18import contextlib
19import errno
18import json 20import json
19import os 21import os
20import re 22import re
@@ -502,6 +504,43 @@ def GetSchemeFromUrl(url):
502 return m.group(1) 504 return m.group(1)
503 return None 505 return None
504 506
507@contextlib.contextmanager
508def GetUrlCookieFile(url, quiet):
509 if url.startswith('persistent-'):
510 try:
511 p = subprocess.Popen(
512 ['git-remote-persistent-https', '-print_config', url],
513 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
514 stderr=subprocess.PIPE)
515 try:
516 cookieprefix = 'http.cookiefile='
517 proxyprefix = 'http.proxy='
518 cookiefile = None
519 proxy = None
520 for line in p.stdout:
521 line = line.strip()
522 if line.startswith(cookieprefix):
523 cookiefile = line[len(cookieprefix):]
524 if line.startswith(proxyprefix):
525 proxy = line[len(proxyprefix):]
526 # Leave subprocess open, as cookie file may be transient.
527 if cookiefile or proxy:
528 yield cookiefile, proxy
529 return
530 finally:
531 p.stdin.close()
532 if p.wait():
533 err_msg = p.stderr.read()
534 if ' -print_config' in err_msg:
535 pass # Persistent proxy doesn't support -print_config.
536 elif not quiet:
537 print(err_msg, file=sys.stderr)
538 except OSError as e:
539 if e.errno == errno.ENOENT:
540 pass # No persistent proxy.
541 raise
542 yield GitConfig.ForUser().GetString('http.cookiefile'), None
543
505def _preconnect(url): 544def _preconnect(url):
506 m = URI_ALL.match(url) 545 m = URI_ALL.match(url)
507 if m: 546 if m:
diff --git a/gitc_utils.py b/gitc_utils.py
new file mode 100644
index 00000000..dd38f890
--- /dev/null
+++ b/gitc_utils.py
@@ -0,0 +1,158 @@
1#
2# Copyright (C) 2015 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
16from __future__ import print_function
17import os
18import platform
19import re
20import sys
21import time
22
23import git_command
24import git_config
25import wrapper
26
27GITC_FS_ROOT_DIR = '/gitc/manifest-rw/'
28NUM_BATCH_RETRIEVE_REVISIONID = 300
29
30def get_gitc_manifest_dir():
31 return wrapper.Wrapper().get_gitc_manifest_dir()
32
33def parse_clientdir(gitc_fs_path):
34 """Parse a path in the GITC FS and return its client name.
35
36 @param gitc_fs_path: A subdirectory path within the GITC_FS_ROOT_DIR.
37
38 @returns: The GITC client name
39 """
40 if (gitc_fs_path == GITC_FS_ROOT_DIR or
41 not gitc_fs_path.startswith(GITC_FS_ROOT_DIR)):
42 return None
43 return gitc_fs_path.split(GITC_FS_ROOT_DIR)[1].split('/')[0]
44
45def _set_project_revisions(projects):
46 """Sets the revisionExpr for a list of projects.
47
48 Because of the limit of open file descriptors allowed, length of projects
49 should not be overly large. Recommend calling this function multiple times
50 with each call not exceeding NUM_BATCH_RETRIEVE_REVISIONID projects.
51
52 @param projects: List of project objects to set the revionExpr for.
53 """
54 # Retrieve the commit id for each project based off of it's current
55 # revisionExpr and it is not already a commit id.
56 project_gitcmds = [(
57 project, git_command.GitCommand(None,
58 ['ls-remote',
59 project.remote.url,
60 project.revisionExpr],
61 capture_stdout=True, cwd='/tmp'))
62 for project in projects if not git_config.IsId(project.revisionExpr)]
63 for proj, gitcmd in project_gitcmds:
64 if gitcmd.Wait():
65 print('FATAL: Failed to retrieve revisionExpr for %s' % proj)
66 sys.exit(1)
67 proj.revisionExpr = gitcmd.stdout.split('\t')[0]
68
69def _manifest_groups(manifest):
70 """Returns the manifest group string that should be synced
71
72 This is the same logic used by Command.GetProjects(), which is used during
73 repo sync
74
75 @param manifest: The XmlManifest object
76 """
77 mp = manifest.manifestProject
78 groups = mp.config.GetString('manifest.groups')
79 if not groups:
80 groups = 'default,platform-' + platform.system().lower()
81 return groups
82
83def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
84 """Generate a manifest for shafsd to use for this GITC client.
85
86 @param gitc_manifest: Current gitc manifest, or None if there isn't one yet.
87 @param manifest: A GitcManifest object loaded with the current repo manifest.
88 @param paths: List of project paths we want to update.
89 """
90
91 print('Generating GITC Manifest by fetching revision SHAs for each '
92 'project.')
93 if paths is None:
94 paths = manifest.paths.keys()
95
96 groups = [x for x in re.split(r'[,\s]+', _manifest_groups(manifest)) if x]
97
98 # Convert the paths to projects, and filter them to the matched groups.
99 projects = [manifest.paths[p] for p in paths]
100 projects = [p for p in projects if p.MatchesGroups(groups)]
101
102 if gitc_manifest is not None:
103 for path, proj in manifest.paths.iteritems():
104 if not proj.MatchesGroups(groups):
105 continue
106
107 if not proj.upstream and not git_config.IsId(proj.revisionExpr):
108 proj.upstream = proj.revisionExpr
109
110 if not path in gitc_manifest.paths:
111 # Any new projects need their first revision, even if we weren't asked
112 # for them.
113 projects.append(proj)
114 elif not path in paths:
115 # And copy revisions from the previous manifest if we're not updating
116 # them now.
117 gitc_proj = gitc_manifest.paths[path]
118 if gitc_proj.old_revision:
119 proj.revisionExpr = None
120 proj.old_revision = gitc_proj.old_revision
121 else:
122 proj.revisionExpr = gitc_proj.revisionExpr
123
124 index = 0
125 while index < len(projects):
126 _set_project_revisions(
127 projects[index:(index+NUM_BATCH_RETRIEVE_REVISIONID)])
128 index += NUM_BATCH_RETRIEVE_REVISIONID
129
130 if gitc_manifest is not None:
131 for path, proj in gitc_manifest.paths.iteritems():
132 if proj.old_revision and path in paths:
133 # If we updated a project that has been started, keep the old-revision
134 # updated.
135 repo_proj = manifest.paths[path]
136 repo_proj.old_revision = repo_proj.revisionExpr
137 repo_proj.revisionExpr = None
138
139 # Convert URLs from relative to absolute.
140 for name, remote in manifest.remotes.iteritems():
141 remote.fetchUrl = remote.resolvedFetchUrl
142
143 # Save the manifest.
144 save_manifest(manifest)
145
146def save_manifest(manifest, client_dir=None):
147 """Save the manifest file in the client_dir.
148
149 @param client_dir: Client directory to save the manifest in.
150 @param manifest: Manifest object to save.
151 """
152 if not client_dir:
153 client_dir = manifest.gitc_client_dir
154 with open(os.path.join(client_dir, '.manifest'), 'w') as f:
155 manifest.Save(f, groups=_manifest_groups(manifest))
156 # TODO(sbasi/jorg): Come up with a solution to remove the sleep below.
157 # Give the GITC filesystem time to register the manifest changes.
158 time.sleep(3)
diff --git a/main.py b/main.py
index 6736abc9..a5979a87 100755
--- a/main.py
+++ b/main.py
@@ -42,6 +42,7 @@ from git_command import git, GitCommand
42from git_config import init_ssh, close_ssh 42from git_config import init_ssh, close_ssh
43from command import InteractiveCommand 43from command import InteractiveCommand
44from command import MirrorSafeCommand 44from command import MirrorSafeCommand
45from command import RequiresGitcCommand
45from subcmds.version import Version 46from subcmds.version import Version
46from editor import Editor 47from editor import Editor
47from error import DownloadError 48from error import DownloadError
@@ -51,7 +52,8 @@ from error import ManifestParseError
51from error import NoManifestException 52from error import NoManifestException
52from error import NoSuchProjectError 53from error import NoSuchProjectError
53from error import RepoChangedException 54from error import RepoChangedException
54from manifest_xml import XmlManifest 55import gitc_utils
56from manifest_xml import GitcManifest, XmlManifest
55from pager import RunPager 57from pager import RunPager
56from wrapper import WrapperPath, Wrapper 58from wrapper import WrapperPath, Wrapper
57 59
@@ -129,6 +131,12 @@ class _Repo(object):
129 131
130 cmd.repodir = self.repodir 132 cmd.repodir = self.repodir
131 cmd.manifest = XmlManifest(cmd.repodir) 133 cmd.manifest = XmlManifest(cmd.repodir)
134 cmd.gitc_manifest = None
135 gitc_client_name = gitc_utils.parse_clientdir(os.getcwd())
136 if gitc_client_name:
137 cmd.gitc_manifest = GitcManifest(cmd.repodir, gitc_client_name)
138 cmd.manifest.isGitcClient = True
139
132 Editor.globalConfig = cmd.manifest.globalConfig 140 Editor.globalConfig = cmd.manifest.globalConfig
133 141
134 if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror: 142 if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
@@ -136,6 +144,11 @@ class _Repo(object):
136 file=sys.stderr) 144 file=sys.stderr)
137 return 1 145 return 1
138 146
147 if isinstance(cmd, RequiresGitcCommand) and not gitc_utils.get_gitc_manifest_dir():
148 print("fatal: '%s' requires GITC to be available" % name,
149 file=sys.stderr)
150 return 1
151
139 try: 152 try:
140 copts, cargs = cmd.OptionParser.parse_args(argv) 153 copts, cargs = cmd.OptionParser.parse_args(argv)
141 copts = cmd.ReadEnvironmentOptions(copts) 154 copts = cmd.ReadEnvironmentOptions(copts)
diff --git a/manifest_xml.py b/manifest_xml.py
index 7e719600..3ac607ec 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -29,6 +29,7 @@ else:
29 urllib = imp.new_module('urllib') 29 urllib = imp.new_module('urllib')
30 urllib.parse = urlparse 30 urllib.parse = urlparse
31 31
32import gitc_utils
32from git_config import GitConfig 33from git_config import GitConfig
33from git_refs import R_HEADS, HEAD 34from git_refs import R_HEADS, HEAD
34from project import RemoteSpec, Project, MetaProject 35from project import RemoteSpec, Project, MetaProject
@@ -112,6 +113,7 @@ class XmlManifest(object):
112 self.manifestFile = os.path.join(self.repodir, MANIFEST_FILE_NAME) 113 self.manifestFile = os.path.join(self.repodir, MANIFEST_FILE_NAME)
113 self.globalConfig = GitConfig.ForUser() 114 self.globalConfig = GitConfig.ForUser()
114 self.localManifestWarning = False 115 self.localManifestWarning = False
116 self.isGitcClient = False
115 117
116 self.repoProject = MetaProject(self, 'repo', 118 self.repoProject = MetaProject(self, 'repo',
117 gitdir = os.path.join(repodir, 'repo/.git'), 119 gitdir = os.path.join(repodir, 'repo/.git'),
@@ -165,12 +167,13 @@ class XmlManifest(object):
165 def _ParseGroups(self, groups): 167 def _ParseGroups(self, groups):
166 return [x for x in re.split(r'[,\s]+', groups) if x] 168 return [x for x in re.split(r'[,\s]+', groups) if x]
167 169
168 def Save(self, fd, peg_rev=False, peg_rev_upstream=True): 170 def Save(self, fd, peg_rev=False, peg_rev_upstream=True, groups=None):
169 """Write the current manifest out to the given file descriptor. 171 """Write the current manifest out to the given file descriptor.
170 """ 172 """
171 mp = self.manifestProject 173 mp = self.manifestProject
172 174
173 groups = mp.config.GetString('manifest.groups') 175 if groups is None:
176 groups = mp.config.GetString('manifest.groups')
174 if groups: 177 if groups:
175 groups = self._ParseGroups(groups) 178 groups = self._ParseGroups(groups)
176 179
@@ -303,6 +306,11 @@ class XmlManifest(object):
303 if p.sync_s: 306 if p.sync_s:
304 e.setAttribute('sync-s', 'true') 307 e.setAttribute('sync-s', 'true')
305 308
309 if p.clone_depth:
310 e.setAttribute('clone-depth', str(p.clone_depth))
311
312 self._output_manifest_project_extras(p, e)
313
306 if p.subprojects: 314 if p.subprojects:
307 subprojects = set(subp.name for subp in p.subprojects) 315 subprojects = set(subp.name for subp in p.subprojects)
308 output_projects(p, e, list(sorted(subprojects))) 316 output_projects(p, e, list(sorted(subprojects)))
@@ -320,6 +328,10 @@ class XmlManifest(object):
320 328
321 doc.writexml(fd, '', ' ', '\n', 'UTF-8') 329 doc.writexml(fd, '', ' ', '\n', 'UTF-8')
322 330
331 def _output_manifest_project_extras(self, p, e):
332 """Manifests can modify e if they support extra project attributes."""
333 pass
334
323 @property 335 @property
324 def paths(self): 336 def paths(self):
325 self._Load() 337 self._Load()
@@ -709,7 +721,7 @@ class XmlManifest(object):
709 def _UnjoinName(self, parent_name, name): 721 def _UnjoinName(self, parent_name, name):
710 return os.path.relpath(name, parent_name) 722 return os.path.relpath(name, parent_name)
711 723
712 def _ParseProject(self, node, parent = None): 724 def _ParseProject(self, node, parent = None, **extra_proj_attrs):
713 """ 725 """
714 reads a <project> element from the manifest file 726 reads a <project> element from the manifest file
715 """ 727 """
@@ -804,7 +816,8 @@ class XmlManifest(object):
804 clone_depth = clone_depth, 816 clone_depth = clone_depth,
805 upstream = upstream, 817 upstream = upstream,
806 parent = parent, 818 parent = parent,
807 dest_branch = dest_branch) 819 dest_branch = dest_branch,
820 **extra_proj_attrs)
808 821
809 for n in node.childNodes: 822 for n in node.childNodes:
810 if n.nodeName == 'copyfile': 823 if n.nodeName == 'copyfile':
@@ -935,3 +948,26 @@ class XmlManifest(object):
935 diff['added'].append(toProjects[proj]) 948 diff['added'].append(toProjects[proj])
936 949
937 return diff 950 return diff
951
952
953class GitcManifest(XmlManifest):
954
955 def __init__(self, repodir, gitc_client_name):
956 """Initialize the GitcManifest object."""
957 super(GitcManifest, self).__init__(repodir)
958 self.isGitcClient = True
959 self.gitc_client_name = gitc_client_name
960 self.gitc_client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
961 gitc_client_name)
962 self.manifestFile = os.path.join(self.gitc_client_dir, '.manifest')
963
964 def _ParseProject(self, node, parent = None):
965 """Override _ParseProject and add support for GITC specific attributes."""
966 return super(GitcManifest, self)._ParseProject(
967 node, parent=parent, old_revision=node.getAttribute('old-revision'))
968
969 def _output_manifest_project_extras(self, p, e):
970 """Output GITC Specific Project attributes"""
971 if p.old_revision:
972 e.setAttribute('old-revision', str(p.old_revision))
973
diff --git a/project.py b/project.py
index a117f4df..5d8f61e1 100644
--- a/project.py
+++ b/project.py
@@ -13,7 +13,6 @@
13# limitations under the License. 13# limitations under the License.
14 14
15from __future__ import print_function 15from __future__ import print_function
16import contextlib
17import errno 16import errno
18import filecmp 17import filecmp
19import glob 18import glob
@@ -31,7 +30,7 @@ import traceback
31 30
32from color import Coloring 31from color import Coloring
33from git_command import GitCommand, git_require 32from git_command import GitCommand, git_require
34from git_config import GitConfig, IsId, GetSchemeFromUrl, ID_RE 33from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, ID_RE
35from error import GitError, HookError, UploadError, DownloadError 34from error import GitError, HookError, UploadError, DownloadError
36from error import ManifestInvalidRevisionError 35from error import ManifestInvalidRevisionError
37from error import NoManifestException 36from error import NoManifestException
@@ -64,6 +63,10 @@ def _error(fmt, *args):
64 msg = fmt % args 63 msg = fmt % args
65 print('error: %s' % msg, file=sys.stderr) 64 print('error: %s' % msg, file=sys.stderr)
66 65
66def _warn(fmt, *args):
67 msg = fmt % args
68 print('warn: %s' % msg, file=sys.stderr)
69
67def not_rev(r): 70def not_rev(r):
68 return '^' + r 71 return '^' + r
69 72
@@ -569,7 +572,8 @@ class Project(object):
569 parent=None, 572 parent=None,
570 is_derived=False, 573 is_derived=False,
571 dest_branch=None, 574 dest_branch=None,
572 optimized_fetch=False): 575 optimized_fetch=False,
576 old_revision=None):
573 """Init a Project object. 577 """Init a Project object.
574 578
575 Args: 579 Args:
@@ -593,6 +597,7 @@ class Project(object):
593 dest_branch: The branch to which to push changes for review by default. 597 dest_branch: The branch to which to push changes for review by default.
594 optimized_fetch: If True, when a project is set to a sha1 revision, only 598 optimized_fetch: If True, when a project is set to a sha1 revision, only
595 fetch from the remote if the sha1 is not present locally. 599 fetch from the remote if the sha1 is not present locally.
600 old_revision: saved git commit id for open GITC projects.
596 """ 601 """
597 self.manifest = manifest 602 self.manifest = manifest
598 self.name = name 603 self.name = name
@@ -640,6 +645,7 @@ class Project(object):
640 self.bare_ref = GitRefs(gitdir) 645 self.bare_ref = GitRefs(gitdir)
641 self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=objdir) 646 self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=objdir)
642 self.dest_branch = dest_branch 647 self.dest_branch = dest_branch
648 self.old_revision = old_revision
643 649
644 # This will be filled in if a project is later identified to be the 650 # This will be filled in if a project is later identified to be the
645 # project containing repo hooks. 651 # project containing repo hooks.
@@ -1093,8 +1099,7 @@ class Project(object):
1093 tar.extractall(path=path) 1099 tar.extractall(path=path)
1094 return True 1100 return True
1095 except (IOError, tarfile.TarError) as e: 1101 except (IOError, tarfile.TarError) as e:
1096 print("error: Cannot extract archive %s: " 1102 _error("Cannot extract archive %s: %s", tarpath, str(e))
1097 "%s" % (tarpath, str(e)), file=sys.stderr)
1098 return False 1103 return False
1099 1104
1100 def Sync_NetworkHalf(self, 1105 def Sync_NetworkHalf(self,
@@ -1111,8 +1116,7 @@ class Project(object):
1111 """ 1116 """
1112 if archive and not isinstance(self, MetaProject): 1117 if archive and not isinstance(self, MetaProject):
1113 if self.remote.url.startswith(('http://', 'https://')): 1118 if self.remote.url.startswith(('http://', 'https://')):
1114 print("error: %s: Cannot fetch archives from http/https " 1119 _error("%s: Cannot fetch archives from http/https remotes.", self.name)
1115 "remotes." % self.name, file=sys.stderr)
1116 return False 1120 return False
1117 1121
1118 name = self.relpath.replace('\\', '/') 1122 name = self.relpath.replace('\\', '/')
@@ -1123,7 +1127,7 @@ class Project(object):
1123 try: 1127 try:
1124 self._FetchArchive(tarpath, cwd=topdir) 1128 self._FetchArchive(tarpath, cwd=topdir)
1125 except GitError as e: 1129 except GitError as e:
1126 print('error: %s' % str(e), file=sys.stderr) 1130 _error('%s', e)
1127 return False 1131 return False
1128 1132
1129 # From now on, we only need absolute tarpath 1133 # From now on, we only need absolute tarpath
@@ -1134,8 +1138,7 @@ class Project(object):
1134 try: 1138 try:
1135 os.remove(tarpath) 1139 os.remove(tarpath)
1136 except OSError as e: 1140 except OSError as e:
1137 print("warn: Cannot remove archive %s: " 1141 _warn("Cannot remove archive %s: %s", tarpath, str(e))
1138 "%s" % (tarpath, str(e)), file=sys.stderr)
1139 self._CopyAndLinkFiles() 1142 self._CopyAndLinkFiles()
1140 return True 1143 return True
1141 if is_new is None: 1144 if is_new is None:
@@ -1195,6 +1198,8 @@ class Project(object):
1195 self._InitHooks() 1198 self._InitHooks()
1196 1199
1197 def _CopyAndLinkFiles(self): 1200 def _CopyAndLinkFiles(self):
1201 if self.manifest.isGitcClient:
1202 return
1198 for copyfile in self.copyfiles: 1203 for copyfile in self.copyfiles:
1199 copyfile._Copy() 1204 copyfile._Copy()
1200 for linkfile in self.linkfiles: 1205 for linkfile in self.linkfiles:
@@ -1270,6 +1275,8 @@ class Project(object):
1270 # Except if the head needs to be detached 1275 # Except if the head needs to be detached
1271 # 1276 #
1272 if not syncbuf.detach_head: 1277 if not syncbuf.detach_head:
1278 # The copy/linkfile config may have changed.
1279 self._CopyAndLinkFiles()
1273 return 1280 return
1274 else: 1281 else:
1275 lost = self._revlist(not_rev(revid), HEAD) 1282 lost = self._revlist(not_rev(revid), HEAD)
@@ -1287,6 +1294,8 @@ class Project(object):
1287 if head == revid: 1294 if head == revid:
1288 # No changes; don't do anything further. 1295 # No changes; don't do anything further.
1289 # 1296 #
1297 # The copy/linkfile config may have changed.
1298 self._CopyAndLinkFiles()
1290 return 1299 return
1291 1300
1292 branch = self.GetBranch(branch) 1301 branch = self.GetBranch(branch)
@@ -1425,9 +1434,11 @@ class Project(object):
1425 1434
1426## Branch Management ## 1435## Branch Management ##
1427 1436
1428 def StartBranch(self, name): 1437 def StartBranch(self, name, branch_merge=''):
1429 """Create a new branch off the manifest's revision. 1438 """Create a new branch off the manifest's revision.
1430 """ 1439 """
1440 if not branch_merge:
1441 branch_merge = self.revisionExpr
1431 head = self.work_git.GetHead() 1442 head = self.work_git.GetHead()
1432 if head == (R_HEADS + name): 1443 if head == (R_HEADS + name):
1433 return True 1444 return True
@@ -1441,9 +1452,9 @@ class Project(object):
1441 1452
1442 branch = self.GetBranch(name) 1453 branch = self.GetBranch(name)
1443 branch.remote = self.GetRemote(self.remote.name) 1454 branch.remote = self.GetRemote(self.remote.name)
1444 branch.merge = self.revisionExpr 1455 branch.merge = branch_merge
1445 if not branch.merge.startswith('refs/') and not ID_RE.match(self.revisionExpr): 1456 if not branch.merge.startswith('refs/') and not ID_RE.match(branch_merge):
1446 branch.merge = R_HEADS + self.revisionExpr 1457 branch.merge = R_HEADS + branch_merge
1447 revid = self.GetRevisionId(all_refs) 1458 revid = self.GetRevisionId(all_refs)
1448 1459
1449 if head.startswith(R_HEADS): 1460 if head.startswith(R_HEADS):
@@ -1451,7 +1462,6 @@ class Project(object):
1451 head = all_refs[head] 1462 head = all_refs[head]
1452 except KeyError: 1463 except KeyError:
1453 head = None 1464 head = None
1454
1455 if revid and head and revid == head: 1465 if revid and head and revid == head:
1456 ref = os.path.join(self.gitdir, R_HEADS + name) 1466 ref = os.path.join(self.gitdir, R_HEADS + name)
1457 try: 1467 try:
@@ -2030,7 +2040,7 @@ class Project(object):
2030 os.remove(tmpPath) 2040 os.remove(tmpPath)
2031 if 'http_proxy' in os.environ and 'darwin' == sys.platform: 2041 if 'http_proxy' in os.environ and 'darwin' == sys.platform:
2032 cmd += ['--proxy', os.environ['http_proxy']] 2042 cmd += ['--proxy', os.environ['http_proxy']]
2033 with self._GetBundleCookieFile(srcUrl, quiet) as cookiefile: 2043 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy):
2034 if cookiefile: 2044 if cookiefile:
2035 cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile] 2045 cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile]
2036 if srcUrl.startswith('persistent-'): 2046 if srcUrl.startswith('persistent-'):
@@ -2078,40 +2088,6 @@ class Project(object):
2078 except OSError: 2088 except OSError:
2079 return False 2089 return False
2080 2090
2081 @contextlib.contextmanager
2082 def _GetBundleCookieFile(self, url, quiet):
2083 if url.startswith('persistent-'):
2084 try:
2085 p = subprocess.Popen(
2086 ['git-remote-persistent-https', '-print_config', url],
2087 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2088 stderr=subprocess.PIPE)
2089 try:
2090 prefix = 'http.cookiefile='
2091 cookiefile = None
2092 for line in p.stdout:
2093 line = line.strip()
2094 if line.startswith(prefix):
2095 cookiefile = line[len(prefix):]
2096 break
2097 # Leave subprocess open, as cookie file may be transient.
2098 if cookiefile:
2099 yield cookiefile
2100 return
2101 finally:
2102 p.stdin.close()
2103 if p.wait():
2104 err_msg = p.stderr.read()
2105 if ' -print_config' in err_msg:
2106 pass # Persistent proxy doesn't support -print_config.
2107 elif not quiet:
2108 print(err_msg, file=sys.stderr)
2109 except OSError as e:
2110 if e.errno == errno.ENOENT:
2111 pass # No persistent proxy.
2112 raise
2113 yield GitConfig.ForUser().GetString('http.cookiefile')
2114
2115 def _Checkout(self, rev, quiet=False): 2091 def _Checkout(self, rev, quiet=False):
2116 cmd = ['checkout'] 2092 cmd = ['checkout']
2117 if quiet: 2093 if quiet:
@@ -2182,8 +2158,8 @@ class Project(object):
2182 try: 2158 try:
2183 self._CheckDirReference(self.objdir, self.gitdir, share_refs=False) 2159 self._CheckDirReference(self.objdir, self.gitdir, share_refs=False)
2184 except GitError as e: 2160 except GitError as e:
2185 print("Retrying clone after deleting %s" % force_sync, file=sys.stderr)
2186 if force_sync: 2161 if force_sync:
2162 print("Retrying clone after deleting %s" % self.gitdir, file=sys.stderr)
2187 try: 2163 try:
2188 shutil.rmtree(os.path.realpath(self.gitdir)) 2164 shutil.rmtree(os.path.realpath(self.gitdir))
2189 if self.worktree and os.path.exists( 2165 if self.worktree and os.path.exists(
@@ -2261,7 +2237,7 @@ class Project(object):
2261 if filecmp.cmp(stock_hook, dst, shallow=False): 2237 if filecmp.cmp(stock_hook, dst, shallow=False):
2262 os.remove(dst) 2238 os.remove(dst)
2263 else: 2239 else:
2264 _error("%s: Not replacing %s hook", self.relpath, name) 2240 _warn("%s: Not replacing locally modified %s hook", self.relpath, name)
2265 continue 2241 continue
2266 try: 2242 try:
2267 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst) 2243 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
@@ -2320,7 +2296,10 @@ class Project(object):
2320 # Fail if the links are pointing to the wrong place 2296 # Fail if the links are pointing to the wrong place
2321 if src != dst: 2297 if src != dst:
2322 raise GitError('--force-sync not enabled; cannot overwrite a local ' 2298 raise GitError('--force-sync not enabled; cannot overwrite a local '
2323 'work tree') 2299 'work tree. If you\'re comfortable with the '
2300 'possibility of losing the work tree\'s git metadata,'
2301 ' use `repo sync --force-sync {0}` to '
2302 'proceed.'.format(self.relpath))
2324 2303
2325 def _ReferenceGitDir(self, gitdir, dotgit, share_refs, copy_all): 2304 def _ReferenceGitDir(self, gitdir, dotgit, share_refs, copy_all):
2326 """Update |dotgit| to reference |gitdir|, using symlinks where possible. 2305 """Update |dotgit| to reference |gitdir|, using symlinks where possible.
diff --git a/repo b/repo
index bf8fa3dc..b7d8024f 100755
--- a/repo
+++ b/repo
@@ -108,7 +108,7 @@ S_repo = 'repo' # special repo repository
108S_manifests = 'manifests' # special manifest repository 108S_manifests = 'manifests' # special manifest repository
109REPO_MAIN = S_repo + '/main.py' # main script 109REPO_MAIN = S_repo + '/main.py' # main script
110MIN_PYTHON_VERSION = (2, 6) # minimum supported python version 110MIN_PYTHON_VERSION = (2, 6) # minimum supported python version
111GITC_MANIFEST_DIR = '/usr/local/google/gitc' 111GITC_CONFIG_FILE = '/gitc/.config'
112 112
113 113
114import errno 114import errno
@@ -214,6 +214,7 @@ group.add_option('--config-name',
214 help='Always prompt for name/e-mail') 214 help='Always prompt for name/e-mail')
215 215
216def _GitcInitOptions(init_optparse): 216def _GitcInitOptions(init_optparse):
217 init_optparse.set_usage("repo gitc-init -u url -c client [options]")
217 g = init_optparse.add_option_group('GITC options') 218 g = init_optparse.add_option_group('GITC options')
218 g.add_option('-f', '--manifest-file', 219 g.add_option('-f', '--manifest-file',
219 dest='manifest_file', 220 dest='manifest_file',
@@ -222,6 +223,21 @@ def _GitcInitOptions(init_optparse):
222 dest='gitc_client', 223 dest='gitc_client',
223 help='The name for the new gitc_client instance.') 224 help='The name for the new gitc_client instance.')
224 225
226_gitc_manifest_dir = None
227def get_gitc_manifest_dir():
228 global _gitc_manifest_dir
229 if _gitc_manifest_dir is None:
230 _gitc_manifest_dir = ''
231 try:
232 with open(GITC_CONFIG_FILE, 'r') as gitc_config:
233 for line in gitc_config:
234 match = re.match('gitc_dir=(?P<gitc_manifest_dir>.*)', line)
235 if match:
236 _gitc_manifest_dir = match.group('gitc_manifest_dir')
237 except IOError:
238 pass
239 return _gitc_manifest_dir
240
225class CloneFailure(Exception): 241class CloneFailure(Exception):
226 """Indicate the remote clone of repo itself failed. 242 """Indicate the remote clone of repo itself failed.
227 """ 243 """
@@ -255,7 +271,15 @@ def _Init(args, gitc_init=False):
255 271
256 try: 272 try:
257 if gitc_init: 273 if gitc_init:
258 client_dir = os.path.join(GITC_MANIFEST_DIR, opt.gitc_client) 274 gitc_manifest_dir = get_gitc_manifest_dir()
275 if not gitc_manifest_dir:
276 _print('fatal: GITC filesystem is not available. Exiting...',
277 file=sys.stderr)
278 sys.exit(1)
279 if not opt.gitc_client:
280 _print('fatal: GITC client (-c) is required.', file=sys.stderr)
281 sys.exit(1)
282 client_dir = os.path.join(gitc_manifest_dir, opt.gitc_client)
259 if not os.path.exists(client_dir): 283 if not os.path.exists(client_dir):
260 os.makedirs(client_dir) 284 os.makedirs(client_dir)
261 os.chdir(client_dir) 285 os.chdir(client_dir)
@@ -661,6 +685,10 @@ def _ParseArguments(args):
661 685
662 686
663def _Usage(): 687def _Usage():
688 gitc_usage = ""
689 if get_gitc_manifest_dir():
690 gitc_usage = " gitc-init Initialize a GITC Client.\n"
691
664 _print( 692 _print(
665"""usage: repo COMMAND [ARGS] 693"""usage: repo COMMAND [ARGS]
666 694
@@ -669,7 +697,8 @@ repo is not yet installed. Use "repo init" to install it here.
669The most commonly used repo commands are: 697The most commonly used repo commands are:
670 698
671 init Install repo in the current working directory 699 init Install repo in the current working directory
672 help Display detailed help on a command 700""" + gitc_usage +
701""" help Display detailed help on a command
673 702
674For access to the full online help, install repo ("repo init"). 703For access to the full online help, install repo ("repo init").
675""", file=sys.stderr) 704""", file=sys.stderr)
@@ -681,6 +710,10 @@ def _Help(args):
681 if args[0] == 'init': 710 if args[0] == 'init':
682 init_optparse.print_help() 711 init_optparse.print_help()
683 sys.exit(0) 712 sys.exit(0)
713 elif args[0] == 'gitc-init':
714 _GitcInitOptions(init_optparse)
715 init_optparse.print_help()
716 sys.exit(0)
684 else: 717 else:
685 _print("error: '%s' is not a bootstrap command.\n" 718 _print("error: '%s' is not a bootstrap command.\n"
686 ' For access to online help, install repo ("repo init").' 719 ' For access to online help, install repo ("repo init").'
@@ -746,6 +779,12 @@ def main(orig_args):
746 wrapper_path = os.path.abspath(__file__) 779 wrapper_path = os.path.abspath(__file__)
747 my_main, my_git = _RunSelf(wrapper_path) 780 my_main, my_git = _RunSelf(wrapper_path)
748 781
782 cwd = os.getcwd()
783 if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()):
784 _print('error: repo cannot be used in the GITC local manifest directory.'
785 '\nIf you want to work on this GITC client please rerun this '
786 'command from the corresponding client under /gitc/', file=sys.stderr)
787 sys.exit(1)
749 if not repo_main: 788 if not repo_main:
750 if opt.help: 789 if opt.help:
751 _Usage() 790 _Usage()
diff --git a/subcmds/gitc_init.py b/subcmds/gitc_init.py
index 9b9cefda..c243bb3b 100644
--- a/subcmds/gitc_init.py
+++ b/subcmds/gitc_init.py
@@ -15,19 +15,15 @@
15 15
16from __future__ import print_function 16from __future__ import print_function
17import os 17import os
18import shutil
19import sys 18import sys
20 19
21import git_command 20import gitc_utils
21from command import RequiresGitcCommand
22from manifest_xml import GitcManifest
22from subcmds import init 23from subcmds import init
23 24
24 25
25GITC_MANIFEST_DIR = '/usr/local/google/gitc' 26class GitcInit(init.Init, RequiresGitcCommand):
26GITC_FS_ROOT_DIR = '/gitc/sha/rw'
27NUM_BATCH_RETRIEVE_REVISIONID = 300
28
29
30class GitcInit(init.Init):
31 common = True 27 common = True
32 helpSummary = "Initialize a GITC Client." 28 helpSummary = "Initialize a GITC Client."
33 helpUsage = """ 29 helpUsage = """
@@ -39,7 +35,7 @@ with the GITC file system.
39 35
40This command will setup the client directory, initialize repo, just 36This command will setup the client directory, initialize repo, just
41like repo init does, and then downloads the manifest collection 37like repo init does, and then downloads the manifest collection
42and installs in in the .repo/directory of the GITC client. 38and installs it in the .repo/directory of the GITC client.
43 39
44Once this is done, a GITC manifest is generated by pulling the HEAD 40Once this is done, a GITC manifest is generated by pulling the HEAD
45SHA for each project and generates the properly formatted XML file 41SHA for each project and generates the properly formatted XML file
@@ -65,59 +61,24 @@ use for this GITC client.
65 if not opt.gitc_client: 61 if not opt.gitc_client:
66 print('fatal: gitc client (-c) is required', file=sys.stderr) 62 print('fatal: gitc client (-c) is required', file=sys.stderr)
67 sys.exit(1) 63 sys.exit(1)
68 self.client_dir = os.path.join(GITC_MANIFEST_DIR, opt.gitc_client) 64 self.client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
69 if not os.path.exists(GITC_MANIFEST_DIR): 65 opt.gitc_client)
70 os.makedirs(GITC_MANIFEST_DIR) 66 if not os.path.exists(gitc_utils.get_gitc_manifest_dir()):
67 os.makedirs(gitc_utils.get_gitc_manifest_dir())
71 if not os.path.exists(self.client_dir): 68 if not os.path.exists(self.client_dir):
72 os.mkdir(self.client_dir) 69 os.mkdir(self.client_dir)
73 super(GitcInit, self).Execute(opt, args) 70 super(GitcInit, self).Execute(opt, args)
71
72 manifest_file = self.manifest.manifestFile
74 if opt.manifest_file: 73 if opt.manifest_file:
75 if not os.path.exists(opt.manifest_file): 74 if not os.path.exists(opt.manifest_file):
76 print('fatal: Specified manifest file %s does not exist.' % 75 print('fatal: Specified manifest file %s does not exist.' %
77 opt.manifest_file) 76 opt.manifest_file)
78 sys.exit(1) 77 sys.exit(1)
79 shutil.copyfile(opt.manifest_file, 78 manifest_file = opt.manifest_file
80 os.path.join(self.client_dir, '.manifest'))
81 else:
82 self._GenerateGITCManifest()
83 print('Please run `cd %s` to view your GITC client.' %
84 os.path.join(GITC_FS_ROOT_DIR, opt.gitc_client))
85
86 def _SetProjectRevisions(self, projects, branch):
87 """Sets the revisionExpr for a list of projects.
88 79
89 Because of the limit of open file descriptors allowed, length of projects 80 manifest = GitcManifest(self.repodir, opt.gitc_client)
90 should not be overly large. Recommend calling this function multiple times 81 manifest.Override(manifest_file)
91 with each call not exceeding NUM_BATCH_RETRIEVE_REVISIONID projects. 82 gitc_utils.generate_gitc_manifest(None, manifest)
92 83 print('Please run `cd %s` to view your GITC client.' %
93 @param projects: List of project objects to set the revionExpr for. 84 os.path.join(gitc_utils.GITC_FS_ROOT_DIR, opt.gitc_client))
94 @param branch: The remote branch to retrieve the SHA from. If branch is
95 None, 'HEAD' is used.
96 """
97 project_gitcmds = [(
98 project, git_command.GitCommand(None,
99 ['ls-remote',
100 project.remote.url,
101 branch], capture_stdout=True))
102 for project in projects]
103 for proj, gitcmd in project_gitcmds:
104 if gitcmd.Wait():
105 print('FATAL: Failed to retrieve revisionID for %s' % project)
106 sys.exit(1)
107 proj.revisionExpr = gitcmd.stdout.split('\t')[0]
108
109 def _GenerateGITCManifest(self):
110 """Generate a manifest for shafsd to use for this GITC client."""
111 print('Generating GITC Manifest by fetching revision SHAs for each '
112 'project.')
113 manifest = self.manifest
114 project_gitcmd_dict = {}
115 index = 0
116 while index < len(manifest.projects):
117 self._SetProjectRevisions(
118 manifest.projects[index:(index+NUM_BATCH_RETRIEVE_REVISIONID)],
119 manifest.default.revisionExpr)
120 index += NUM_BATCH_RETRIEVE_REVISIONID
121 # Save the manifest.
122 with open(os.path.join(self.client_dir, '.manifest'), 'w') as f:
123 manifest.Save(f)
diff --git a/subcmds/help.py b/subcmds/help.py
index 4aa3f863..ae5b8f08 100644
--- a/subcmds/help.py
+++ b/subcmds/help.py
@@ -19,7 +19,8 @@ import sys
19from formatter import AbstractFormatter, DumbWriter 19from formatter import AbstractFormatter, DumbWriter
20 20
21from color import Coloring 21from color import Coloring
22from command import PagedCommand, MirrorSafeCommand 22from command import PagedCommand, MirrorSafeCommand, RequiresGitcCommand
23import gitc_utils
23 24
24class Help(PagedCommand, MirrorSafeCommand): 25class Help(PagedCommand, MirrorSafeCommand):
25 common = False 26 common = False
@@ -54,9 +55,17 @@ Displays detailed usage information about a command.
54 def _PrintCommonCommands(self): 55 def _PrintCommonCommands(self):
55 print('usage: repo COMMAND [ARGS]') 56 print('usage: repo COMMAND [ARGS]')
56 print('The most commonly used repo commands are:') 57 print('The most commonly used repo commands are:')
58
59 def gitc_supported(cmd):
60 if not isinstance(cmd, RequiresGitcCommand):
61 return True
62 if gitc_utils.get_gitc_manifest_dir():
63 return True
64 return False
65
57 commandNames = list(sorted([name 66 commandNames = list(sorted([name
58 for name, command in self.commands.items() 67 for name, command in self.commands.items()
59 if command.common])) 68 if command.common and gitc_supported(command)]))
60 69
61 maxlen = 0 70 maxlen = 0
62 for name in commandNames: 71 for name in commandNames:
diff --git a/subcmds/start.py b/subcmds/start.py
index 60ad41e0..940c3413 100644
--- a/subcmds/start.py
+++ b/subcmds/start.py
@@ -14,11 +14,15 @@
14# limitations under the License. 14# limitations under the License.
15 15
16from __future__ import print_function 16from __future__ import print_function
17import os
17import sys 18import sys
19
18from command import Command 20from command import Command
19from git_config import IsId 21from git_config import IsId
20from git_command import git 22from git_command import git
23import gitc_utils
21from progress import Progress 24from progress import Progress
25from project import SyncBuffer
22 26
23class Start(Command): 27class Start(Command):
24 common = True 28 common = True
@@ -53,20 +57,50 @@ revision specified in the manifest.
53 print("error: at least one project must be specified", file=sys.stderr) 57 print("error: at least one project must be specified", file=sys.stderr)
54 sys.exit(1) 58 sys.exit(1)
55 59
56 all_projects = self.GetProjects(projects) 60 if self.gitc_manifest:
61 all_projects = self.GetProjects(projects, manifest=self.gitc_manifest,
62 missing_ok=True)
63 for project in all_projects:
64 if project.old_revision:
65 project.already_synced = True
66 else:
67 project.already_synced = False
68 project.old_revision = project.revisionExpr
69 project.revisionExpr = None
70 # Save the GITC manifest.
71 gitc_utils.save_manifest(self.gitc_manifest)
57 72
73 all_projects = self.GetProjects(projects,
74 missing_ok=bool(self.gitc_manifest))
58 pm = Progress('Starting %s' % nb, len(all_projects)) 75 pm = Progress('Starting %s' % nb, len(all_projects))
59 for project in all_projects: 76 for project in all_projects:
60 pm.update() 77 pm.update()
78
79 if self.gitc_manifest:
80 gitc_project = self.gitc_manifest.paths[project.relpath]
81 # Sync projects that have not been opened.
82 if not gitc_project.already_synced:
83 proj_localdir = os.path.join(self.gitc_manifest.gitc_client_dir,
84 project.relpath)
85 project.worktree = proj_localdir
86 if not os.path.exists(proj_localdir):
87 os.makedirs(proj_localdir)
88 project.Sync_NetworkHalf()
89 sync_buf = SyncBuffer(self.manifest.manifestProject.config)
90 project.Sync_LocalHalf(sync_buf)
91 project.revisionId = gitc_project.old_revision
92
61 # If the current revision is a specific SHA1 then we can't push back 93 # If the current revision is a specific SHA1 then we can't push back
62 # to it; so substitute with dest_branch if defined, or with manifest 94 # to it; so substitute with dest_branch if defined, or with manifest
63 # default revision instead. 95 # default revision instead.
96 branch_merge = ''
64 if IsId(project.revisionExpr): 97 if IsId(project.revisionExpr):
65 if project.dest_branch: 98 if project.dest_branch:
66 project.revisionExpr = project.dest_branch 99 branch_merge = project.dest_branch
67 else: 100 else:
68 project.revisionExpr = self.manifest.default.revisionExpr 101 branch_merge = self.manifest.default.revisionExpr
69 if not project.StartBranch(nb): 102
103 if not project.StartBranch(nb, branch_merge=branch_merge):
70 err.append(project) 104 err.append(project)
71 pm.end() 105 pm.end()
72 106
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 43d450be..a99d7e74 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -23,18 +23,26 @@ import shutil
23import socket 23import socket
24import subprocess 24import subprocess
25import sys 25import sys
26import tempfile
26import time 27import time
27 28
28from pyversion import is_python3 29from pyversion import is_python3
29if is_python3(): 30if is_python3():
31 import http.cookiejar as cookielib
32 import urllib.error
30 import urllib.parse 33 import urllib.parse
34 import urllib.request
31 import xmlrpc.client 35 import xmlrpc.client
32else: 36else:
37 import cookielib
33 import imp 38 import imp
39 import urllib2
34 import urlparse 40 import urlparse
35 import xmlrpclib 41 import xmlrpclib
36 urllib = imp.new_module('urllib') 42 urllib = imp.new_module('urllib')
43 urllib.error = urllib2
37 urllib.parse = urlparse 44 urllib.parse = urlparse
45 urllib.request = urllib2
38 xmlrpc = imp.new_module('xmlrpc') 46 xmlrpc = imp.new_module('xmlrpc')
39 xmlrpc.client = xmlrpclib 47 xmlrpc.client = xmlrpclib
40 48
@@ -57,7 +65,9 @@ except ImportError:
57 multiprocessing = None 65 multiprocessing = None
58 66
59from git_command import GIT, git_require 67from git_command import GIT, git_require
68from git_config import GetUrlCookieFile
60from git_refs import R_HEADS, HEAD 69from git_refs import R_HEADS, HEAD
70import gitc_utils
61from project import Project 71from project import Project
62from project import RemoteSpec 72from project import RemoteSpec
63from command import Command, MirrorSafeCommand 73from command import Command, MirrorSafeCommand
@@ -65,6 +75,7 @@ from error import RepoChangedException, GitError, ManifestParseError
65from project import SyncBuffer 75from project import SyncBuffer
66from progress import Progress 76from progress import Progress
67from wrapper import Wrapper 77from wrapper import Wrapper
78from manifest_xml import GitcManifest
68 79
69_ONE_DAY_S = 24 * 60 * 60 80_ONE_DAY_S = 24 * 60 * 60
70 81
@@ -554,19 +565,18 @@ later is required to fix a server side protocol bug.
554 try: 565 try:
555 info = netrc.netrc() 566 info = netrc.netrc()
556 except IOError: 567 except IOError:
557 print('.netrc file does not exist or could not be opened', 568 # .netrc file does not exist or could not be opened
558 file=sys.stderr) 569 pass
559 else: 570 else:
560 try: 571 try:
561 parse_result = urllib.parse.urlparse(manifest_server) 572 parse_result = urllib.parse.urlparse(manifest_server)
562 if parse_result.hostname: 573 if parse_result.hostname:
563 username, _account, password = \ 574 auth = info.authenticators(parse_result.hostname)
564 info.authenticators(parse_result.hostname) 575 if auth:
565 except TypeError: 576 username, _account, password = auth
566 # TypeError is raised when the given hostname is not present 577 else:
567 # in the .netrc file. 578 print('No credentials found for %s in .netrc'
568 print('No credentials found for %s in .netrc' 579 % parse_result.hostname, file=sys.stderr)
569 % parse_result.hostname, file=sys.stderr)
570 except netrc.NetrcParseError as e: 580 except netrc.NetrcParseError as e:
571 print('Error parsing .netrc file: %s' % e, file=sys.stderr) 581 print('Error parsing .netrc file: %s' % e, file=sys.stderr)
572 582
@@ -575,8 +585,12 @@ later is required to fix a server side protocol bug.
575 (username, password), 585 (username, password),
576 1) 586 1)
577 587
588 transport = PersistentTransport(manifest_server)
589 if manifest_server.startswith('persistent-'):
590 manifest_server = manifest_server[len('persistent-'):]
591
578 try: 592 try:
579 server = xmlrpc.client.Server(manifest_server) 593 server = xmlrpc.client.Server(manifest_server, transport=transport)
580 if opt.smart_sync: 594 if opt.smart_sync:
581 p = self.manifest.manifestProject 595 p = self.manifest.manifestProject
582 b = p.GetBranch(p.CurrentBranch) 596 b = p.GetBranch(p.CurrentBranch)
@@ -656,6 +670,42 @@ later is required to fix a server side protocol bug.
656 self._ReloadManifest(manifest_name) 670 self._ReloadManifest(manifest_name)
657 if opt.jobs is None: 671 if opt.jobs is None:
658 self.jobs = self.manifest.default.sync_j 672 self.jobs = self.manifest.default.sync_j
673
674 if self.gitc_manifest:
675 gitc_manifest_projects = self.GetProjects(args,
676 missing_ok=True)
677 gitc_projects = []
678 opened_projects = []
679 for project in gitc_manifest_projects:
680 if project.relpath in self.gitc_manifest.paths and \
681 self.gitc_manifest.paths[project.relpath].old_revision:
682 opened_projects.append(project.relpath)
683 else:
684 gitc_projects.append(project.relpath)
685
686 if not args:
687 gitc_projects = None
688
689 if gitc_projects != [] and not opt.local_only:
690 print('Updating GITC client: %s' % self.gitc_manifest.gitc_client_name)
691 manifest = GitcManifest(self.repodir, self.gitc_manifest.gitc_client_name)
692 if manifest_name:
693 manifest.Override(manifest_name)
694 else:
695 manifest.Override(self.manifest.manifestFile)
696 gitc_utils.generate_gitc_manifest(self.gitc_manifest,
697 manifest,
698 gitc_projects)
699 print('GITC client successfully synced.')
700
701 # The opened projects need to be synced as normal, therefore we
702 # generate a new args list to represent the opened projects.
703 # TODO: make this more reliable -- if there's a project name/path overlap,
704 # this may choose the wrong project.
705 args = [os.path.relpath(self.manifest.paths[p].worktree, os.getcwd())
706 for p in opened_projects]
707 if not args:
708 return
659 all_projects = self.GetProjects(args, 709 all_projects = self.GetProjects(args,
660 missing_ok=True, 710 missing_ok=True,
661 submodules_ok=opt.fetch_submodules) 711 submodules_ok=opt.fetch_submodules)
@@ -850,3 +900,96 @@ class _FetchTimes(object):
850 os.remove(self._path) 900 os.remove(self._path)
851 except OSError: 901 except OSError:
852 pass 902 pass
903
904# This is a replacement for xmlrpc.client.Transport using urllib2
905# and supporting persistent-http[s]. It cannot change hosts from
906# request to request like the normal transport, the real url
907# is passed during initialization.
908class PersistentTransport(xmlrpc.client.Transport):
909 def __init__(self, orig_host):
910 self.orig_host = orig_host
911
912 def request(self, host, handler, request_body, verbose=False):
913 with GetUrlCookieFile(self.orig_host, not verbose) as (cookiefile, proxy):
914 # Python doesn't understand cookies with the #HttpOnly_ prefix
915 # Since we're only using them for HTTP, copy the file temporarily,
916 # stripping those prefixes away.
917 if cookiefile:
918 tmpcookiefile = tempfile.NamedTemporaryFile()
919 try:
920 with open(cookiefile) as f:
921 for line in f:
922 if line.startswith("#HttpOnly_"):
923 line = line[len("#HttpOnly_"):]
924 tmpcookiefile.write(line)
925 tmpcookiefile.flush()
926
927 cookiejar = cookielib.MozillaCookieJar(tmpcookiefile.name)
928 cookiejar.load()
929 finally:
930 tmpcookiefile.close()
931 else:
932 cookiejar = cookielib.CookieJar()
933
934 proxyhandler = urllib.request.ProxyHandler
935 if proxy:
936 proxyhandler = urllib.request.ProxyHandler({
937 "http": proxy,
938 "https": proxy })
939
940 opener = urllib.request.build_opener(
941 urllib.request.HTTPCookieProcessor(cookiejar),
942 proxyhandler)
943
944 url = urllib.parse.urljoin(self.orig_host, handler)
945 parse_results = urllib.parse.urlparse(url)
946
947 scheme = parse_results.scheme
948 if scheme == 'persistent-http':
949 scheme = 'http'
950 if scheme == 'persistent-https':
951 # If we're proxying through persistent-https, use http. The
952 # proxy itself will do the https.
953 if proxy:
954 scheme = 'http'
955 else:
956 scheme = 'https'
957
958 # Parse out any authentication information using the base class
959 host, extra_headers, _ = self.get_host_info(parse_results.netloc)
960
961 url = urllib.parse.urlunparse((
962 scheme,
963 host,
964 parse_results.path,
965 parse_results.params,
966 parse_results.query,
967 parse_results.fragment))
968
969 request = urllib.request.Request(url, request_body)
970 if extra_headers is not None:
971 for (name, header) in extra_headers:
972 request.add_header(name, header)
973 request.add_header('Content-Type', 'text/xml')
974 try:
975 response = opener.open(request)
976 except urllib.error.HTTPError as e:
977 if e.code == 501:
978 # We may have been redirected through a login process
979 # but our POST turned into a GET. Retry.
980 response = opener.open(request)
981 else:
982 raise
983
984 p, u = xmlrpc.client.getparser()
985 while 1:
986 data = response.read(1024)
987 if not data:
988 break
989 p.feed(data)
990 p.close()
991 return u.close()
992
993 def close(self):
994 pass
995