summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--SUBMITTING_PATCHES17
-rw-r--r--command.py38
-rw-r--r--docs/manifest-format.txt (renamed from docs/manifest_xml.txt)59
-rw-r--r--docs/manifest_submodule.txt136
-rw-r--r--error.py28
-rw-r--r--git_command.py63
-rw-r--r--git_config.py97
-rw-r--r--git_refs.py13
-rwxr-xr-xhooks/pre-auto-gc5
-rwxr-xr-xmain.py169
-rw-r--r--manifest.py59
-rw-r--r--manifest_loader.py34
-rw-r--r--manifest_submodule.py481
-rw-r--r--manifest_xml.py331
-rw-r--r--progress.py15
-rw-r--r--project.py814
-rwxr-xr-xrepo146
-rw-r--r--subcmds/abandon.py27
-rw-r--r--subcmds/checkout.py24
-rw-r--r--subcmds/cherry_pick.py114
-rw-r--r--subcmds/diff.py15
-rw-r--r--subcmds/download.py21
-rw-r--r--subcmds/forall.py7
-rw-r--r--subcmds/help.py2
-rw-r--r--subcmds/init.py173
-rw-r--r--subcmds/list.py48
-rw-r--r--subcmds/manifest.py81
-rw-r--r--subcmds/overview.py80
-rw-r--r--subcmds/rebase.py23
-rw-r--r--subcmds/start.py5
-rw-r--r--subcmds/status.py86
-rw-r--r--subcmds/sync.py200
-rw-r--r--subcmds/upload.py96
-rw-r--r--subcmds/version.py8
34 files changed, 2171 insertions, 1344 deletions
diff --git a/SUBMITTING_PATCHES b/SUBMITTING_PATCHES
index f68906cc..63b0e571 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES
@@ -5,7 +5,7 @@ Short Version:
5 - Make sure all code is under the Apache License, 2.0. 5 - Make sure all code is under the Apache License, 2.0.
6 - Publish your changes for review: 6 - Publish your changes for review:
7 7
8 git push ssh://review.source.android.com:29418/tools/repo.git HEAD:refs/for/master 8 git push https://gerrit-review.googlesource.com/git-repo HEAD:refs/for/maint
9 9
10 10
11Long Version: 11Long Version:
@@ -55,24 +55,23 @@ Do not email your patches to anyone.
55 55
56Instead, login to the Gerrit Code Review tool at: 56Instead, login to the Gerrit Code Review tool at:
57 57
58 https://review.source.android.com/ 58 https://gerrit-review.googlesource.com/
59 59
60Ensure you have completed one of the necessary contributor 60Ensure you have completed one of the necessary contributor
61agreements, providing documentation to the project maintainers that 61agreements, providing documentation to the project maintainers that
62they have right to redistribute your work under the Apache License: 62they have right to redistribute your work under the Apache License:
63 63
64 https://review.source.android.com/#settings,agreements 64 https://gerrit-review.googlesource.com/#/settings/agreements
65 65
66Ensure you have registered one or more SSH public keys, so you can 66Ensure you have obtained an HTTP password to authenticate:
67push your commits directly over SSH:
68 67
69 https://review.source.android.com/#settings,ssh-keys 68 https://gerrit-review.googlesource.com/new-password
70 69
71Push your patches over SSH to the review server, possibly through 70Push your patches over HTTPS to the review server, possibly through
72a remembered remote to make this easier in the future: 71a remembered remote to make this easier in the future:
73 72
74 git config remote.review.url ssh://review.source.android.com:29418/tools/repo.git 73 git config remote.review.url https://gerrit-review.googlesource.com/git-repo
75 git config remote.review.push HEAD:refs/for/master 74 git config remote.review.push HEAD:refs/for/maint
76 75
77 git push review 76 git push review
78 77
diff --git a/command.py b/command.py
index 4e0253fc..4dbe2e77 100644
--- a/command.py
+++ b/command.py
@@ -15,17 +15,19 @@
15 15
16import os 16import os
17import optparse 17import optparse
18import platform
19import re
18import sys 20import sys
19 21
20import manifest_loader
21
22from error import NoSuchProjectError 22from error import NoSuchProjectError
23from error import InvalidProjectGroupsError
23 24
24class Command(object): 25class Command(object):
25 """Base class for any command line action in repo. 26 """Base class for any command line action in repo.
26 """ 27 """
27 28
28 common = False 29 common = False
30 manifest = None
29 _optparse = None 31 _optparse = None
30 32
31 def WantPager(self, opt): 33 def WantPager(self, opt):
@@ -57,31 +59,24 @@ class Command(object):
57 """Perform the action, after option parsing is complete. 59 """Perform the action, after option parsing is complete.
58 """ 60 """
59 raise NotImplementedError 61 raise NotImplementedError
60
61 @property
62 def manifest(self):
63 return self.GetManifest()
64
65 def GetManifest(self, reparse=False, type=None):
66 return manifest_loader.GetManifest(self.repodir,
67 reparse=reparse,
68 type=type)
69 62
70 def GetProjects(self, args, missing_ok=False): 63 def GetProjects(self, args, missing_ok=False):
71 """A list of projects that match the arguments. 64 """A list of projects that match the arguments.
72 """ 65 """
73 all = self.manifest.projects 66 all = self.manifest.projects
67 result = []
74 68
75 mp = self.manifest.manifestProject 69 mp = self.manifest.manifestProject
76 if mp.relpath == '.':
77 all = dict(all)
78 all[mp.name] = mp
79 70
80 result = [] 71 groups = mp.config.GetString('manifest.groups')
72 if not groups:
73 groups = 'default,platform-' + platform.system().lower()
74 groups = [x for x in re.split('[,\s]+', groups) if x]
81 75
82 if not args: 76 if not args:
83 for project in all.values(): 77 for project in all.values():
84 if missing_ok or project.Exists: 78 if ((missing_ok or project.Exists) and
79 project.MatchesGroups(groups)):
85 result.append(project) 80 result.append(project)
86 else: 81 else:
87 by_path = None 82 by_path = None
@@ -97,9 +92,7 @@ class Command(object):
97 for p in all.values(): 92 for p in all.values():
98 by_path[p.worktree] = p 93 by_path[p.worktree] = p
99 94
100 try: 95 if os.path.exists(path):
101 project = by_path[path]
102 except KeyError:
103 oldpath = None 96 oldpath = None
104 while path \ 97 while path \
105 and path != oldpath \ 98 and path != oldpath \
@@ -110,11 +103,18 @@ class Command(object):
110 except KeyError: 103 except KeyError:
111 oldpath = path 104 oldpath = path
112 path = os.path.dirname(path) 105 path = os.path.dirname(path)
106 else:
107 try:
108 project = by_path[path]
109 except KeyError:
110 pass
113 111
114 if not project: 112 if not project:
115 raise NoSuchProjectError(arg) 113 raise NoSuchProjectError(arg)
116 if not missing_ok and not project.Exists: 114 if not missing_ok and not project.Exists:
117 raise NoSuchProjectError(arg) 115 raise NoSuchProjectError(arg)
116 if not project.MatchesGroups(groups):
117 raise InvalidProjectGroupsError(arg)
118 118
119 result.append(project) 119 result.append(project)
120 120
diff --git a/docs/manifest_xml.txt b/docs/manifest-format.txt
index 37fbd5cd..38868f10 100644
--- a/docs/manifest_xml.txt
+++ b/docs/manifest-format.txt
@@ -25,30 +25,48 @@ following DTD:
25 default?, 25 default?,
26 manifest-server?, 26 manifest-server?,
27 remove-project*, 27 remove-project*,
28 project*)> 28 project*,
29 repo-hooks?)>
29 30
30 <!ELEMENT notice (#PCDATA)> 31 <!ELEMENT notice (#PCDATA)>
31 32
32 <!ELEMENT remote (EMPTY)> 33 <!ELEMENT remote (EMPTY)>
33 <!ATTLIST remote name ID #REQUIRED> 34 <!ATTLIST remote name ID #REQUIRED>
35 <!ATTLIST remote alias CDATA #IMPLIED>
34 <!ATTLIST remote fetch CDATA #REQUIRED> 36 <!ATTLIST remote fetch CDATA #REQUIRED>
35 <!ATTLIST remote review CDATA #IMPLIED> 37 <!ATTLIST remote review CDATA #IMPLIED>
36 38
37 <!ELEMENT default (EMPTY)> 39 <!ELEMENT default (EMPTY)>
38 <!ATTLIST default remote IDREF #IMPLIED> 40 <!ATTLIST default remote IDREF #IMPLIED>
39 <!ATTLIST default revision CDATA #IMPLIED> 41 <!ATTLIST default revision CDATA #IMPLIED>
40 42 <!ATTLIST default sync-j CDATA #IMPLIED>
43 <!ATTLIST default sync-c CDATA #IMPLIED>
44
41 <!ELEMENT manifest-server (EMPTY)> 45 <!ELEMENT manifest-server (EMPTY)>
42 <!ATTLIST url CDATA #REQUIRED> 46 <!ATTLIST url CDATA #REQUIRED>
43 47
44 <!ELEMENT project (EMPTY)> 48 <!ELEMENT project (annotation?)>
45 <!ATTLIST project name CDATA #REQUIRED> 49 <!ATTLIST project name CDATA #REQUIRED>
46 <!ATTLIST project path CDATA #IMPLIED> 50 <!ATTLIST project path CDATA #IMPLIED>
47 <!ATTLIST project remote IDREF #IMPLIED> 51 <!ATTLIST project remote IDREF #IMPLIED>
48 <!ATTLIST project revision CDATA #IMPLIED> 52 <!ATTLIST project revision CDATA #IMPLIED>
53 <!ATTLIST project groups CDATA #IMPLIED>
54 <!ATTLIST project sync-c CDATA #IMPLIED>
55
56 <!ELEMENT annotation (EMPTY)>
57 <!ATTLIST annotation name CDATA #REQUIRED>
58 <!ATTLIST annotation value CDATA #REQUIRED>
59 <!ATTLIST annotation keep CDATA "true">
49 60
50 <!ELEMENT remove-project (EMPTY)> 61 <!ELEMENT remove-project (EMPTY)>
51 <!ATTLIST remove-project name CDATA #REQUIRED> 62 <!ATTLIST remove-project name CDATA #REQUIRED>
63
64 <!ELEMENT repo-hooks (EMPTY)>
65 <!ATTLIST repo-hooks in-project CDATA #REQUIRED>
66 <!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
67
68 <!ELEMENT include (EMPTY)>
69 <!ATTLIST include name CDATA #REQUIRED>
52 ]> 70 ]>
53 71
54A description of the elements and their attributes follows. 72A description of the elements and their attributes follows.
@@ -72,6 +90,12 @@ name specified here is used as the remote name in each project's
72.git/config, and is therefore automatically available to commands 90.git/config, and is therefore automatically available to commands
73like `git fetch`, `git remote`, `git pull` and `git push`. 91like `git fetch`, `git remote`, `git pull` and `git push`.
74 92
93Attribute `alias`: The alias, if specified, is used to override
94`name` to be set as the remote name in each project's .git/config.
95Its value can be duplicated while attribute `name` has to be unique
96in the manifest file. This helps each project to be able to have
97same remote name which actually points to different remote url.
98
75Attribute `fetch`: The Git URL prefix for all projects which use 99Attribute `fetch`: The Git URL prefix for all projects which use
76this remote. Each project's name is appended to this prefix to 100this remote. Each project's name is appended to this prefix to
77form the actual URL used to clone the project. 101form the actual URL used to clone the project.
@@ -152,6 +176,25 @@ Tags and/or explicit SHA-1s should work in theory, but have not
152been extensively tested. If not supplied the revision given by 176been extensively tested. If not supplied the revision given by
153the default element is used. 177the default element is used.
154 178
179Attribute `groups`: List of groups to which this project belongs,
180whitespace or comma separated. All projects belong to the group
181"default", and each project automatically belongs to a group of
182it's name:`name` and path:`path`. E.g. for
183<project name="monkeys" path="barrel-of"/>, that project
184definition is implicitly in the following manifest groups:
185default, name:monkeys, and path:barrel-of.
186
187Element annotation
188------------------
189
190Zero or more annotation elements may be specified as children of a
191project element. Each element describes a name-value pair that will be
192exported into each project's environment during a 'forall' command,
193prefixed with REPO__. In addition, there is an optional attribute
194"keep" which accepts the case insensitive values "true" (default) or
195"false". This attribute determines whether or not the annotation will
196be kept when exported with the manifest subcommand.
197
155Element remove-project 198Element remove-project
156---------------------- 199----------------------
157 200
@@ -163,6 +206,16 @@ This element is mostly useful in the local_manifest.xml, where
163the user can remove a project, and possibly replace it with their 206the user can remove a project, and possibly replace it with their
164own definition. 207own definition.
165 208
209Element include
210---------------
211
212This element provides the capability of including another manifest
213file into the originating manifest. Normal rules apply for the
214target manifest to include- it must be a usable manifest on it's own.
215
216Attribute `name`; the manifest to include, specified relative to
217the manifest repositories root.
218
166 219
167Local Manifest 220Local Manifest
168============== 221==============
diff --git a/docs/manifest_submodule.txt b/docs/manifest_submodule.txt
deleted file mode 100644
index 1718284b..00000000
--- a/docs/manifest_submodule.txt
+++ /dev/null
@@ -1,136 +0,0 @@
1repo Manifest Format (submodule)
2================================
3
4A repo manifest describes the structure of a repo client; that is
5the directories that are visible and where they should be obtained
6from with git.
7
8The basic structure of a manifest is a bare Git repository holding
9a 'gitmodules' file in the top level directory, and one or more
10gitlink references pointing at commits from the referenced projects.
11This is the same structure as used by 'git submodule'.
12
13Manifests are inherently version controlled, since they are kept
14within a Git repository. Updates to manifests are automatically
15obtained by clients during `repo sync`.
16
17.gitmodules
18===========
19
20The '.gitmodules' file, located in the top-level directory of the
21client's working tree (or manifest repository), is a text file with
22a syntax matching the requirements of 'git config'.
23
24This file contains one subsection per project (also called a
25submodule by git), and the subsection value is a unique name to
26describe the project. Each submodule section must contain the
27following required keys:
28
29 * path
30 * url
31
32submodule.<name>.path
33---------------------
34
35Defines the path, relative to the top-level directory of the client's
36working tree, where the project is expected to be checked out. The
37path name must not end with a '/'. All paths must be unique within
38the .gitmodules file.
39
40At the specified path within the manifest repository a gitlink
41tree entry (an entry with file mode 160000) must exist referencing
42a commit SHA-1 from the project. This tree entry specifies the
43exact version of the project that `repo sync` will synchronize the
44client's working tree to.
45
46submodule.<name>.url
47--------------------
48
49Defines a URL from where the project repository can be cloned.
50By default `repo sync` will clone from this URL whenever a user
51needs to access this project.
52
53submodule.<name>.revision
54-------------------------
55
56Name of the branch in the project repository that Gerrit Code Review
57should automatically refresh the project's gitlink entry from.
58
59If set, during submit of a change within the referenced project,
60Gerrit Code Review will automatically update the manifest
61repository's corresponding gitlink to the new commit SHA-1 of
62this branch.
63
64Valid values are a short branch name (e.g. 'master'), a full ref
65name (e.g. 'refs/heads/master'), or '.' to request using the same
66branch name as the manifest branch itself. Since '.' automatically
67uses the manifest branch, '.' is the recommended value.
68
69If this key is not set, Gerrit Code Review will NOT automatically
70update the gitlink. An unset key requires the manifest maintainer
71to manually update the gitlink when it is necessary to reference
72a different revision of the project.
73
74submodule.<name>.update
75-----------------------
76
77This key is not supported by repo. If set, it will be ignored.
78
79repo.notice
80-----------
81
82A message displayed when repo sync uses this manifest.
83
84
85.review
86=======
87
88The optional '.review' file, located in the top-level directory of
89the client's working tree (or manifest repository), is a text file
90with a syntax matching the requirements of 'git config'.
91
92This file describes how `repo upload` should interact with the
93project's preferred code review system.
94
95review.url
96----------
97
98URL of the default Gerrit Code Review server. If a project does
99not have a specific URL in the '.review' file, this default URL
100will be used instead.
101
102review.<name>.url
103-----------------
104
105Project specific URL of the Gerrit Code Review server, for the
106submodule whose project name is <name>.
107
108Example
109=======
110
111 $ cat .gitmodules
112 [submodule "app/Clock"]
113 path = clock
114 url = git://vcs.example.com/ClockWidget.git
115 revision = .
116 [submodule "app/Browser"]
117 path = net/browser
118 url = git://netgroup.example.com/network/web/Browser.git
119 revision = .
120
121 $ cat .review
122 [review]
123 url = vcs-gerrit.example.com
124 [review "app/Browser"]
125 url = netgroup.example.com
126
127In the above example, the app/Clock project will send its code
128reviews to the default server, vcs-gerrit.example.com, while
129app/Browser will send its code reviews to netgroup.example.com.
130
131See Also
132========
133
134 * http://www.kernel.org/pub/software/scm/git/docs/gitmodules.html
135 * http://www.kernel.org/pub/software/scm/git/docs/git-config.html
136 * http://code.google.com/p/gerrit/
diff --git a/error.py b/error.py
index cb3b7258..78c5c0e0 100644
--- a/error.py
+++ b/error.py
@@ -57,6 +57,15 @@ class UploadError(Exception):
57 def __str__(self): 57 def __str__(self):
58 return self.reason 58 return self.reason
59 59
60class DownloadError(Exception):
61 """Cannot download a repository.
62 """
63 def __init__(self, reason):
64 self.reason = reason
65
66 def __str__(self):
67 return self.reason
68
60class NoSuchProjectError(Exception): 69class NoSuchProjectError(Exception):
61 """A specified project does not exist in the work tree. 70 """A specified project does not exist in the work tree.
62 """ 71 """
@@ -68,6 +77,18 @@ class NoSuchProjectError(Exception):
68 return 'in current directory' 77 return 'in current directory'
69 return self.name 78 return self.name
70 79
80
81class InvalidProjectGroupsError(Exception):
82 """A specified project is not suitable for the specified groups
83 """
84 def __init__(self, name=None):
85 self.name = name
86
87 def __str__(self):
88 if self.Name is None:
89 return 'in current directory'
90 return self.name
91
71class RepoChangedException(Exception): 92class RepoChangedException(Exception):
72 """Thrown if 'repo sync' results in repo updating its internal 93 """Thrown if 'repo sync' results in repo updating its internal
73 repo or manifest repositories. In this special case we must 94 repo or manifest repositories. In this special case we must
@@ -75,3 +96,10 @@ class RepoChangedException(Exception):
75 """ 96 """
76 def __init__(self, extra_args=[]): 97 def __init__(self, extra_args=[]):
77 self.extra_args = extra_args 98 self.extra_args = extra_args
99
100class HookError(Exception):
101 """Thrown if a 'repo-hook' could not be run.
102
103 The common case is that the file wasn't present when we tried to run it.
104 """
105 pass
diff --git a/git_command.py b/git_command.py
index 513b9ebf..5988cc28 100644
--- a/git_command.py
+++ b/git_command.py
@@ -72,6 +72,8 @@ def terminate_ssh_clients():
72 pass 72 pass
73 _ssh_clients = [] 73 _ssh_clients = []
74 74
75_git_version = None
76
75class _GitCall(object): 77class _GitCall(object):
76 def version(self): 78 def version(self):
77 p = GitCommand(None, ['--version'], capture_stdout=True) 79 p = GitCommand(None, ['--version'], capture_stdout=True)
@@ -79,6 +81,21 @@ class _GitCall(object):
79 return p.stdout 81 return p.stdout
80 return None 82 return None
81 83
84 def version_tuple(self):
85 global _git_version
86
87 if _git_version is None:
88 ver_str = git.version()
89 if ver_str.startswith('git version '):
90 _git_version = tuple(
91 map(lambda x: int(x),
92 ver_str[len('git version '):].strip().split('-')[0].split('.')[0:3]
93 ))
94 else:
95 print >>sys.stderr, 'fatal: "%s" unsupported' % ver_str
96 sys.exit(1)
97 return _git_version
98
82 def __getattr__(self, name): 99 def __getattr__(self, name):
83 name = name.replace('_','-') 100 name = name.replace('_','-')
84 def fun(*cmdv): 101 def fun(*cmdv):
@@ -88,23 +105,9 @@ class _GitCall(object):
88 return fun 105 return fun
89git = _GitCall() 106git = _GitCall()
90 107
91_git_version = None
92
93def git_require(min_version, fail=False): 108def git_require(min_version, fail=False):
94 global _git_version 109 git_version = git.version_tuple()
95 110 if min_version <= git_version:
96 if _git_version is None:
97 ver_str = git.version()
98 if ver_str.startswith('git version '):
99 _git_version = tuple(
100 map(lambda x: int(x),
101 ver_str[len('git version '):].strip().split('.')[0:3]
102 ))
103 else:
104 print >>sys.stderr, 'fatal: "%s" unsupported' % ver_str
105 sys.exit(1)
106
107 if min_version <= _git_version:
108 return True 111 return True
109 if fail: 112 if fail:
110 need = '.'.join(map(lambda x: str(x), min_version)) 113 need = '.'.join(map(lambda x: str(x), min_version))
@@ -144,6 +147,12 @@ class GitCommand(object):
144 if ssh_proxy: 147 if ssh_proxy:
145 _setenv(env, 'REPO_SSH_SOCK', ssh_sock()) 148 _setenv(env, 'REPO_SSH_SOCK', ssh_sock())
146 _setenv(env, 'GIT_SSH', _ssh_proxy()) 149 _setenv(env, 'GIT_SSH', _ssh_proxy())
150 if 'http_proxy' in env and 'darwin' == sys.platform:
151 s = "'http.proxy=%s'" % (env['http_proxy'],)
152 p = env.get('GIT_CONFIG_PARAMETERS')
153 if p is not None:
154 s = p + ' ' + s
155 _setenv(env, 'GIT_CONFIG_PARAMETERS', s)
147 156
148 if project: 157 if project:
149 if not cwd: 158 if not cwd:
@@ -218,26 +227,10 @@ class GitCommand(object):
218 self.stdin = p.stdin 227 self.stdin = p.stdin
219 228
220 def Wait(self): 229 def Wait(self):
221 p = self.process
222
223 if p.stdin:
224 p.stdin.close()
225 self.stdin = None
226
227 if p.stdout:
228 self.stdout = p.stdout.read()
229 p.stdout.close()
230 else:
231 p.stdout = None
232
233 if p.stderr:
234 self.stderr = p.stderr.read()
235 p.stderr.close()
236 else:
237 p.stderr = None
238
239 try: 230 try:
240 rc = p.wait() 231 p = self.process
232 (self.stdout, self.stderr) = p.communicate()
233 rc = p.returncode
241 finally: 234 finally:
242 _remove_ssh_client(p) 235 _remove_ssh_client(p)
243 return rc 236 return rc
diff --git a/git_config.py b/git_config.py
index ff815e35..eb532d02 100644
--- a/git_config.py
+++ b/git_config.py
@@ -26,7 +26,6 @@ import time
26import urllib2 26import urllib2
27 27
28from signal import SIGTERM 28from signal import SIGTERM
29from urllib2 import urlopen, HTTPError
30from error import GitError, UploadError 29from error import GitError, UploadError
31from trace import Trace 30from trace import Trace
32 31
@@ -80,14 +79,6 @@ class GitConfig(object):
80 else: 79 else:
81 self._pickle = pickleFile 80 self._pickle = pickleFile
82 81
83 def ClearCache(self):
84 if os.path.exists(self._pickle):
85 os.remove(self._pickle)
86 self._cache_dict = None
87 self._section_dict = None
88 self._remotes = {}
89 self._branches = {}
90
91 def Has(self, name, include_defaults = True): 82 def Has(self, name, include_defaults = True):
92 """Return true if this configuration file has the key. 83 """Return true if this configuration file has the key.
93 """ 84 """
@@ -206,6 +197,15 @@ class GitConfig(object):
206 except KeyError: 197 except KeyError:
207 return False 198 return False
208 199
200 def UrlInsteadOf(self, url):
201 """Resolve any url.*.insteadof references.
202 """
203 for new_url in self.GetSubSections('url'):
204 old_url = self.GetString('url.%s.insteadof' % new_url)
205 if old_url is not None and url.startswith(old_url):
206 return new_url + url[len(old_url):]
207 return url
208
209 @property 209 @property
210 def _sections(self): 210 def _sections(self):
211 d = self._section_dict 211 d = self._section_dict
@@ -488,7 +488,13 @@ def close_ssh():
488 _master_keys_lock = None 488 _master_keys_lock = None
489 489
490URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):') 490URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
491URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/') 491URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
492
493def GetSchemeFromUrl(url):
494 m = URI_ALL.match(url)
495 if m:
496 return m.group(1)
497 return None
492 498
493def _preconnect(url): 499def _preconnect(url):
494 m = URI_ALL.match(url) 500 m = URI_ALL.match(url)
@@ -521,7 +527,7 @@ class Remote(object):
521 self.projectname = self._Get('projectname') 527 self.projectname = self._Get('projectname')
522 self.fetch = map(lambda x: RefSpec.FromString(x), 528 self.fetch = map(lambda x: RefSpec.FromString(x),
523 self._Get('fetch', all=True)) 529 self._Get('fetch', all=True))
524 self._review_protocol = None 530 self._review_url = None
525 531
526 def _InsteadOf(self): 532 def _InsteadOf(self):
527 globCfg = GitConfig.ForUser() 533 globCfg = GitConfig.ForUser()
@@ -548,9 +554,8 @@ class Remote(object):
548 connectionUrl = self._InsteadOf() 554 connectionUrl = self._InsteadOf()
549 return _preconnect(connectionUrl) 555 return _preconnect(connectionUrl)
550 556
551 @property 557 def ReviewUrl(self, userEmail):
552 def ReviewProtocol(self): 558 if self._review_url is None:
553 if self._review_protocol is None:
554 if self.review is None: 559 if self.review is None:
555 return None 560 return None
556 561
@@ -559,57 +564,47 @@ class Remote(object):
559 u = 'http://%s' % u 564 u = 'http://%s' % u
560 if u.endswith('/Gerrit'): 565 if u.endswith('/Gerrit'):
561 u = u[:len(u) - len('/Gerrit')] 566 u = u[:len(u) - len('/Gerrit')]
562 if not u.endswith('/ssh_info'): 567 if u.endswith('/ssh_info'):
563 if not u.endswith('/'): 568 u = u[:len(u) - len('/ssh_info')]
564 u += '/' 569 if not u.endswith('/'):
565 u += 'ssh_info' 570 u += '/'
571 http_url = u
566 572
567 if u in REVIEW_CACHE: 573 if u in REVIEW_CACHE:
568 info = REVIEW_CACHE[u] 574 self._review_url = REVIEW_CACHE[u]
569 self._review_protocol = info[0] 575 elif 'REPO_HOST_PORT_INFO' in os.environ:
570 self._review_host = info[1] 576 host, port = os.environ['REPO_HOST_PORT_INFO'].split()
571 self._review_port = info[2] 577 self._review_url = self._SshReviewUrl(userEmail, host, port)
578 REVIEW_CACHE[u] = self._review_url
572 else: 579 else:
573 try: 580 try:
574 info = urlopen(u).read() 581 info_url = u + 'ssh_info'
575 if info == 'NOT_AVAILABLE': 582 info = urllib2.urlopen(info_url).read()
576 raise UploadError('%s: SSH disabled' % self.review)
577 if '<' in info: 583 if '<' in info:
578 # Assume the server gave us some sort of HTML 584 # Assume the server gave us some sort of HTML
579 # response back, like maybe a login page. 585 # response back, like maybe a login page.
580 # 586 #
581 raise UploadError('%s: Cannot parse response' % u) 587 raise UploadError('%s: Cannot parse response' % info_url)
582 588
583 self._review_protocol = 'ssh' 589 if info == 'NOT_AVAILABLE':
584 self._review_host = info.split(" ")[0] 590 # Assume HTTP if SSH is not enabled.
585 self._review_port = info.split(" ")[1] 591 self._review_url = http_url + 'p/'
586 except urllib2.URLError, e:
587 raise UploadError('%s: %s' % (self.review, e.reason[1]))
588 except HTTPError, e:
589 if e.code == 404:
590 self._review_protocol = 'http-post'
591 self._review_host = None
592 self._review_port = None
593 else: 592 else:
594 raise UploadError('Upload over ssh unavailable') 593 host, port = info.split()
594 self._review_url = self._SshReviewUrl(userEmail, host, port)
595 except urllib2.HTTPError, e:
596 raise UploadError('%s: %s' % (self.review, str(e)))
597 except urllib2.URLError, e:
598 raise UploadError('%s: %s' % (self.review, str(e)))
595 599
596 REVIEW_CACHE[u] = ( 600 REVIEW_CACHE[u] = self._review_url
597 self._review_protocol, 601 return self._review_url + self.projectname
598 self._review_host,
599 self._review_port)
600 return self._review_protocol
601 602
602 def SshReviewUrl(self, userEmail): 603 def _SshReviewUrl(self, userEmail, host, port):
603 if self.ReviewProtocol != 'ssh':
604 return None
605 username = self._config.GetString('review.%s.username' % self.review) 604 username = self._config.GetString('review.%s.username' % self.review)
606 if username is None: 605 if username is None:
607 username = userEmail.split("@")[0] 606 username = userEmail.split('@')[0]
608 return 'ssh://%s@%s:%s/%s' % ( 607 return 'ssh://%s@%s:%s/' % (username, host, port)
609 username,
610 self._review_host,
611 self._review_port,
612 self.projectname)
613 608
614 def ToLocal(self, rev): 609 def ToLocal(self, rev):
615 """Convert a remote revision string to something we have locally. 610 """Convert a remote revision string to something we have locally.
diff --git a/git_refs.py b/git_refs.py
index b24a0b4e..0e3cc820 100644
--- a/git_refs.py
+++ b/git_refs.py
@@ -21,6 +21,7 @@ HEAD = 'HEAD'
21R_HEADS = 'refs/heads/' 21R_HEADS = 'refs/heads/'
22R_TAGS = 'refs/tags/' 22R_TAGS = 'refs/tags/'
23R_PUB = 'refs/published/' 23R_PUB = 'refs/published/'
24R_M = 'refs/remotes/m/'
24 25
25 26
26class GitRefs(object): 27class GitRefs(object):
@@ -138,13 +139,15 @@ class GitRefs(object):
138 def _ReadLoose1(self, path, name): 139 def _ReadLoose1(self, path, name):
139 try: 140 try:
140 fd = open(path, 'rb') 141 fd = open(path, 'rb')
141 mtime = os.path.getmtime(path) 142 except:
142 except OSError:
143 return
144 except IOError:
145 return 143 return
144
146 try: 145 try:
147 id = fd.readline() 146 try:
147 mtime = os.path.getmtime(path)
148 id = fd.readline()
149 except:
150 return
148 finally: 151 finally:
149 fd.close() 152 fd.close()
150 153
diff --git a/hooks/pre-auto-gc b/hooks/pre-auto-gc
index 110e3194..360e5e1f 100755
--- a/hooks/pre-auto-gc
+++ b/hooks/pre-auto-gc
@@ -38,6 +38,11 @@ elif test -x /usr/bin/pmset && /usr/bin/pmset -g batt |
38 grep -q "Currently drawing from 'AC Power'" 38 grep -q "Currently drawing from 'AC Power'"
39then 39then
40 exit 0 40 exit 0
41elif test -d /sys/bus/acpi/drivers/battery && test 0 = \
42 "$(find /sys/bus/acpi/drivers/battery/ -type l | wc -l)";
43then
44 # No battery exists.
45 exit 0
41fi 46fi
42 47
43echo "Auto packing deferred; not on AC" 48echo "Auto packing deferred; not on AC"
diff --git a/main.py b/main.py
index 07b26ef7..ea29851e 100755
--- a/main.py
+++ b/main.py
@@ -22,19 +22,27 @@ if __name__ == '__main__':
22 del sys.argv[-1] 22 del sys.argv[-1]
23del magic 23del magic
24 24
25import netrc
25import optparse 26import optparse
26import os 27import os
27import re 28import re
28import sys 29import sys
30import time
31import urllib2
29 32
30from trace import SetTrace 33from trace import SetTrace
34from git_command import git, GitCommand
31from git_config import init_ssh, close_ssh 35from git_config import init_ssh, close_ssh
32from command import InteractiveCommand 36from command import InteractiveCommand
33from command import MirrorSafeCommand 37from command import MirrorSafeCommand
34from command import PagedCommand 38from command import PagedCommand
39from subcmds.version import Version
40from editor import Editor
41from error import DownloadError
35from error import ManifestInvalidRevisionError 42from error import ManifestInvalidRevisionError
36from error import NoSuchProjectError 43from error import NoSuchProjectError
37from error import RepoChangedException 44from error import RepoChangedException
45from manifest_xml import XmlManifest
38from pager import RunPager 46from pager import RunPager
39 47
40from subcmds import all as all_commands 48from subcmds import all as all_commands
@@ -51,6 +59,9 @@ global_options.add_option('--no-pager',
51global_options.add_option('--trace', 59global_options.add_option('--trace',
52 dest='trace', action='store_true', 60 dest='trace', action='store_true',
53 help='trace git command execution') 61 help='trace git command execution')
62global_options.add_option('--time',
63 dest='time', action='store_true',
64 help='time repo command execution')
54global_options.add_option('--version', 65global_options.add_option('--version',
55 dest='show_version', action='store_true', 66 dest='show_version', action='store_true',
56 help='display this version of repo') 67 help='display this version of repo')
@@ -63,6 +74,7 @@ class _Repo(object):
63 all_commands['branch'] = all_commands['branches'] 74 all_commands['branch'] = all_commands['branches']
64 75
65 def _Run(self, argv): 76 def _Run(self, argv):
77 result = 0
66 name = None 78 name = None
67 glob = [] 79 glob = []
68 80
@@ -86,7 +98,7 @@ class _Repo(object):
86 name = 'version' 98 name = 'version'
87 else: 99 else:
88 print >>sys.stderr, 'fatal: invalid usage of --version' 100 print >>sys.stderr, 'fatal: invalid usage of --version'
89 sys.exit(1) 101 return 1
90 102
91 try: 103 try:
92 cmd = self.commands[name] 104 cmd = self.commands[name]
@@ -94,15 +106,17 @@ class _Repo(object):
94 print >>sys.stderr,\ 106 print >>sys.stderr,\
95 "repo: '%s' is not a repo command. See 'repo help'."\ 107 "repo: '%s' is not a repo command. See 'repo help'."\
96 % name 108 % name
97 sys.exit(1) 109 return 1
98 110
99 cmd.repodir = self.repodir 111 cmd.repodir = self.repodir
112 cmd.manifest = XmlManifest(cmd.repodir)
113 Editor.globalConfig = cmd.manifest.globalConfig
100 114
101 if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror: 115 if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
102 print >>sys.stderr, \ 116 print >>sys.stderr, \
103 "fatal: '%s' requires a working directory"\ 117 "fatal: '%s' requires a working directory"\
104 % name 118 % name
105 sys.exit(1) 119 return 1
106 120
107 copts, cargs = cmd.OptionParser.parse_args(argv) 121 copts, cargs = cmd.OptionParser.parse_args(argv)
108 122
@@ -118,16 +132,37 @@ class _Repo(object):
118 RunPager(config) 132 RunPager(config)
119 133
120 try: 134 try:
121 cmd.Execute(copts, cargs) 135 start = time.time()
136 try:
137 result = cmd.Execute(copts, cargs)
138 finally:
139 elapsed = time.time() - start
140 hours, remainder = divmod(elapsed, 3600)
141 minutes, seconds = divmod(remainder, 60)
142 if gopts.time:
143 if hours == 0:
144 print >>sys.stderr, 'real\t%dm%.3fs' \
145 % (minutes, seconds)
146 else:
147 print >>sys.stderr, 'real\t%dh%dm%.3fs' \
148 % (hours, minutes, seconds)
149 except DownloadError, e:
150 print >>sys.stderr, 'error: %s' % str(e)
151 return 1
122 except ManifestInvalidRevisionError, e: 152 except ManifestInvalidRevisionError, e:
123 print >>sys.stderr, 'error: %s' % str(e) 153 print >>sys.stderr, 'error: %s' % str(e)
124 sys.exit(1) 154 return 1
125 except NoSuchProjectError, e: 155 except NoSuchProjectError, e:
126 if e.name: 156 if e.name:
127 print >>sys.stderr, 'error: project %s not found' % e.name 157 print >>sys.stderr, 'error: project %s not found' % e.name
128 else: 158 else:
129 print >>sys.stderr, 'error: no project in current directory' 159 print >>sys.stderr, 'error: no project in current directory'
130 sys.exit(1) 160 return 1
161
162 return result
163
164def _MyRepoPath():
165 return os.path.dirname(__file__)
131 166
132def _MyWrapperPath(): 167def _MyWrapperPath():
133 return os.path.join(os.path.dirname(__file__), 'repo') 168 return os.path.join(os.path.dirname(__file__), 'repo')
@@ -195,7 +230,117 @@ def _PruneOptions(argv, opt):
195 continue 230 continue
196 i += 1 231 i += 1
197 232
233_user_agent = None
234
235def _UserAgent():
236 global _user_agent
237
238 if _user_agent is None:
239 py_version = sys.version_info
240
241 os_name = sys.platform
242 if os_name == 'linux2':
243 os_name = 'Linux'
244 elif os_name == 'win32':
245 os_name = 'Win32'
246 elif os_name == 'cygwin':
247 os_name = 'Cygwin'
248 elif os_name == 'darwin':
249 os_name = 'Darwin'
250
251 p = GitCommand(
252 None, ['describe', 'HEAD'],
253 cwd = _MyRepoPath(),
254 capture_stdout = True)
255 if p.Wait() == 0:
256 repo_version = p.stdout
257 if len(repo_version) > 0 and repo_version[-1] == '\n':
258 repo_version = repo_version[0:-1]
259 if len(repo_version) > 0 and repo_version[0] == 'v':
260 repo_version = repo_version[1:]
261 else:
262 repo_version = 'unknown'
263
264 _user_agent = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % (
265 repo_version,
266 os_name,
267 '.'.join(map(lambda d: str(d), git.version_tuple())),
268 py_version[0], py_version[1], py_version[2])
269 return _user_agent
270
271class _UserAgentHandler(urllib2.BaseHandler):
272 def http_request(self, req):
273 req.add_header('User-Agent', _UserAgent())
274 return req
275
276 def https_request(self, req):
277 req.add_header('User-Agent', _UserAgent())
278 return req
279
280class _BasicAuthHandler(urllib2.HTTPBasicAuthHandler):
281 def http_error_auth_reqed(self, authreq, host, req, headers):
282 try:
283 old_add_header = req.add_header
284 def _add_header(name, val):
285 val = val.replace('\n', '')
286 old_add_header(name, val)
287 req.add_header = _add_header
288 return urllib2.AbstractBasicAuthHandler.http_error_auth_reqed(
289 self, authreq, host, req, headers)
290 except:
291 reset = getattr(self, 'reset_retry_count', None)
292 if reset is not None:
293 reset()
294 elif getattr(self, 'retried', None):
295 self.retried = 0
296 raise
297
298class _DigestAuthHandler(urllib2.HTTPDigestAuthHandler):
299 def http_error_auth_reqed(self, auth_header, host, req, headers):
300 try:
301 old_add_header = req.add_header
302 def _add_header(name, val):
303 val = val.replace('\n', '')
304 old_add_header(name, val)
305 req.add_header = _add_header
306 return urllib2.AbstractDigestAuthHandler.http_error_auth_reqed(
307 self, auth_header, host, req, headers)
308 except:
309 reset = getattr(self, 'reset_retry_count', None)
310 if reset is not None:
311 reset()
312 elif getattr(self, 'retried', None):
313 self.retried = 0
314 raise
315
316def init_http():
317 handlers = [_UserAgentHandler()]
318
319 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
320 try:
321 n = netrc.netrc()
322 for host in n.hosts:
323 p = n.hosts[host]
324 mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2])
325 mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2])
326 except netrc.NetrcParseError:
327 pass
328 except IOError:
329 pass
330 handlers.append(_BasicAuthHandler(mgr))
331 handlers.append(_DigestAuthHandler(mgr))
332
333 if 'http_proxy' in os.environ:
334 url = os.environ['http_proxy']
335 handlers.append(urllib2.ProxyHandler({'http': url, 'https': url}))
336 if 'REPO_CURL_VERBOSE' in os.environ:
337 handlers.append(urllib2.HTTPHandler(debuglevel=1))
338 handlers.append(urllib2.HTTPSHandler(debuglevel=1))
339 urllib2.install_opener(urllib2.build_opener(*handlers))
340
198def _Main(argv): 341def _Main(argv):
342 result = 0
343
199 opt = optparse.OptionParser(usage="repo wrapperinfo -- ...") 344 opt = optparse.OptionParser(usage="repo wrapperinfo -- ...")
200 opt.add_option("--repo-dir", dest="repodir", 345 opt.add_option("--repo-dir", dest="repodir",
201 help="path to .repo/") 346 help="path to .repo/")
@@ -209,15 +354,19 @@ def _Main(argv):
209 _CheckWrapperVersion(opt.wrapper_version, opt.wrapper_path) 354 _CheckWrapperVersion(opt.wrapper_version, opt.wrapper_path)
210 _CheckRepoDir(opt.repodir) 355 _CheckRepoDir(opt.repodir)
211 356
357 Version.wrapper_version = opt.wrapper_version
358 Version.wrapper_path = opt.wrapper_path
359
212 repo = _Repo(opt.repodir) 360 repo = _Repo(opt.repodir)
213 try: 361 try:
214 try: 362 try:
215 init_ssh() 363 init_ssh()
216 repo._Run(argv) 364 init_http()
365 result = repo._Run(argv) or 0
217 finally: 366 finally:
218 close_ssh() 367 close_ssh()
219 except KeyboardInterrupt: 368 except KeyboardInterrupt:
220 sys.exit(1) 369 result = 1
221 except RepoChangedException, rce: 370 except RepoChangedException, rce:
222 # If repo changed, re-exec ourselves. 371 # If repo changed, re-exec ourselves.
223 # 372 #
@@ -228,7 +377,9 @@ def _Main(argv):
228 except OSError, e: 377 except OSError, e:
229 print >>sys.stderr, 'fatal: cannot restart repo after upgrade' 378 print >>sys.stderr, 'fatal: cannot restart repo after upgrade'
230 print >>sys.stderr, 'fatal: %s' % e 379 print >>sys.stderr, 'fatal: %s' % e
231 sys.exit(128) 380 result = 128
381
382 sys.exit(result)
232 383
233if __name__ == '__main__': 384if __name__ == '__main__':
234 _Main(sys.argv[1:]) 385 _Main(sys.argv[1:])
diff --git a/manifest.py b/manifest.py
deleted file mode 100644
index c03cb4a7..00000000
--- a/manifest.py
+++ /dev/null
@@ -1,59 +0,0 @@
1#
2# Copyright (C) 2009 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import os
17
18from error import ManifestParseError
19from editor import Editor
20from git_config import GitConfig
21from project import MetaProject
22
23class Manifest(object):
24 """any manifest format"""
25
26 def __init__(self, repodir):
27 self.repodir = os.path.abspath(repodir)
28 self.topdir = os.path.dirname(self.repodir)
29 self.globalConfig = GitConfig.ForUser()
30 Editor.globalConfig = self.globalConfig
31
32 self.repoProject = MetaProject(self, 'repo',
33 gitdir = os.path.join(repodir, 'repo/.git'),
34 worktree = os.path.join(repodir, 'repo'))
35
36 @property
37 def IsMirror(self):
38 return self.manifestProject.config.GetBoolean('repo.mirror')
39
40 @property
41 def projects(self):
42 return {}
43
44 @property
45 def notice(self):
46 return None
47
48 @property
49 def manifest_server(self):
50 return None
51
52 def InitBranch(self):
53 pass
54
55 def SetMRefs(self, project):
56 pass
57
58 def Upgrade_Local(self, old):
59 raise ManifestParseError, 'unsupported upgrade path'
diff --git a/manifest_loader.py b/manifest_loader.py
deleted file mode 100644
index 467cb42a..00000000
--- a/manifest_loader.py
+++ /dev/null
@@ -1,34 +0,0 @@
1#
2# Copyright (C) 2009 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16from manifest_submodule import SubmoduleManifest
17from manifest_xml import XmlManifest
18
19def ParseManifest(repodir, type=None):
20 if type:
21 return type(repodir)
22 if SubmoduleManifest.Is(repodir):
23 return SubmoduleManifest(repodir)
24 return XmlManifest(repodir)
25
26_manifest = None
27
28def GetManifest(repodir, reparse=False, type=None):
29 global _manifest
30 if _manifest is None \
31 or reparse \
32 or (type and _manifest.__class__ != type):
33 _manifest = ParseManifest(repodir, type=type)
34 return _manifest
diff --git a/manifest_submodule.py b/manifest_submodule.py
deleted file mode 100644
index cac271cd..00000000
--- a/manifest_submodule.py
+++ /dev/null
@@ -1,481 +0,0 @@
1#
2# Copyright (C) 2009 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import sys
17import os
18import shutil
19
20from error import GitError
21from error import ManifestParseError
22from git_command import GitCommand
23from git_config import GitConfig
24from git_config import IsId
25from manifest import Manifest
26from progress import Progress
27from project import RemoteSpec
28from project import Project
29from project import MetaProject
30from project import R_HEADS
31from project import HEAD
32from project import _lwrite
33
34import manifest_xml
35
36GITLINK = '160000'
37
38def _rmdir(dir, top):
39 while dir != top:
40 try:
41 os.rmdir(dir)
42 except OSError:
43 break
44 dir = os.path.dirname(dir)
45
46def _rmref(gitdir, ref):
47 os.remove(os.path.join(gitdir, ref))
48 log = os.path.join(gitdir, 'logs', ref)
49 if os.path.exists(log):
50 os.remove(log)
51 _rmdir(os.path.dirname(log), gitdir)
52
53def _has_gitmodules(d):
54 return os.path.exists(os.path.join(d, '.gitmodules'))
55
56class SubmoduleManifest(Manifest):
57 """manifest from .gitmodules file"""
58
59 @classmethod
60 def Is(cls, repodir):
61 return _has_gitmodules(os.path.dirname(repodir)) \
62 or _has_gitmodules(os.path.join(repodir, 'manifest')) \
63 or _has_gitmodules(os.path.join(repodir, 'manifests'))
64
65 @classmethod
66 def IsBare(cls, p):
67 try:
68 p.bare_git.cat_file('-e', '%s:.gitmodules' % p.GetRevisionId())
69 except GitError:
70 return False
71 return True
72
73 def __init__(self, repodir):
74 Manifest.__init__(self, repodir)
75
76 gitdir = os.path.join(repodir, 'manifest.git')
77 config = GitConfig.ForRepository(gitdir = gitdir)
78
79 if config.GetBoolean('repo.mirror'):
80 worktree = os.path.join(repodir, 'manifest')
81 relpath = None
82 else:
83 worktree = self.topdir
84 relpath = '.'
85
86 self.manifestProject = MetaProject(self, '__manifest__',
87 gitdir = gitdir,
88 worktree = worktree,
89 relpath = relpath)
90 self._modules = GitConfig(os.path.join(worktree, '.gitmodules'),
91 pickleFile = os.path.join(
92 repodir, '.repopickle_gitmodules'
93 ))
94 self._review = GitConfig(os.path.join(worktree, '.review'),
95 pickleFile = os.path.join(
96 repodir, '.repopickle_review'
97 ))
98 self._Unload()
99
100 @property
101 def projects(self):
102 self._Load()
103 return self._projects
104
105 @property
106 def notice(self):
107 return self._modules.GetString('repo.notice')
108
109 def InitBranch(self):
110 m = self.manifestProject
111 if m.CurrentBranch is None:
112 b = m.revisionExpr
113 if b.startswith(R_HEADS):
114 b = b[len(R_HEADS):]
115 return m.StartBranch(b)
116 return True
117
118 def SetMRefs(self, project):
119 if project.revisionId is None:
120 # Special project, e.g. the manifest or repo executable.
121 #
122 return
123
124 ref = 'refs/remotes/m'
125 cur = project.bare_ref.get(ref)
126 exp = project.revisionId
127 if cur != exp:
128 msg = 'manifest set to %s' % exp
129 project.bare_git.UpdateRef(ref, exp, message = msg, detach = True)
130
131 ref = 'refs/remotes/m-revision'
132 cur = project.bare_ref.symref(ref)
133 exp = project.revisionExpr
134 if exp is None:
135 if cur:
136 _rmref(project.gitdir, ref)
137 elif cur != exp:
138 remote = project.GetRemote(project.remote.name)
139 dst = remote.ToLocal(exp)
140 msg = 'manifest set to %s (%s)' % (exp, dst)
141 project.bare_git.symbolic_ref('-m', msg, ref, dst)
142
143 def Upgrade_Local(self, old):
144 if isinstance(old, manifest_xml.XmlManifest):
145 self.FromXml_Local_1(old, checkout=True)
146 self.FromXml_Local_2(old)
147 else:
148 raise ManifestParseError, 'cannot upgrade manifest'
149
150 def FromXml_Local_1(self, old, checkout):
151 os.rename(old.manifestProject.gitdir,
152 os.path.join(old.repodir, 'manifest.git'))
153
154 oldmp = old.manifestProject
155 oldBranch = oldmp.CurrentBranch
156 b = oldmp.GetBranch(oldBranch).merge
157 if not b:
158 raise ManifestParseError, 'cannot upgrade manifest'
159 if b.startswith(R_HEADS):
160 b = b[len(R_HEADS):]
161
162 newmp = self.manifestProject
163 self._CleanOldMRefs(newmp)
164 if oldBranch != b:
165 newmp.bare_git.branch('-m', oldBranch, b)
166 newmp.config.ClearCache()
167
168 old_remote = newmp.GetBranch(b).remote.name
169 act_remote = self._GuessRemoteName(old)
170 if old_remote != act_remote:
171 newmp.bare_git.remote('rename', old_remote, act_remote)
172 newmp.config.ClearCache()
173 newmp.remote.name = act_remote
174 print >>sys.stderr, "Assuming remote named '%s'" % act_remote
175
176 if checkout:
177 for p in old.projects.values():
178 for c in p.copyfiles:
179 if os.path.exists(c.abs_dest):
180 os.remove(c.abs_dest)
181 newmp._InitWorkTree()
182 else:
183 newmp._LinkWorkTree()
184
185 _lwrite(os.path.join(newmp.worktree,'.git',HEAD),
186 'ref: refs/heads/%s\n' % b)
187
188 def _GuessRemoteName(self, old):
189 used = {}
190 for p in old.projects.values():
191 n = p.remote.name
192 used[n] = used.get(n, 0) + 1
193
194 remote_name = 'origin'
195 remote_used = 0
196 for n in used.keys():
197 if remote_used < used[n]:
198 remote_used = used[n]
199 remote_name = n
200 return remote_name
201
202 def FromXml_Local_2(self, old):
203 shutil.rmtree(old.manifestProject.worktree)
204 os.remove(old._manifestFile)
205
206 my_remote = self._Remote().name
207 new_base = os.path.join(self.repodir, 'projects')
208 old_base = os.path.join(self.repodir, 'projects.old')
209 os.rename(new_base, old_base)
210 os.makedirs(new_base)
211
212 info = []
213 pm = Progress('Converting projects', len(self.projects))
214 for p in self.projects.values():
215 pm.update()
216
217 old_p = old.projects.get(p.name)
218 old_gitdir = os.path.join(old_base, '%s.git' % p.relpath)
219 if not os.path.isdir(old_gitdir):
220 continue
221
222 parent = os.path.dirname(p.gitdir)
223 if not os.path.isdir(parent):
224 os.makedirs(parent)
225 os.rename(old_gitdir, p.gitdir)
226 _rmdir(os.path.dirname(old_gitdir), self.repodir)
227
228 if not os.path.isdir(p.worktree):
229 os.makedirs(p.worktree)
230
231 if os.path.isdir(os.path.join(p.worktree, '.git')):
232 p._LinkWorkTree(relink=True)
233
234 self._CleanOldMRefs(p)
235 if old_p and old_p.remote.name != my_remote:
236 info.append("%s/: renamed remote '%s' to '%s'" \
237 % (p.relpath, old_p.remote.name, my_remote))
238 p.bare_git.remote('rename', old_p.remote.name, my_remote)
239 p.config.ClearCache()
240
241 self.SetMRefs(p)
242 pm.end()
243 for i in info:
244 print >>sys.stderr, i
245
246 def _CleanOldMRefs(self, p):
247 all_refs = p._allrefs
248 for ref in all_refs.keys():
249 if ref.startswith(manifest_xml.R_M):
250 if p.bare_ref.symref(ref) != '':
251 _rmref(p.gitdir, ref)
252 else:
253 p.bare_git.DeleteRef(ref, all_refs[ref])
254
255 def FromXml_Definition(self, old):
256 """Convert another manifest representation to this one.
257 """
258 mp = self.manifestProject
259 gm = self._modules
260 gr = self._review
261
262 fd = open(os.path.join(mp.worktree, '.gitignore'), 'ab')
263 fd.write('/.repo\n')
264 fd.close()
265
266 sort_projects = list(old.projects.keys())
267 sort_projects.sort()
268
269 b = mp.GetBranch(mp.CurrentBranch).merge
270 if b.startswith(R_HEADS):
271 b = b[len(R_HEADS):]
272
273 if old.notice:
274 gm.SetString('repo.notice', old.notice)
275
276 info = []
277 pm = Progress('Converting manifest', len(sort_projects))
278 for p in sort_projects:
279 pm.update()
280 p = old.projects[p]
281
282 gm.SetString('submodule.%s.path' % p.name, p.relpath)
283 gm.SetString('submodule.%s.url' % p.name, p.remote.url)
284
285 if gr.GetString('review.url') is None:
286 gr.SetString('review.url', p.remote.review)
287 elif gr.GetString('review.url') != p.remote.review:
288 gr.SetString('review.%s.url' % p.name, p.remote.review)
289
290 r = p.revisionExpr
291 if r and not IsId(r):
292 if r.startswith(R_HEADS):
293 r = r[len(R_HEADS):]
294 if r == b:
295 r = '.'
296 gm.SetString('submodule.%s.revision' % p.name, r)
297
298 for c in p.copyfiles:
299 info.append('Moved %s out of %s' % (c.src, p.relpath))
300 c._Copy()
301 p.work_git.rm(c.src)
302 mp.work_git.add(c.dest)
303
304 self.SetRevisionId(p.relpath, p.GetRevisionId())
305 mp.work_git.add('.gitignore', '.gitmodules', '.review')
306 pm.end()
307 for i in info:
308 print >>sys.stderr, i
309
310 def _Unload(self):
311 self._loaded = False
312 self._projects = {}
313 self._revisionIds = None
314 self.branch = None
315
316 def _Load(self):
317 if not self._loaded:
318 f = os.path.join(self.repodir, manifest_xml.LOCAL_MANIFEST_NAME)
319 if os.path.exists(f):
320 print >>sys.stderr, 'warning: ignoring %s' % f
321
322 m = self.manifestProject
323 b = m.CurrentBranch
324 if not b:
325 raise ManifestParseError, 'manifest cannot be on detached HEAD'
326 b = m.GetBranch(b).merge
327 if b.startswith(R_HEADS):
328 b = b[len(R_HEADS):]
329 self.branch = b
330 m.remote.name = self._Remote().name
331
332 self._ParseModules()
333
334 if self.IsMirror:
335 self._AddMetaProjectMirror(self.repoProject)
336 self._AddMetaProjectMirror(self.manifestProject)
337
338 self._loaded = True
339
340 def _ParseModules(self):
341 byPath = dict()
342 for name in self._modules.GetSubSections('submodule'):
343 p = self._ParseProject(name)
344 if self._projects.get(p.name):
345 raise ManifestParseError, 'duplicate project "%s"' % p.name
346 if byPath.get(p.relpath):
347 raise ManifestParseError, 'duplicate path "%s"' % p.relpath
348 self._projects[p.name] = p
349 byPath[p.relpath] = p
350
351 for relpath in self._allRevisionIds.keys():
352 if relpath not in byPath:
353 raise ManifestParseError, \
354 'project "%s" not in .gitmodules' \
355 % relpath
356
357 def _Remote(self):
358 m = self.manifestProject
359 b = m.GetBranch(m.CurrentBranch)
360 return b.remote
361
362 def _ResolveUrl(self, url):
363 if url.startswith('./') or url.startswith('../'):
364 base = self._Remote().url
365 try:
366 base = base[:base.rindex('/')+1]
367 except ValueError:
368 base = base[:base.rindex(':')+1]
369 if url.startswith('./'):
370 url = url[2:]
371 while '/' in base and url.startswith('../'):
372 base = base[:base.rindex('/')+1]
373 url = url[3:]
374 return base + url
375 return url
376
377 def _GetRevisionId(self, path):
378 return self._allRevisionIds.get(path)
379
380 @property
381 def _allRevisionIds(self):
382 if self._revisionIds is None:
383 a = dict()
384 p = GitCommand(self.manifestProject,
385 ['ls-files','-z','--stage'],
386 capture_stdout = True)
387 for line in p.process.stdout.read().split('\0')[:-1]:
388 l_info, l_path = line.split('\t', 2)
389 l_mode, l_id, l_stage = l_info.split(' ', 2)
390 if l_mode == GITLINK and l_stage == '0':
391 a[l_path] = l_id
392 p.Wait()
393 self._revisionIds = a
394 return self._revisionIds
395
396 def SetRevisionId(self, path, id):
397 self.manifestProject.work_git.update_index(
398 '--add','--cacheinfo', GITLINK, id, path)
399
400 def _ParseProject(self, name):
401 gm = self._modules
402 gr = self._review
403
404 path = gm.GetString('submodule.%s.path' % name)
405 if not path:
406 path = name
407
408 revId = self._GetRevisionId(path)
409 if not revId:
410 raise ManifestParseError(
411 'submodule "%s" has no revision at "%s"' \
412 % (name, path))
413
414 url = gm.GetString('submodule.%s.url' % name)
415 if not url:
416 url = name
417 url = self._ResolveUrl(url)
418
419 review = gr.GetString('review.%s.url' % name)
420 if not review:
421 review = gr.GetString('review.url')
422 if not review:
423 review = self._Remote().review
424
425 remote = RemoteSpec(self._Remote().name, url, review)
426 revExpr = gm.GetString('submodule.%s.revision' % name)
427 if revExpr == '.':
428 revExpr = self.branch
429
430 if self.IsMirror:
431 relpath = None
432 worktree = None
433 gitdir = os.path.join(self.topdir, '%s.git' % name)
434 else:
435 worktree = os.path.join(self.topdir, path)
436 gitdir = os.path.join(self.repodir, 'projects/%s.git' % name)
437
438 return Project(manifest = self,
439 name = name,
440 remote = remote,
441 gitdir = gitdir,
442 worktree = worktree,
443 relpath = path,
444 revisionExpr = revExpr,
445 revisionId = revId)
446
447 def _AddMetaProjectMirror(self, m):
448 m_url = m.GetRemote(m.remote.name).url
449 if m_url.endswith('/.git'):
450 raise ManifestParseError, 'refusing to mirror %s' % m_url
451
452 name = self._GuessMetaName(m_url)
453 if name.endswith('.git'):
454 name = name[:-4]
455
456 if name not in self._projects:
457 m.PreSync()
458 gitdir = os.path.join(self.topdir, '%s.git' % name)
459 project = Project(manifest = self,
460 name = name,
461 remote = RemoteSpec(self._Remote().name, m_url),
462 gitdir = gitdir,
463 worktree = None,
464 relpath = None,
465 revisionExpr = m.revisionExpr,
466 revisionId = None)
467 self._projects[project.name] = project
468
469 def _GuessMetaName(self, m_url):
470 parts = m_url.split('/')
471 name = parts[-1]
472 parts = parts[0:-1]
473 s = len(parts) - 1
474 while s > 0:
475 l = '/'.join(parts[0:s]) + '/'
476 r = '/'.join(parts[s:]) + '/'
477 for p in self._projects.values():
478 if p.name.startswith(r) and p.remote.url.startswith(l):
479 return r + name
480 s -= 1
481 return m_url[m_url.rindex('/') + 1:]
diff --git a/manifest_xml.py b/manifest_xml.py
index 1d02f9d4..26cc14f6 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -13,53 +13,75 @@
13# See the License for the specific language governing permissions and 13# See the License for the specific language governing permissions and
14# limitations under the License. 14# limitations under the License.
15 15
16import itertools
16import os 17import os
18import re
17import sys 19import sys
20import urlparse
18import xml.dom.minidom 21import xml.dom.minidom
19 22
20from git_config import GitConfig 23from git_config import GitConfig, IsId
21from git_config import IsId 24from project import RemoteSpec, Project, MetaProject, R_HEADS, HEAD
22from manifest import Manifest
23from project import RemoteSpec
24from project import Project
25from project import MetaProject
26from project import R_HEADS
27from project import HEAD
28from error import ManifestParseError 25from error import ManifestParseError
29 26
30MANIFEST_FILE_NAME = 'manifest.xml' 27MANIFEST_FILE_NAME = 'manifest.xml'
31LOCAL_MANIFEST_NAME = 'local_manifest.xml' 28LOCAL_MANIFEST_NAME = 'local_manifest.xml'
32R_M = 'refs/remotes/m/' 29
30urlparse.uses_relative.extend(['ssh', 'git'])
31urlparse.uses_netloc.extend(['ssh', 'git'])
33 32
34class _Default(object): 33class _Default(object):
35 """Project defaults within the manifest.""" 34 """Project defaults within the manifest."""
36 35
37 revisionExpr = None 36 revisionExpr = None
38 remote = None 37 remote = None
38 sync_j = 1
39 sync_c = False
39 40
40class _XmlRemote(object): 41class _XmlRemote(object):
41 def __init__(self, 42 def __init__(self,
42 name, 43 name,
44 alias=None,
43 fetch=None, 45 fetch=None,
46 manifestUrl=None,
44 review=None): 47 review=None):
45 self.name = name 48 self.name = name
46 self.fetchUrl = fetch 49 self.fetchUrl = fetch
50 self.manifestUrl = manifestUrl
51 self.remoteAlias = alias
47 self.reviewUrl = review 52 self.reviewUrl = review
53 self.resolvedFetchUrl = self._resolveFetchUrl()
54
55 def _resolveFetchUrl(self):
56 url = self.fetchUrl.rstrip('/')
57 manifestUrl = self.manifestUrl.rstrip('/')
58 # urljoin will get confused if there is no scheme in the base url
59 # ie, if manifestUrl is of the form <hostname:port>
60 if manifestUrl.find(':') != manifestUrl.find('/') - 1:
61 manifestUrl = 'gopher://' + manifestUrl
62 url = urlparse.urljoin(manifestUrl, url)
63 return re.sub(r'^gopher://', '', url)
48 64
49 def ToRemoteSpec(self, projectName): 65 def ToRemoteSpec(self, projectName):
50 url = self.fetchUrl 66 url = self.resolvedFetchUrl.rstrip('/') + '/' + projectName
51 while url.endswith('/'): 67 remoteName = self.name
52 url = url[:-1] 68 if self.remoteAlias:
53 url += '/%s.git' % projectName 69 remoteName = self.remoteAlias
54 return RemoteSpec(self.name, url, self.reviewUrl) 70 return RemoteSpec(remoteName, url, self.reviewUrl)
55 71
56class XmlManifest(Manifest): 72class XmlManifest(object):
57 """manages the repo configuration file""" 73 """manages the repo configuration file"""
58 74
59 def __init__(self, repodir): 75 def __init__(self, repodir):
60 Manifest.__init__(self, repodir) 76 self.repodir = os.path.abspath(repodir)
77 self.topdir = os.path.dirname(self.repodir)
78 self.manifestFile = os.path.join(self.repodir, MANIFEST_FILE_NAME)
79 self.globalConfig = GitConfig.ForUser()
80
81 self.repoProject = MetaProject(self, 'repo',
82 gitdir = os.path.join(repodir, 'repo/.git'),
83 worktree = os.path.join(repodir, 'repo'))
61 84
62 self._manifestFile = os.path.join(repodir, MANIFEST_FILE_NAME)
63 self.manifestProject = MetaProject(self, 'manifests', 85 self.manifestProject = MetaProject(self, 'manifests',
64 gitdir = os.path.join(repodir, 'manifests.git'), 86 gitdir = os.path.join(repodir, 'manifests.git'),
65 worktree = os.path.join(repodir, 'manifests')) 87 worktree = os.path.join(repodir, 'manifests'))
@@ -73,13 +95,13 @@ class XmlManifest(Manifest):
73 if not os.path.isfile(path): 95 if not os.path.isfile(path):
74 raise ManifestParseError('manifest %s not found' % name) 96 raise ManifestParseError('manifest %s not found' % name)
75 97
76 old = self._manifestFile 98 old = self.manifestFile
77 try: 99 try:
78 self._manifestFile = path 100 self.manifestFile = path
79 self._Unload() 101 self._Unload()
80 self._Load() 102 self._Load()
81 finally: 103 finally:
82 self._manifestFile = old 104 self.manifestFile = old
83 105
84 def Link(self, name): 106 def Link(self, name):
85 """Update the repo metadata to use a different manifest. 107 """Update the repo metadata to use a different manifest.
@@ -87,9 +109,9 @@ class XmlManifest(Manifest):
87 self.Override(name) 109 self.Override(name)
88 110
89 try: 111 try:
90 if os.path.exists(self._manifestFile): 112 if os.path.exists(self.manifestFile):
91 os.remove(self._manifestFile) 113 os.remove(self.manifestFile)
92 os.symlink('manifests/%s' % name, self._manifestFile) 114 os.symlink('manifests/%s' % name, self.manifestFile)
93 except OSError, e: 115 except OSError, e:
94 raise ManifestParseError('cannot link manifest %s' % name) 116 raise ManifestParseError('cannot link manifest %s' % name)
95 117
@@ -104,6 +126,13 @@ class XmlManifest(Manifest):
104 def Save(self, fd, peg_rev=False): 126 def Save(self, fd, peg_rev=False):
105 """Write the current manifest out to the given file descriptor. 127 """Write the current manifest out to the given file descriptor.
106 """ 128 """
129 mp = self.manifestProject
130
131 groups = mp.config.GetString('manifest.groups')
132 if not groups:
133 groups = 'default'
134 groups = [x for x in re.split(r'[,\s]+', groups) if x]
135
107 doc = xml.dom.minidom.Document() 136 doc = xml.dom.minidom.Document()
108 root = doc.createElement('manifest') 137 root = doc.createElement('manifest')
109 doc.appendChild(root) 138 doc.appendChild(root)
@@ -134,6 +163,12 @@ class XmlManifest(Manifest):
134 if d.revisionExpr: 163 if d.revisionExpr:
135 have_default = True 164 have_default = True
136 e.setAttribute('revision', d.revisionExpr) 165 e.setAttribute('revision', d.revisionExpr)
166 if d.sync_j > 1:
167 have_default = True
168 e.setAttribute('sync-j', '%d' % d.sync_j)
169 if d.sync_c:
170 have_default = True
171 e.setAttribute('sync-c', 'true')
137 if have_default: 172 if have_default:
138 root.appendChild(e) 173 root.appendChild(e)
139 root.appendChild(doc.createTextNode('')) 174 root.appendChild(doc.createTextNode(''))
@@ -149,6 +184,10 @@ class XmlManifest(Manifest):
149 184
150 for p in sort_projects: 185 for p in sort_projects:
151 p = self.projects[p] 186 p = self.projects[p]
187
188 if not p.MatchesGroups(groups):
189 continue
190
152 e = doc.createElement('project') 191 e = doc.createElement('project')
153 root.appendChild(e) 192 root.appendChild(e)
154 e.setAttribute('name', p.name) 193 e.setAttribute('name', p.name)
@@ -172,6 +211,29 @@ class XmlManifest(Manifest):
172 ce.setAttribute('dest', c.dest) 211 ce.setAttribute('dest', c.dest)
173 e.appendChild(ce) 212 e.appendChild(ce)
174 213
214 default_groups = ['default', 'name:%s' % p.name, 'path:%s' % p.relpath]
215 egroups = [g for g in p.groups if g not in default_groups]
216 if egroups:
217 e.setAttribute('groups', ','.join(egroups))
218
219 for a in p.annotations:
220 if a.keep == "true":
221 ae = doc.createElement('annotation')
222 ae.setAttribute('name', a.name)
223 ae.setAttribute('value', a.value)
224 e.appendChild(ae)
225
226 if p.sync_c:
227 e.setAttribute('sync-c', 'true')
228
229 if self._repo_hooks_project:
230 root.appendChild(doc.createTextNode(''))
231 e = doc.createElement('repo-hooks')
232 e.setAttribute('in-project', self._repo_hooks_project.name)
233 e.setAttribute('enabled-list',
234 ' '.join(self._repo_hooks_project.enabled_repo_hooks))
235 root.appendChild(e)
236
175 doc.writexml(fd, '', ' ', '\n', 'UTF-8') 237 doc.writexml(fd, '', ' ', '\n', 'UTF-8')
176 238
177 @property 239 @property
@@ -190,6 +252,11 @@ class XmlManifest(Manifest):
190 return self._default 252 return self._default
191 253
192 @property 254 @property
255 def repo_hooks_project(self):
256 self._Load()
257 return self._repo_hooks_project
258
259 @property
193 def notice(self): 260 def notice(self):
194 self._Load() 261 self._Load()
195 return self._notice 262 return self._notice
@@ -199,21 +266,16 @@ class XmlManifest(Manifest):
199 self._Load() 266 self._Load()
200 return self._manifest_server 267 return self._manifest_server
201 268
202 def InitBranch(self): 269 @property
203 m = self.manifestProject 270 def IsMirror(self):
204 if m.CurrentBranch is None: 271 return self.manifestProject.config.GetBoolean('repo.mirror')
205 return m.StartBranch('default')
206 return True
207
208 def SetMRefs(self, project):
209 if self.branch:
210 project._InitAnyMRef(R_M + self.branch)
211 272
212 def _Unload(self): 273 def _Unload(self):
213 self._loaded = False 274 self._loaded = False
214 self._projects = {} 275 self._projects = {}
215 self._remotes = {} 276 self._remotes = {}
216 self._default = None 277 self._default = None
278 self._repo_hooks_project = None
217 self._notice = None 279 self._notice = None
218 self.branch = None 280 self.branch = None
219 self._manifest_server = None 281 self._manifest_server = None
@@ -221,24 +283,20 @@ class XmlManifest(Manifest):
221 def _Load(self): 283 def _Load(self):
222 if not self._loaded: 284 if not self._loaded:
223 m = self.manifestProject 285 m = self.manifestProject
224 b = m.GetBranch(m.CurrentBranch) 286 b = m.GetBranch(m.CurrentBranch).merge
225 if b.remote and b.remote.name:
226 m.remote.name = b.remote.name
227 b = b.merge
228 if b is not None and b.startswith(R_HEADS): 287 if b is not None and b.startswith(R_HEADS):
229 b = b[len(R_HEADS):] 288 b = b[len(R_HEADS):]
230 self.branch = b 289 self.branch = b
231 290
232 self._ParseManifest(True) 291 nodes = []
292 nodes.append(self._ParseManifestXml(self.manifestFile,
293 self.manifestProject.worktree))
233 294
234 local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME) 295 local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME)
235 if os.path.exists(local): 296 if os.path.exists(local):
236 try: 297 nodes.append(self._ParseManifestXml(local, self.repodir))
237 real = self._manifestFile 298
238 self._manifestFile = local 299 self._ParseManifest(nodes)
239 self._ParseManifest(False)
240 finally:
241 self._manifestFile = real
242 300
243 if self.IsMirror: 301 if self.IsMirror:
244 self._AddMetaProjectMirror(self.repoProject) 302 self._AddMetaProjectMirror(self.repoProject)
@@ -246,73 +304,117 @@ class XmlManifest(Manifest):
246 304
247 self._loaded = True 305 self._loaded = True
248 306
249 def _ParseManifest(self, is_root_file): 307 def _ParseManifestXml(self, path, include_root):
250 root = xml.dom.minidom.parse(self._manifestFile) 308 root = xml.dom.minidom.parse(path)
251 if not root or not root.childNodes: 309 if not root or not root.childNodes:
252 raise ManifestParseError, \ 310 raise ManifestParseError("no root node in %s" % (path,))
253 "no root node in %s" % \
254 self._manifestFile
255 311
256 config = root.childNodes[0] 312 config = root.childNodes[0]
257 if config.nodeName != 'manifest': 313 if config.nodeName != 'manifest':
258 raise ManifestParseError, \ 314 raise ManifestParseError("no <manifest> in %s" % (path,))
259 "no <manifest> in %s" % \
260 self._manifestFile
261 315
316 nodes = []
262 for node in config.childNodes: 317 for node in config.childNodes:
263 if node.nodeName == 'remove-project': 318 if node.nodeName == 'include':
264 name = self._reqatt(node, 'name') 319 name = self._reqatt(node, 'name')
265 try: 320 fp = os.path.join(include_root, name)
266 del self._projects[name] 321 if not os.path.isfile(fp):
267 except KeyError: 322 raise ManifestParseError, \
268 raise ManifestParseError, \ 323 "include %s doesn't exist or isn't a file" % \
269 'project %s not found' % \ 324 (name,)
270 (name) 325 try:
326 nodes.extend(self._ParseManifestXml(fp, include_root))
327 # should isolate this to the exact exception, but that's
328 # tricky. actual parsing implementation may vary.
329 except (KeyboardInterrupt, RuntimeError, SystemExit):
330 raise
331 except Exception, e:
332 raise ManifestParseError(
333 "failed parsing included manifest %s: %s", (name, e))
334 else:
335 nodes.append(node)
336 return nodes
271 337
272 for node in config.childNodes: 338 def _ParseManifest(self, node_list):
339 for node in itertools.chain(*node_list):
273 if node.nodeName == 'remote': 340 if node.nodeName == 'remote':
274 remote = self._ParseRemote(node) 341 remote = self._ParseRemote(node)
275 if self._remotes.get(remote.name): 342 if self._remotes.get(remote.name):
276 raise ManifestParseError, \ 343 raise ManifestParseError(
277 'duplicate remote %s in %s' % \ 344 'duplicate remote %s in %s' %
278 (remote.name, self._manifestFile) 345 (remote.name, self.manifestFile))
279 self._remotes[remote.name] = remote 346 self._remotes[remote.name] = remote
280 347
281 for node in config.childNodes: 348 for node in itertools.chain(*node_list):
282 if node.nodeName == 'default': 349 if node.nodeName == 'default':
283 if self._default is not None: 350 if self._default is not None:
284 raise ManifestParseError, \ 351 raise ManifestParseError(
285 'duplicate default in %s' % \ 352 'duplicate default in %s' %
286 (self._manifestFile) 353 (self.manifestFile))
287 self._default = self._ParseDefault(node) 354 self._default = self._ParseDefault(node)
288 if self._default is None: 355 if self._default is None:
289 self._default = _Default() 356 self._default = _Default()
290 357
291 for node in config.childNodes: 358 for node in itertools.chain(*node_list):
292 if node.nodeName == 'notice': 359 if node.nodeName == 'notice':
293 if self._notice is not None: 360 if self._notice is not None:
294 raise ManifestParseError, \ 361 raise ManifestParseError(
295 'duplicate notice in %s' % \ 362 'duplicate notice in %s' %
296 (self.manifestFile) 363 (self.manifestFile))
297 self._notice = self._ParseNotice(node) 364 self._notice = self._ParseNotice(node)
298 365
299 for node in config.childNodes: 366 for node in itertools.chain(*node_list):
300 if node.nodeName == 'manifest-server': 367 if node.nodeName == 'manifest-server':
301 url = self._reqatt(node, 'url') 368 url = self._reqatt(node, 'url')
302 if self._manifest_server is not None: 369 if self._manifest_server is not None:
303 raise ManifestParseError, \ 370 raise ManifestParseError(
304 'duplicate manifest-server in %s' % \ 371 'duplicate manifest-server in %s' %
305 (self.manifestFile) 372 (self.manifestFile))
306 self._manifest_server = url 373 self._manifest_server = url
307 374
308 for node in config.childNodes: 375 for node in itertools.chain(*node_list):
309 if node.nodeName == 'project': 376 if node.nodeName == 'project':
310 project = self._ParseProject(node) 377 project = self._ParseProject(node)
311 if self._projects.get(project.name): 378 if self._projects.get(project.name):
312 raise ManifestParseError, \ 379 raise ManifestParseError(
313 'duplicate project %s in %s' % \ 380 'duplicate project %s in %s' %
314 (project.name, self._manifestFile) 381 (project.name, self.manifestFile))
315 self._projects[project.name] = project 382 self._projects[project.name] = project
383 if node.nodeName == 'repo-hooks':
384 # Get the name of the project and the (space-separated) list of enabled.
385 repo_hooks_project = self._reqatt(node, 'in-project')
386 enabled_repo_hooks = self._reqatt(node, 'enabled-list').split()
387
388 # Only one project can be the hooks project
389 if self._repo_hooks_project is not None:
390 raise ManifestParseError(
391 'duplicate repo-hooks in %s' %
392 (self.manifestFile))
393
394 # Store a reference to the Project.
395 try:
396 self._repo_hooks_project = self._projects[repo_hooks_project]
397 except KeyError:
398 raise ManifestParseError(
399 'project %s not found for repo-hooks' %
400 (repo_hooks_project))
401
402 # Store the enabled hooks in the Project object.
403 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
404 if node.nodeName == 'remove-project':
405 name = self._reqatt(node, 'name')
406 try:
407 del self._projects[name]
408 except KeyError:
409 raise ManifestParseError(
410 'project %s not found' %
411 (name))
412
413 # If the manifest removes the hooks project, treat it as if it deleted
414 # the repo-hooks element too.
415 if self._repo_hooks_project and (self._repo_hooks_project.name == name):
416 self._repo_hooks_project = None
417
316 418
317 def _AddMetaProjectMirror(self, m): 419 def _AddMetaProjectMirror(self, m):
318 name = None 420 name = None
@@ -321,7 +423,7 @@ class XmlManifest(Manifest):
321 raise ManifestParseError, 'refusing to mirror %s' % m_url 423 raise ManifestParseError, 'refusing to mirror %s' % m_url
322 424
323 if self._default and self._default.remote: 425 if self._default and self._default.remote:
324 url = self._default.remote.fetchUrl 426 url = self._default.remote.resolvedFetchUrl
325 if not url.endswith('/'): 427 if not url.endswith('/'):
326 url += '/' 428 url += '/'
327 if m_url.startswith(url): 429 if m_url.startswith(url):
@@ -330,7 +432,8 @@ class XmlManifest(Manifest):
330 432
331 if name is None: 433 if name is None:
332 s = m_url.rindex('/') + 1 434 s = m_url.rindex('/') + 1
333 remote = _XmlRemote('origin', m_url[:s]) 435 manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
436 remote = _XmlRemote('origin', fetch=m_url[:s], manifestUrl=manifestUrl)
334 name = m_url[s:] 437 name = m_url[s:]
335 438
336 if name.endswith('.git'): 439 if name.endswith('.git'):
@@ -354,11 +457,15 @@ class XmlManifest(Manifest):
354 reads a <remote> element from the manifest file 457 reads a <remote> element from the manifest file
355 """ 458 """
356 name = self._reqatt(node, 'name') 459 name = self._reqatt(node, 'name')
460 alias = node.getAttribute('alias')
461 if alias == '':
462 alias = None
357 fetch = self._reqatt(node, 'fetch') 463 fetch = self._reqatt(node, 'fetch')
358 review = node.getAttribute('review') 464 review = node.getAttribute('review')
359 if review == '': 465 if review == '':
360 review = None 466 review = None
361 return _XmlRemote(name, fetch, review) 467 manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
468 return _XmlRemote(name, alias, fetch, manifestUrl, review)
362 469
363 def _ParseDefault(self, node): 470 def _ParseDefault(self, node):
364 """ 471 """
@@ -369,6 +476,18 @@ class XmlManifest(Manifest):
369 d.revisionExpr = node.getAttribute('revision') 476 d.revisionExpr = node.getAttribute('revision')
370 if d.revisionExpr == '': 477 if d.revisionExpr == '':
371 d.revisionExpr = None 478 d.revisionExpr = None
479
480 sync_j = node.getAttribute('sync-j')
481 if sync_j == '' or sync_j is None:
482 d.sync_j = 1
483 else:
484 d.sync_j = int(sync_j)
485
486 sync_c = node.getAttribute('sync-c')
487 if not sync_c:
488 d.sync_c = False
489 else:
490 d.sync_c = sync_c.lower() in ("yes", "true", "1")
372 return d 491 return d
373 492
374 def _ParseNotice(self, node): 493 def _ParseNotice(self, node):
@@ -422,7 +541,7 @@ class XmlManifest(Manifest):
422 if remote is None: 541 if remote is None:
423 raise ManifestParseError, \ 542 raise ManifestParseError, \
424 "no remote for project %s within %s" % \ 543 "no remote for project %s within %s" % \
425 (name, self._manifestFile) 544 (name, self.manifestFile)
426 545
427 revisionExpr = node.getAttribute('revision') 546 revisionExpr = node.getAttribute('revision')
428 if not revisionExpr: 547 if not revisionExpr:
@@ -430,7 +549,7 @@ class XmlManifest(Manifest):
430 if not revisionExpr: 549 if not revisionExpr:
431 raise ManifestParseError, \ 550 raise ManifestParseError, \
432 "no revision for project %s within %s" % \ 551 "no revision for project %s within %s" % \
433 (name, self._manifestFile) 552 (name, self.manifestFile)
434 553
435 path = node.getAttribute('path') 554 path = node.getAttribute('path')
436 if not path: 555 if not path:
@@ -438,7 +557,27 @@ class XmlManifest(Manifest):
438 if path.startswith('/'): 557 if path.startswith('/'):
439 raise ManifestParseError, \ 558 raise ManifestParseError, \
440 "project %s path cannot be absolute in %s" % \ 559 "project %s path cannot be absolute in %s" % \
441 (name, self._manifestFile) 560 (name, self.manifestFile)
561
562 rebase = node.getAttribute('rebase')
563 if not rebase:
564 rebase = True
565 else:
566 rebase = rebase.lower() in ("yes", "true", "1")
567
568 sync_c = node.getAttribute('sync-c')
569 if not sync_c:
570 sync_c = False
571 else:
572 sync_c = sync_c.lower() in ("yes", "true", "1")
573
574 groups = ''
575 if node.hasAttribute('groups'):
576 groups = node.getAttribute('groups')
577 groups = [x for x in re.split('[,\s]+', groups) if x]
578
579 default_groups = ['default', 'name:%s' % name, 'path:%s' % path]
580 groups.extend(set(default_groups).difference(groups))
442 581
443 if self.IsMirror: 582 if self.IsMirror:
444 relpath = None 583 relpath = None
@@ -455,11 +594,16 @@ class XmlManifest(Manifest):
455 worktree = worktree, 594 worktree = worktree,
456 relpath = path, 595 relpath = path,
457 revisionExpr = revisionExpr, 596 revisionExpr = revisionExpr,
458 revisionId = None) 597 revisionId = None,
598 rebase = rebase,
599 groups = groups,
600 sync_c = sync_c)
459 601
460 for n in node.childNodes: 602 for n in node.childNodes:
461 if n.nodeName == 'copyfile': 603 if n.nodeName == 'copyfile':
462 self._ParseCopyFile(project, n) 604 self._ParseCopyFile(project, n)
605 if n.nodeName == 'annotation':
606 self._ParseAnnotation(project, n)
463 607
464 return project 608 return project
465 609
@@ -471,6 +615,17 @@ class XmlManifest(Manifest):
471 # dest is relative to the top of the tree 615 # dest is relative to the top of the tree
472 project.AddCopyFile(src, dest, os.path.join(self.topdir, dest)) 616 project.AddCopyFile(src, dest, os.path.join(self.topdir, dest))
473 617
618 def _ParseAnnotation(self, project, node):
619 name = self._reqatt(node, 'name')
620 value = self._reqatt(node, 'value')
621 try:
622 keep = self._reqatt(node, 'keep').lower()
623 except ManifestParseError:
624 keep = "true"
625 if keep != "true" and keep != "false":
626 raise ManifestParseError, "optional \"keep\" attribute must be \"true\" or \"false\""
627 project.AddAnnotation(name, value, keep)
628
474 def _get_remote(self, node): 629 def _get_remote(self, node):
475 name = node.getAttribute('remote') 630 name = node.getAttribute('remote')
476 if not name: 631 if not name:
@@ -480,7 +635,7 @@ class XmlManifest(Manifest):
480 if not v: 635 if not v:
481 raise ManifestParseError, \ 636 raise ManifestParseError, \
482 "remote %s not defined in %s" % \ 637 "remote %s not defined in %s" % \
483 (name, self._manifestFile) 638 (name, self.manifestFile)
484 return v 639 return v
485 640
486 def _reqatt(self, node, attname): 641 def _reqatt(self, node, attname):
@@ -491,5 +646,5 @@ class XmlManifest(Manifest):
491 if not v: 646 if not v:
492 raise ManifestParseError, \ 647 raise ManifestParseError, \
493 "no %s in <%s> within %s" % \ 648 "no %s in <%s> within %s" % \
494 (attname, node.nodeName, self._manifestFile) 649 (attname, node.nodeName, self.manifestFile)
495 return v 650 return v
diff --git a/progress.py b/progress.py
index 2ace7010..d948654f 100644
--- a/progress.py
+++ b/progress.py
@@ -21,13 +21,14 @@ from trace import IsTrace
21_NOT_TTY = not os.isatty(2) 21_NOT_TTY = not os.isatty(2)
22 22
23class Progress(object): 23class Progress(object):
24 def __init__(self, title, total=0): 24 def __init__(self, title, total=0, units=''):
25 self._title = title 25 self._title = title
26 self._total = total 26 self._total = total
27 self._done = 0 27 self._done = 0
28 self._lastp = -1 28 self._lastp = -1
29 self._start = time() 29 self._start = time()
30 self._show = False 30 self._show = False
31 self._units = units
31 32
32 def update(self, inc=1): 33 def update(self, inc=1):
33 self._done += inc 34 self._done += inc
@@ -51,11 +52,11 @@ class Progress(object):
51 52
52 if self._lastp != p: 53 if self._lastp != p:
53 self._lastp = p 54 self._lastp = p
54 sys.stderr.write('\r%s: %3d%% (%d/%d) ' % ( 55 sys.stderr.write('\r%s: %3d%% (%d%s/%d%s) ' % (
55 self._title, 56 self._title,
56 p, 57 p,
57 self._done, 58 self._done, self._units,
58 self._total)) 59 self._total, self._units))
59 sys.stderr.flush() 60 sys.stderr.flush()
60 61
61 def end(self): 62 def end(self):
@@ -69,9 +70,9 @@ class Progress(object):
69 sys.stderr.flush() 70 sys.stderr.flush()
70 else: 71 else:
71 p = (100 * self._done) / self._total 72 p = (100 * self._done) / self._total
72 sys.stderr.write('\r%s: %3d%% (%d/%d), done. \n' % ( 73 sys.stderr.write('\r%s: %3d%% (%d%s/%d%s), done. \n' % (
73 self._title, 74 self._title,
74 p, 75 p,
75 self._done, 76 self._done, self._units,
76 self._total)) 77 self._total, self._units))
77 sys.stderr.flush() 78 sys.stderr.flush()
diff --git a/project.py b/project.py
index b4044943..00ebb17f 100644
--- a/project.py
+++ b/project.py
@@ -12,22 +12,28 @@
12# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
13# limitations under the License. 13# limitations under the License.
14 14
15import traceback
15import errno 16import errno
16import filecmp 17import filecmp
17import os 18import os
19import random
18import re 20import re
19import shutil 21import shutil
20import stat 22import stat
23import subprocess
21import sys 24import sys
22import urllib2 25import time
23 26
24from color import Coloring 27from color import Coloring
25from git_command import GitCommand 28from git_command import GitCommand
26from git_config import GitConfig, IsId 29from git_config import GitConfig, IsId, GetSchemeFromUrl, ID_RE
27from error import GitError, ImportError, UploadError 30from error import DownloadError
31from error import GitError, HookError, ImportError, UploadError
28from error import ManifestInvalidRevisionError 32from error import ManifestInvalidRevisionError
33from progress import Progress
34from trace import IsTrace, Trace
29 35
30from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB 36from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
31 37
32def _lwrite(path, content): 38def _lwrite(path, content):
33 lock = '%s.lock' % path 39 lock = '%s.lock' % path
@@ -54,29 +60,25 @@ def not_rev(r):
54def sq(r): 60def sq(r):
55 return "'" + r.replace("'", "'\''") + "'" 61 return "'" + r.replace("'", "'\''") + "'"
56 62
57hook_list = None 63_project_hook_list = None
58def repo_hooks(): 64def _ProjectHooks():
59 global hook_list 65 """List the hooks present in the 'hooks' directory.
60 if hook_list is None: 66
61 d = os.path.abspath(os.path.dirname(__file__)) 67 These hooks are project hooks and are copied to the '.git/hooks' directory
62 d = os.path.join(d , 'hooks') 68 of all subprojects.
63 hook_list = map(lambda x: os.path.join(d, x), os.listdir(d))
64 return hook_list
65 69
66def relpath(dst, src): 70 This function caches the list of hooks (based on the contents of the
67 src = os.path.dirname(src) 71 'repo/hooks' directory) on the first call.
68 top = os.path.commonprefix([dst, src])
69 if top.endswith('/'):
70 top = top[:-1]
71 else:
72 top = os.path.dirname(top)
73 72
74 tmp = src 73 Returns:
75 rel = '' 74 A list of absolute paths to all of the files in the hooks directory.
76 while top != tmp: 75 """
77 rel += '../' 76 global _project_hook_list
78 tmp = os.path.dirname(tmp) 77 if _project_hook_list is None:
79 return rel + dst[len(top) + 1:] 78 d = os.path.abspath(os.path.dirname(__file__))
79 d = os.path.join(d , 'hooks')
80 _project_hook_list = map(lambda x: os.path.join(d, x), os.listdir(d))
81 return _project_hook_list
80 82
81 83
82class DownloadedChange(object): 84class DownloadedChange(object):
@@ -148,10 +150,11 @@ class ReviewableBranch(object):
148 R_HEADS + self.name, 150 R_HEADS + self.name,
149 '--') 151 '--')
150 152
151 def UploadForReview(self, people, auto_topic=False): 153 def UploadForReview(self, people, auto_topic=False, draft=False):
152 self.project.UploadForReview(self.name, 154 self.project.UploadForReview(self.name,
153 people, 155 people,
154 auto_topic=auto_topic) 156 auto_topic=auto_topic,
157 draft=draft)
155 158
156 def GetPublishedRefs(self): 159 def GetPublishedRefs(self):
157 refs = {} 160 refs = {}
@@ -185,6 +188,11 @@ class DiffColoring(Coloring):
185 Coloring.__init__(self, config, 'diff') 188 Coloring.__init__(self, config, 'diff')
186 self.project = self.printer('header', attr = 'bold') 189 self.project = self.printer('header', attr = 'bold')
187 190
191class _Annotation:
192 def __init__(self, name, value, keep):
193 self.name = name
194 self.value = value
195 self.keep = keep
188 196
189class _CopyFile: 197class _CopyFile:
190 def __init__(self, src, dest, abssrc, absdest): 198 def __init__(self, src, dest, abssrc, absdest):
@@ -223,6 +231,249 @@ class RemoteSpec(object):
223 self.url = url 231 self.url = url
224 self.review = review 232 self.review = review
225 233
234class RepoHook(object):
235 """A RepoHook contains information about a script to run as a hook.
236
237 Hooks are used to run a python script before running an upload (for instance,
238 to run presubmit checks). Eventually, we may have hooks for other actions.
239
240 This shouldn't be confused with files in the 'repo/hooks' directory. Those
241 files are copied into each '.git/hooks' folder for each project. Repo-level
242 hooks are associated instead with repo actions.
243
244 Hooks are always python. When a hook is run, we will load the hook into the
245 interpreter and execute its main() function.
246 """
247 def __init__(self,
248 hook_type,
249 hooks_project,
250 topdir,
251 abort_if_user_denies=False):
252 """RepoHook constructor.
253
254 Params:
255 hook_type: A string representing the type of hook. This is also used
256 to figure out the name of the file containing the hook. For
257 example: 'pre-upload'.
258 hooks_project: The project containing the repo hooks. If you have a
259 manifest, this is manifest.repo_hooks_project. OK if this is None,
260 which will make the hook a no-op.
261 topdir: Repo's top directory (the one containing the .repo directory).
262 Scripts will run with CWD as this directory. If you have a manifest,
263 this is manifest.topdir
264 abort_if_user_denies: If True, we'll throw a HookError() if the user
265 doesn't allow us to run the hook.
266 """
267 self._hook_type = hook_type
268 self._hooks_project = hooks_project
269 self._topdir = topdir
270 self._abort_if_user_denies = abort_if_user_denies
271
272 # Store the full path to the script for convenience.
273 if self._hooks_project:
274 self._script_fullpath = os.path.join(self._hooks_project.worktree,
275 self._hook_type + '.py')
276 else:
277 self._script_fullpath = None
278
279 def _GetHash(self):
280 """Return a hash of the contents of the hooks directory.
281
282 We'll just use git to do this. This hash has the property that if anything
283 changes in the directory we will return a different has.
284
285 SECURITY CONSIDERATION:
286 This hash only represents the contents of files in the hook directory, not
287 any other files imported or called by hooks. Changes to imported files
288 can change the script behavior without affecting the hash.
289
290 Returns:
291 A string representing the hash. This will always be ASCII so that it can
292 be printed to the user easily.
293 """
294 assert self._hooks_project, "Must have hooks to calculate their hash."
295
296 # We will use the work_git object rather than just calling GetRevisionId().
297 # That gives us a hash of the latest checked in version of the files that
298 # the user will actually be executing. Specifically, GetRevisionId()
299 # doesn't appear to change even if a user checks out a different version
300 # of the hooks repo (via git checkout) nor if a user commits their own revs.
301 #
302 # NOTE: Local (non-committed) changes will not be factored into this hash.
303 # I think this is OK, since we're really only worried about warning the user
304 # about upstream changes.
305 return self._hooks_project.work_git.rev_parse('HEAD')
306
307 def _GetMustVerb(self):
308 """Return 'must' if the hook is required; 'should' if not."""
309 if self._abort_if_user_denies:
310 return 'must'
311 else:
312 return 'should'
313
314 def _CheckForHookApproval(self):
315 """Check to see whether this hook has been approved.
316
317 We'll look at the hash of all of the hooks. If this matches the hash that
318 the user last approved, we're done. If it doesn't, we'll ask the user
319 about approval.
320
321 Note that we ask permission for each individual hook even though we use
322 the hash of all hooks when detecting changes. We'd like the user to be
323 able to approve / deny each hook individually. We only use the hash of all
324 hooks because there is no other easy way to detect changes to local imports.
325
326 Returns:
327 True if this hook is approved to run; False otherwise.
328
329 Raises:
330 HookError: Raised if the user doesn't approve and abort_if_user_denies
331 was passed to the consturctor.
332 """
333 hooks_dir = self._hooks_project.worktree
334 hooks_config = self._hooks_project.config
335 git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type
336
337 # Get the last hash that the user approved for this hook; may be None.
338 old_hash = hooks_config.GetString(git_approval_key)
339
340 # Get the current hash so we can tell if scripts changed since approval.
341 new_hash = self._GetHash()
342
343 if old_hash is not None:
344 # User previously approved hook and asked not to be prompted again.
345 if new_hash == old_hash:
346 # Approval matched. We're done.
347 return True
348 else:
349 # Give the user a reason why we're prompting, since they last told
350 # us to "never ask again".
351 prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % (
352 self._hook_type)
353 else:
354 prompt = ''
355
356 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
357 if sys.stdout.isatty():
358 prompt += ('Repo %s run the script:\n'
359 ' %s\n'
360 '\n'
361 'Do you want to allow this script to run '
362 '(yes/yes-never-ask-again/NO)? ') % (
363 self._GetMustVerb(), self._script_fullpath)
364 response = raw_input(prompt).lower()
365 print
366
367 # User is doing a one-time approval.
368 if response in ('y', 'yes'):
369 return True
370 elif response == 'yes-never-ask-again':
371 hooks_config.SetString(git_approval_key, new_hash)
372 return True
373
374 # For anything else, we'll assume no approval.
375 if self._abort_if_user_denies:
376 raise HookError('You must allow the %s hook or use --no-verify.' %
377 self._hook_type)
378
379 return False
380
381 def _ExecuteHook(self, **kwargs):
382 """Actually execute the given hook.
383
384 This will run the hook's 'main' function in our python interpreter.
385
386 Args:
387 kwargs: Keyword arguments to pass to the hook. These are often specific
388 to the hook type. For instance, pre-upload hooks will contain
389 a project_list.
390 """
391 # Keep sys.path and CWD stashed away so that we can always restore them
392 # upon function exit.
393 orig_path = os.getcwd()
394 orig_syspath = sys.path
395
396 try:
397 # Always run hooks with CWD as topdir.
398 os.chdir(self._topdir)
399
400 # Put the hook dir as the first item of sys.path so hooks can do
401 # relative imports. We want to replace the repo dir as [0] so
402 # hooks can't import repo files.
403 sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
404
405 # Exec, storing global context in the context dict. We catch exceptions
406 # and convert to a HookError w/ just the failing traceback.
407 context = {}
408 try:
409 execfile(self._script_fullpath, context)
410 except Exception:
411 raise HookError('%s\nFailed to import %s hook; see traceback above.' % (
412 traceback.format_exc(), self._hook_type))
413
414 # Running the script should have defined a main() function.
415 if 'main' not in context:
416 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
417
418
419 # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
420 # We don't actually want hooks to define their main with this argument--
421 # it's there to remind them that their hook should always take **kwargs.
422 # For instance, a pre-upload hook should be defined like:
423 # def main(project_list, **kwargs):
424 #
425 # This allows us to later expand the API without breaking old hooks.
426 kwargs = kwargs.copy()
427 kwargs['hook_should_take_kwargs'] = True
428
429 # Call the main function in the hook. If the hook should cause the
430 # build to fail, it will raise an Exception. We'll catch that convert
431 # to a HookError w/ just the failing traceback.
432 try:
433 context['main'](**kwargs)
434 except Exception:
435 raise HookError('%s\nFailed to run main() for %s hook; see traceback '
436 'above.' % (
437 traceback.format_exc(), self._hook_type))
438 finally:
439 # Restore sys.path and CWD.
440 sys.path = orig_syspath
441 os.chdir(orig_path)
442
443 def Run(self, user_allows_all_hooks, **kwargs):
444 """Run the hook.
445
446 If the hook doesn't exist (because there is no hooks project or because
447 this particular hook is not enabled), this is a no-op.
448
449 Args:
450 user_allows_all_hooks: If True, we will never prompt about running the
451 hook--we'll just assume it's OK to run it.
452 kwargs: Keyword arguments to pass to the hook. These are often specific
453 to the hook type. For instance, pre-upload hooks will contain
454 a project_list.
455
456 Raises:
457 HookError: If there was a problem finding the hook or the user declined
458 to run a required hook (from _CheckForHookApproval).
459 """
460 # No-op if there is no hooks project or if hook is disabled.
461 if ((not self._hooks_project) or
462 (self._hook_type not in self._hooks_project.enabled_repo_hooks)):
463 return
464
465 # Bail with a nice error if we can't find the hook.
466 if not os.path.isfile(self._script_fullpath):
467 raise HookError('Couldn\'t find repo hook: "%s"' % self._script_fullpath)
468
469 # Make sure the user is OK with running the hook.
470 if (not user_allows_all_hooks) and (not self._CheckForHookApproval()):
471 return
472
473 # Run the hook with the same version of python we're using.
474 self._ExecuteHook(**kwargs)
475
476
226class Project(object): 477class Project(object):
227 def __init__(self, 478 def __init__(self,
228 manifest, 479 manifest,
@@ -232,7 +483,10 @@ class Project(object):
232 worktree, 483 worktree,
233 relpath, 484 relpath,
234 revisionExpr, 485 revisionExpr,
235 revisionId): 486 revisionId,
487 rebase = True,
488 groups = None,
489 sync_c = False):
236 self.manifest = manifest 490 self.manifest = manifest
237 self.name = name 491 self.name = name
238 self.remote = remote 492 self.remote = remote
@@ -251,8 +505,13 @@ class Project(object):
251 else: 505 else:
252 self.revisionId = revisionId 506 self.revisionId = revisionId
253 507
508 self.rebase = rebase
509 self.groups = groups
510 self.sync_c = sync_c
511
254 self.snapshots = {} 512 self.snapshots = {}
255 self.copyfiles = [] 513 self.copyfiles = []
514 self.annotations = []
256 self.config = GitConfig.ForRepository( 515 self.config = GitConfig.ForRepository(
257 gitdir = self.gitdir, 516 gitdir = self.gitdir,
258 defaults = self.manifest.globalConfig) 517 defaults = self.manifest.globalConfig)
@@ -264,6 +523,10 @@ class Project(object):
264 self.bare_git = self._GitGetByExec(self, bare=True) 523 self.bare_git = self._GitGetByExec(self, bare=True)
265 self.bare_ref = GitRefs(gitdir) 524 self.bare_ref = GitRefs(gitdir)
266 525
526 # This will be filled in if a project is later identified to be the
527 # project containing repo hooks.
528 self.enabled_repo_hooks = []
529
267 @property 530 @property
268 def Exists(self): 531 def Exists(self):
269 return os.path.isdir(self.gitdir) 532 return os.path.isdir(self.gitdir)
@@ -367,6 +630,27 @@ class Project(object):
367 630
368 return heads 631 return heads
369 632
633 def MatchesGroups(self, manifest_groups):
634 """Returns true if the manifest groups specified at init should cause
635 this project to be synced.
636 Prefixing a manifest group with "-" inverts the meaning of a group.
637 All projects are implicitly labelled with "default".
638
639 labels are resolved in order. In the example case of
640 project_groups: "default,group1,group2"
641 manifest_groups: "-group1,group2"
642 the project will be matched.
643 """
644 if self.groups is None:
645 return True
646 matched = False
647 for group in manifest_groups:
648 if group.startswith('-') and group[1:] in self.groups:
649 matched = False
650 elif group in self.groups:
651 matched = True
652
653 return matched
370 654
371## Status Display ## 655## Status Display ##
372 656
@@ -391,13 +675,18 @@ class Project(object):
391 675
392 return False 676 return False
393 677
394 def PrintWorkTreeStatus(self): 678 def PrintWorkTreeStatus(self, output_redir=None):
395 """Prints the status of the repository to stdout. 679 """Prints the status of the repository to stdout.
680
681 Args:
682 output: If specified, redirect the output to this object.
396 """ 683 """
397 if not os.path.isdir(self.worktree): 684 if not os.path.isdir(self.worktree):
398 print '' 685 if output_redir == None:
399 print 'project %s/' % self.relpath 686 output_redir = sys.stdout
400 print ' missing (run "repo sync")' 687 print >>output_redir, ''
688 print >>output_redir, 'project %s/' % self.relpath
689 print >>output_redir, ' missing (run "repo sync")'
401 return 690 return
402 691
403 self.work_git.update_index('-q', 692 self.work_git.update_index('-q',
@@ -408,10 +697,12 @@ class Project(object):
408 di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD) 697 di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD)
409 df = self.work_git.DiffZ('diff-files') 698 df = self.work_git.DiffZ('diff-files')
410 do = self.work_git.LsOthers() 699 do = self.work_git.LsOthers()
411 if not rb and not di and not df and not do: 700 if not rb and not di and not df and not do and not self.CurrentBranch:
412 return 'CLEAN' 701 return 'CLEAN'
413 702
414 out = StatusColoring(self.config) 703 out = StatusColoring(self.config)
704 if not output_redir == None:
705 out.redirect(output_redir)
415 out.project('project %-40s', self.relpath + '/') 706 out.project('project %-40s', self.relpath + '/')
416 707
417 branch = self.CurrentBranch 708 branch = self.CurrentBranch
@@ -461,9 +752,10 @@ class Project(object):
461 else: 752 else:
462 out.write('%s', line) 753 out.write('%s', line)
463 out.nl() 754 out.nl()
755
464 return 'DIRTY' 756 return 'DIRTY'
465 757
466 def PrintWorkTreeDiff(self): 758 def PrintWorkTreeDiff(self, absolute_paths=False):
467 """Prints the status of the repository to stdout. 759 """Prints the status of the repository to stdout.
468 """ 760 """
469 out = DiffColoring(self.config) 761 out = DiffColoring(self.config)
@@ -471,6 +763,9 @@ class Project(object):
471 if out.is_on: 763 if out.is_on:
472 cmd.append('--color') 764 cmd.append('--color')
473 cmd.append(HEAD) 765 cmd.append(HEAD)
766 if absolute_paths:
767 cmd.append('--src-prefix=a/%s/' % self.relpath)
768 cmd.append('--dst-prefix=b/%s/' % self.relpath)
474 cmd.append('--') 769 cmd.append('--')
475 p = GitCommand(self, 770 p = GitCommand(self,
476 cmd, 771 cmd,
@@ -524,7 +819,7 @@ class Project(object):
524 if R_HEADS + n not in heads: 819 if R_HEADS + n not in heads:
525 self.bare_git.DeleteRef(name, id) 820 self.bare_git.DeleteRef(name, id)
526 821
527 def GetUploadableBranches(self): 822 def GetUploadableBranches(self, selected_branch=None):
528 """List any branches which can be uploaded for review. 823 """List any branches which can be uploaded for review.
529 """ 824 """
530 heads = {} 825 heads = {}
@@ -540,6 +835,8 @@ class Project(object):
540 for branch, id in heads.iteritems(): 835 for branch, id in heads.iteritems():
541 if branch in pubed and pubed[branch] == id: 836 if branch in pubed and pubed[branch] == id:
542 continue 837 continue
838 if selected_branch and branch != selected_branch:
839 continue
543 840
544 rb = self.GetUploadableBranch(branch) 841 rb = self.GetUploadableBranch(branch)
545 if rb: 842 if rb:
@@ -559,7 +856,8 @@ class Project(object):
559 856
560 def UploadForReview(self, branch=None, 857 def UploadForReview(self, branch=None,
561 people=([],[]), 858 people=([],[]),
562 auto_topic=False): 859 auto_topic=False,
860 draft=False):
563 """Uploads the named branch for code review. 861 """Uploads the named branch for code review.
564 """ 862 """
565 if branch is None: 863 if branch is None:
@@ -581,31 +879,36 @@ class Project(object):
581 branch.remote.projectname = self.name 879 branch.remote.projectname = self.name
582 branch.remote.Save() 880 branch.remote.Save()
583 881
584 if branch.remote.ReviewProtocol == 'ssh': 882 url = branch.remote.ReviewUrl(self.UserEmail)
585 if dest_branch.startswith(R_HEADS): 883 if url is None:
586 dest_branch = dest_branch[len(R_HEADS):] 884 raise UploadError('review not configured')
885 cmd = ['push']
587 886
887 if url.startswith('ssh://'):
588 rp = ['gerrit receive-pack'] 888 rp = ['gerrit receive-pack']
589 for e in people[0]: 889 for e in people[0]:
590 rp.append('--reviewer=%s' % sq(e)) 890 rp.append('--reviewer=%s' % sq(e))
591 for e in people[1]: 891 for e in people[1]:
592 rp.append('--cc=%s' % sq(e)) 892 rp.append('--cc=%s' % sq(e))
893 cmd.append('--receive-pack=%s' % " ".join(rp))
593 894
594 ref_spec = '%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch) 895 cmd.append(url)
595 if auto_topic:
596 ref_spec = ref_spec + '/' + branch.name
597 896
598 cmd = ['push'] 897 if dest_branch.startswith(R_HEADS):
599 cmd.append('--receive-pack=%s' % " ".join(rp)) 898 dest_branch = dest_branch[len(R_HEADS):]
600 cmd.append(branch.remote.SshReviewUrl(self.UserEmail))
601 cmd.append(ref_spec)
602 899
603 if GitCommand(self, cmd, bare = True).Wait() != 0: 900 upload_type = 'for'
604 raise UploadError('Upload failed') 901 if draft:
902 upload_type = 'drafts'
605 903
606 else: 904 ref_spec = '%s:refs/%s/%s' % (R_HEADS + branch.name, upload_type,
607 raise UploadError('Unsupported protocol %s' \ 905 dest_branch)
608 % branch.remote.review) 906 if auto_topic:
907 ref_spec = ref_spec + '/' + branch.name
908 cmd.append(ref_spec)
909
910 if GitCommand(self, cmd, bare = True).Wait() != 0:
911 raise UploadError('Upload failed')
609 912
610 msg = "posted to %s for %s" % (branch.remote.review, dest_branch) 913 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
611 self.bare_git.UpdateRef(R_PUB + branch.name, 914 self.bare_git.UpdateRef(R_PUB + branch.name,
@@ -615,35 +918,53 @@ class Project(object):
615 918
616## Sync ## 919## Sync ##
617 920
618 def Sync_NetworkHalf(self, quiet=False): 921 def Sync_NetworkHalf(self,
922 quiet=False,
923 is_new=None,
924 current_branch_only=False,
925 clone_bundle=True):
619 """Perform only the network IO portion of the sync process. 926 """Perform only the network IO portion of the sync process.
620 Local working directory/branch state is not affected. 927 Local working directory/branch state is not affected.
621 """ 928 """
622 is_new = not self.Exists 929 if is_new is None:
930 is_new = not self.Exists
623 if is_new: 931 if is_new:
624 if not quiet:
625 print >>sys.stderr
626 print >>sys.stderr, 'Initializing project %s ...' % self.name
627 self._InitGitDir() 932 self._InitGitDir()
628
629 self._InitRemote() 933 self._InitRemote()
630 if not self._RemoteFetch(initial=is_new, quiet=quiet):
631 return False
632 934
633 #Check that the requested ref was found after fetch 935 if is_new:
634 # 936 alt = os.path.join(self.gitdir, 'objects/info/alternates')
635 try: 937 try:
636 self.GetRevisionId() 938 fd = open(alt, 'rb')
637 except ManifestInvalidRevisionError: 939 try:
638 # if the ref is a tag. We can try fetching 940 alt_dir = fd.readline().rstrip()
639 # the tag manually as a last resort 941 finally:
640 # 942 fd.close()
641 rev = self.revisionExpr 943 except IOError:
642 if rev.startswith(R_TAGS): 944 alt_dir = None
643 self._RemoteFetch(None, rev[len(R_TAGS):], quiet=quiet) 945 else:
946 alt_dir = None
947
948 if clone_bundle \
949 and alt_dir is None \
950 and self._ApplyCloneBundle(initial=is_new, quiet=quiet):
951 is_new = False
952
953 if not current_branch_only:
954 if self.sync_c:
955 current_branch_only = True
956 elif not self.manifest._loaded:
957 # Manifest cannot check defaults until it syncs.
958 current_branch_only = False
959 elif self.manifest.default.sync_c:
960 current_branch_only = True
961
962 if not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir,
963 current_branch_only=current_branch_only):
964 return False
644 965
645 if self.worktree: 966 if self.worktree:
646 self.manifest.SetMRefs(self) 967 self._InitMRef()
647 else: 968 else:
648 self._InitMirrorHead() 969 self._InitMirrorHead()
649 try: 970 try:
@@ -680,11 +1001,11 @@ class Project(object):
680 """Perform only the local IO portion of the sync process. 1001 """Perform only the local IO portion of the sync process.
681 Network access is not required. 1002 Network access is not required.
682 """ 1003 """
683 self._InitWorkTree()
684 all = self.bare_ref.all 1004 all = self.bare_ref.all
685 self.CleanPublishedCache(all) 1005 self.CleanPublishedCache(all)
686
687 revid = self.GetRevisionId(all) 1006 revid = self.GetRevisionId(all)
1007
1008 self._InitWorkTree()
688 head = self.work_git.GetHead() 1009 head = self.work_git.GetHead()
689 if head.startswith(R_HEADS): 1010 if head.startswith(R_HEADS):
690 branch = head[len(R_HEADS):] 1011 branch = head[len(R_HEADS):]
@@ -705,12 +1026,15 @@ class Project(object):
705 1026
706 if head == revid: 1027 if head == revid:
707 # No changes; don't do anything further. 1028 # No changes; don't do anything further.
1029 # Except if the head needs to be detached
708 # 1030 #
709 return 1031 if not syncbuf.detach_head:
1032 return
1033 else:
1034 lost = self._revlist(not_rev(revid), HEAD)
1035 if lost:
1036 syncbuf.info(self, "discarding %d commits", len(lost))
710 1037
711 lost = self._revlist(not_rev(revid), HEAD)
712 if lost:
713 syncbuf.info(self, "discarding %d commits", len(lost))
714 try: 1038 try:
715 self._Checkout(revid, quiet=True) 1039 self._Checkout(revid, quiet=True)
716 except GitError, e: 1040 except GitError, e:
@@ -728,7 +1052,7 @@ class Project(object):
728 1052
729 if not branch.LocalMerge: 1053 if not branch.LocalMerge:
730 # The current branch has no tracking configuration. 1054 # The current branch has no tracking configuration.
731 # Jump off it to a deatched HEAD. 1055 # Jump off it to a detached HEAD.
732 # 1056 #
733 syncbuf.info(self, 1057 syncbuf.info(self,
734 "leaving %s; does not track upstream", 1058 "leaving %s; does not track upstream",
@@ -806,10 +1130,12 @@ class Project(object):
806 len(local_changes) - cnt_mine) 1130 len(local_changes) - cnt_mine)
807 1131
808 branch.remote = self.GetRemote(self.remote.name) 1132 branch.remote = self.GetRemote(self.remote.name)
809 branch.merge = self.revisionExpr 1133 if not ID_RE.match(self.revisionExpr):
1134 # in case of manifest sync the revisionExpr might be a SHA1
1135 branch.merge = self.revisionExpr
810 branch.Save() 1136 branch.Save()
811 1137
812 if cnt_mine > 0: 1138 if cnt_mine > 0 and self.rebase:
813 def _dorebase(): 1139 def _dorebase():
814 self._Rebase(upstream = '%s^1' % last_mine, onto = revid) 1140 self._Rebase(upstream = '%s^1' % last_mine, onto = revid)
815 self._CopyFiles() 1141 self._CopyFiles()
@@ -833,6 +1159,9 @@ class Project(object):
833 abssrc = os.path.join(self.worktree, src) 1159 abssrc = os.path.join(self.worktree, src)
834 self.copyfiles.append(_CopyFile(src, dest, abssrc, absdest)) 1160 self.copyfiles.append(_CopyFile(src, dest, abssrc, absdest))
835 1161
1162 def AddAnnotation(self, name, value, keep):
1163 self.annotations.append(_Annotation(name, value, keep))
1164
836 def DownloadPatchSet(self, change_id, patch_id): 1165 def DownloadPatchSet(self, change_id, patch_id):
837 """Download a single patch set of a single change to FETCH_HEAD. 1166 """Download a single patch set of a single change to FETCH_HEAD.
838 """ 1167 """
@@ -900,6 +1229,13 @@ class Project(object):
900 1229
901 def CheckoutBranch(self, name): 1230 def CheckoutBranch(self, name):
902 """Checkout a local topic branch. 1231 """Checkout a local topic branch.
1232
1233 Args:
1234 name: The name of the branch to checkout.
1235
1236 Returns:
1237 True if the checkout succeeded; False if it didn't; None if the branch
1238 didn't exist.
903 """ 1239 """
904 rev = R_HEADS + name 1240 rev = R_HEADS + name
905 head = self.work_git.GetHead() 1241 head = self.work_git.GetHead()
@@ -914,7 +1250,7 @@ class Project(object):
914 except KeyError: 1250 except KeyError:
915 # Branch does not exist in this project 1251 # Branch does not exist in this project
916 # 1252 #
917 return False 1253 return None
918 1254
919 if head.startswith(R_HEADS): 1255 if head.startswith(R_HEADS):
920 try: 1256 try:
@@ -937,13 +1273,19 @@ class Project(object):
937 1273
938 def AbandonBranch(self, name): 1274 def AbandonBranch(self, name):
939 """Destroy a local topic branch. 1275 """Destroy a local topic branch.
1276
1277 Args:
1278 name: The name of the branch to abandon.
1279
1280 Returns:
1281 True if the abandon succeeded; False if it didn't; None if the branch
1282 didn't exist.
940 """ 1283 """
941 rev = R_HEADS + name 1284 rev = R_HEADS + name
942 all = self.bare_ref.all 1285 all = self.bare_ref.all
943 if rev not in all: 1286 if rev not in all:
944 # Doesn't exist; assume already abandoned. 1287 # Doesn't exist
945 # 1288 return None
946 return True
947 1289
948 head = self.work_git.GetHead() 1290 head = self.work_git.GetHead()
949 if head == rev: 1291 if head == rev:
@@ -1023,31 +1365,43 @@ class Project(object):
1023 1365
1024## Direct Git Commands ## 1366## Direct Git Commands ##
1025 1367
1026 def _RemoteFetch(self, name=None, tag=None, 1368 def _RemoteFetch(self, name=None,
1369 current_branch_only=False,
1027 initial=False, 1370 initial=False,
1028 quiet=False): 1371 quiet=False,
1372 alt_dir=None):
1373
1374 is_sha1 = False
1375 tag_name = None
1376
1377 if current_branch_only:
1378 if ID_RE.match(self.revisionExpr) is not None:
1379 is_sha1 = True
1380 elif self.revisionExpr.startswith(R_TAGS):
1381 # this is a tag and its sha1 value should never change
1382 tag_name = self.revisionExpr[len(R_TAGS):]
1383
1384 if is_sha1 or tag_name is not None:
1385 try:
1386 # if revision (sha or tag) is not present then following function
1387 # throws an error.
1388 self.bare_git.rev_parse('--verify', '%s^0' % self.revisionExpr)
1389 return True
1390 except GitError:
1391 # There is no such persistent revision. We have to fetch it.
1392 pass
1393
1029 if not name: 1394 if not name:
1030 name = self.remote.name 1395 name = self.remote.name
1031 1396
1032 ssh_proxy = False 1397 ssh_proxy = False
1033 if self.GetRemote(name).PreConnectFetch(): 1398 remote = self.GetRemote(name)
1399 if remote.PreConnectFetch():
1034 ssh_proxy = True 1400 ssh_proxy = True
1035 1401
1036 if initial: 1402 if initial:
1037 alt = os.path.join(self.gitdir, 'objects/info/alternates') 1403 if alt_dir and 'objects' == os.path.basename(alt_dir):
1038 try: 1404 ref_dir = os.path.dirname(alt_dir)
1039 fd = open(alt, 'rb')
1040 try:
1041 ref_dir = fd.readline()
1042 if ref_dir and ref_dir.endswith('\n'):
1043 ref_dir = ref_dir[:-1]
1044 finally:
1045 fd.close()
1046 except IOError, e:
1047 ref_dir = None
1048
1049 if ref_dir and 'objects' == os.path.basename(ref_dir):
1050 ref_dir = os.path.dirname(ref_dir)
1051 packed_refs = os.path.join(self.gitdir, 'packed-refs') 1405 packed_refs = os.path.join(self.gitdir, 'packed-refs')
1052 remote = self.GetRemote(name) 1406 remote = self.GetRemote(name)
1053 1407
@@ -1083,35 +1437,130 @@ class Project(object):
1083 old_packed += line 1437 old_packed += line
1084 1438
1085 _lwrite(packed_refs, tmp_packed) 1439 _lwrite(packed_refs, tmp_packed)
1086
1087 else: 1440 else:
1088 ref_dir = None 1441 alt_dir = None
1089 1442
1090 cmd = ['fetch'] 1443 cmd = ['fetch']
1444
1445 # The --depth option only affects the initial fetch; after that we'll do
1446 # full fetches of changes.
1447 depth = self.manifest.manifestProject.config.GetString('repo.depth')
1448 if depth and initial:
1449 cmd.append('--depth=%s' % depth)
1450
1091 if quiet: 1451 if quiet:
1092 cmd.append('--quiet') 1452 cmd.append('--quiet')
1093 if not self.worktree: 1453 if not self.worktree:
1094 cmd.append('--update-head-ok') 1454 cmd.append('--update-head-ok')
1095 cmd.append(name) 1455 cmd.append(name)
1096 if tag is not None:
1097 cmd.append('tag')
1098 cmd.append(tag)
1099 1456
1100 ok = GitCommand(self, 1457 if not current_branch_only or is_sha1:
1101 cmd, 1458 # Fetch whole repo
1102 bare = True, 1459 cmd.append('--tags')
1103 ssh_proxy = ssh_proxy).Wait() == 0 1460 cmd.append((u'+refs/heads/*:') + remote.ToLocal('refs/heads/*'))
1461 elif tag_name is not None:
1462 cmd.append('tag')
1463 cmd.append(tag_name)
1464 else:
1465 branch = self.revisionExpr
1466 if branch.startswith(R_HEADS):
1467 branch = branch[len(R_HEADS):]
1468 cmd.append((u'+refs/heads/%s:' % branch) + remote.ToLocal('refs/heads/%s' % branch))
1469
1470 ok = False
1471 for i in range(2):
1472 if GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy).Wait() == 0:
1473 ok = True
1474 break
1475 time.sleep(random.randint(30, 45))
1104 1476
1105 if initial: 1477 if initial:
1106 if ref_dir: 1478 if alt_dir:
1107 if old_packed != '': 1479 if old_packed != '':
1108 _lwrite(packed_refs, old_packed) 1480 _lwrite(packed_refs, old_packed)
1109 else: 1481 else:
1110 os.remove(packed_refs) 1482 os.remove(packed_refs)
1111 self.bare_git.pack_refs('--all', '--prune') 1483 self.bare_git.pack_refs('--all', '--prune')
1484 return ok
1485
1486 def _ApplyCloneBundle(self, initial=False, quiet=False):
1487 if initial and self.manifest.manifestProject.config.GetString('repo.depth'):
1488 return False
1489
1490 remote = self.GetRemote(self.remote.name)
1491 bundle_url = remote.url + '/clone.bundle'
1492 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
1493 if GetSchemeFromUrl(bundle_url) in ('persistent-http', 'persistent-https'):
1494 bundle_url = bundle_url[len('persistent-'):]
1495 if GetSchemeFromUrl(bundle_url) not in ('http', 'https'):
1496 return False
1497
1498 bundle_dst = os.path.join(self.gitdir, 'clone.bundle')
1499 bundle_tmp = os.path.join(self.gitdir, 'clone.bundle.tmp')
1500
1501 exist_dst = os.path.exists(bundle_dst)
1502 exist_tmp = os.path.exists(bundle_tmp)
1503
1504 if not initial and not exist_dst and not exist_tmp:
1505 return False
1112 1506
1507 if not exist_dst:
1508 exist_dst = self._FetchBundle(bundle_url, bundle_tmp, bundle_dst, quiet)
1509 if not exist_dst:
1510 return False
1511
1512 cmd = ['fetch']
1513 if quiet:
1514 cmd.append('--quiet')
1515 if not self.worktree:
1516 cmd.append('--update-head-ok')
1517 cmd.append(bundle_dst)
1518 for f in remote.fetch:
1519 cmd.append(str(f))
1520 cmd.append('refs/tags/*:refs/tags/*')
1521
1522 ok = GitCommand(self, cmd, bare=True).Wait() == 0
1523 if os.path.exists(bundle_dst):
1524 os.remove(bundle_dst)
1525 if os.path.exists(bundle_tmp):
1526 os.remove(bundle_tmp)
1113 return ok 1527 return ok
1114 1528
1529 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet):
1530 if os.path.exists(dstPath):
1531 os.remove(dstPath)
1532
1533 cmd = ['curl', '--output', tmpPath, '--netrc', '--location']
1534 if quiet:
1535 cmd += ['--silent']
1536 if os.path.exists(tmpPath):
1537 size = os.stat(tmpPath).st_size
1538 if size >= 1024:
1539 cmd += ['--continue-at', '%d' % (size,)]
1540 else:
1541 os.remove(tmpPath)
1542 if 'http_proxy' in os.environ and 'darwin' == sys.platform:
1543 cmd += ['--proxy', os.environ['http_proxy']]
1544 cmd += [srcUrl]
1545
1546 if IsTrace():
1547 Trace('%s', ' '.join(cmd))
1548 try:
1549 proc = subprocess.Popen(cmd)
1550 except OSError:
1551 return False
1552
1553 ok = proc.wait() == 0
1554 if os.path.exists(tmpPath):
1555 if ok and os.stat(tmpPath).st_size > 16:
1556 os.rename(tmpPath, dstPath)
1557 return True
1558 else:
1559 os.remove(tmpPath)
1560 return False
1561 else:
1562 return False
1563
1115 def _Checkout(self, rev, quiet=False): 1564 def _Checkout(self, rev, quiet=False):
1116 cmd = ['checkout'] 1565 cmd = ['checkout']
1117 if quiet: 1566 if quiet:
@@ -1122,6 +1571,23 @@ class Project(object):
1122 if self._allrefs: 1571 if self._allrefs:
1123 raise GitError('%s checkout %s ' % (self.name, rev)) 1572 raise GitError('%s checkout %s ' % (self.name, rev))
1124 1573
1574 def _CherryPick(self, rev, quiet=False):
1575 cmd = ['cherry-pick']
1576 cmd.append(rev)
1577 cmd.append('--')
1578 if GitCommand(self, cmd).Wait() != 0:
1579 if self._allrefs:
1580 raise GitError('%s cherry-pick %s ' % (self.name, rev))
1581
1582 def _Revert(self, rev, quiet=False):
1583 cmd = ['revert']
1584 cmd.append('--no-edit')
1585 cmd.append(rev)
1586 cmd.append('--')
1587 if GitCommand(self, cmd).Wait() != 0:
1588 if self._allrefs:
1589 raise GitError('%s revert %s ' % (self.name, rev))
1590
1125 def _ResetHard(self, rev, quiet=True): 1591 def _ResetHard(self, rev, quiet=True):
1126 cmd = ['reset', '--hard'] 1592 cmd = ['reset', '--hard']
1127 if quiet: 1593 if quiet:
@@ -1138,8 +1604,10 @@ class Project(object):
1138 if GitCommand(self, cmd).Wait() != 0: 1604 if GitCommand(self, cmd).Wait() != 0:
1139 raise GitError('%s rebase %s ' % (self.name, upstream)) 1605 raise GitError('%s rebase %s ' % (self.name, upstream))
1140 1606
1141 def _FastForward(self, head): 1607 def _FastForward(self, head, ffonly=False):
1142 cmd = ['merge', head] 1608 cmd = ['merge', head]
1609 if ffonly:
1610 cmd.append("--ff-only")
1143 if GitCommand(self, cmd).Wait() != 0: 1611 if GitCommand(self, cmd).Wait() != 0:
1144 raise GitError('%s merge %s ' % (self.name, head)) 1612 raise GitError('%s merge %s ' % (self.name, head))
1145 1613
@@ -1192,13 +1660,16 @@ class Project(object):
1192 hooks = self._gitdir_path('hooks') 1660 hooks = self._gitdir_path('hooks')
1193 if not os.path.exists(hooks): 1661 if not os.path.exists(hooks):
1194 os.makedirs(hooks) 1662 os.makedirs(hooks)
1195 for stock_hook in repo_hooks(): 1663 for stock_hook in _ProjectHooks():
1196 name = os.path.basename(stock_hook) 1664 name = os.path.basename(stock_hook)
1197 1665
1198 if name in ('commit-msg') and not self.remote.review: 1666 if name in ('commit-msg',) and not self.remote.review \
1667 and not self is self.manifest.manifestProject:
1199 # Don't install a Gerrit Code Review hook if this 1668 # Don't install a Gerrit Code Review hook if this
1200 # project does not appear to use it for reviews. 1669 # project does not appear to use it for reviews.
1201 # 1670 #
1671 # Since the manifest project is one of those, but also
1672 # managed through gerrit, it's excluded
1202 continue 1673 continue
1203 1674
1204 dst = os.path.join(hooks, name) 1675 dst = os.path.join(hooks, name)
@@ -1211,7 +1682,7 @@ class Project(object):
1211 _error("%s: Not replacing %s hook", self.relpath, name) 1682 _error("%s: Not replacing %s hook", self.relpath, name)
1212 continue 1683 continue
1213 try: 1684 try:
1214 os.symlink(relpath(stock_hook, dst), dst) 1685 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
1215 except OSError, e: 1686 except OSError, e:
1216 if e.errno == errno.EPERM: 1687 if e.errno == errno.EPERM:
1217 raise GitError('filesystem must support symlinks') 1688 raise GitError('filesystem must support symlinks')
@@ -1231,6 +1702,10 @@ class Project(object):
1231 remote.ResetFetch(mirror=True) 1702 remote.ResetFetch(mirror=True)
1232 remote.Save() 1703 remote.Save()
1233 1704
1705 def _InitMRef(self):
1706 if self.manifest.branch:
1707 self._InitAnyMRef(R_M + self.manifest.branch)
1708
1234 def _InitMirrorHead(self): 1709 def _InitMirrorHead(self):
1235 self._InitAnyMRef(HEAD) 1710 self._InitAnyMRef(HEAD)
1236 1711
@@ -1249,40 +1724,33 @@ class Project(object):
1249 msg = 'manifest set to %s' % self.revisionExpr 1724 msg = 'manifest set to %s' % self.revisionExpr
1250 self.bare_git.symbolic_ref('-m', msg, ref, dst) 1725 self.bare_git.symbolic_ref('-m', msg, ref, dst)
1251 1726
1252 def _LinkWorkTree(self, relink=False):
1253 dotgit = os.path.join(self.worktree, '.git')
1254 if not relink:
1255 os.makedirs(dotgit)
1256
1257 for name in ['config',
1258 'description',
1259 'hooks',
1260 'info',
1261 'logs',
1262 'objects',
1263 'packed-refs',
1264 'refs',
1265 'rr-cache',
1266 'svn']:
1267 try:
1268 src = os.path.join(self.gitdir, name)
1269 dst = os.path.join(dotgit, name)
1270 if relink:
1271 os.remove(dst)
1272 if os.path.islink(dst) or not os.path.exists(dst):
1273 os.symlink(relpath(src, dst), dst)
1274 else:
1275 raise GitError('cannot overwrite a local work tree')
1276 except OSError, e:
1277 if e.errno == errno.EPERM:
1278 raise GitError('filesystem must support symlinks')
1279 else:
1280 raise
1281
1282 def _InitWorkTree(self): 1727 def _InitWorkTree(self):
1283 dotgit = os.path.join(self.worktree, '.git') 1728 dotgit = os.path.join(self.worktree, '.git')
1284 if not os.path.exists(dotgit): 1729 if not os.path.exists(dotgit):
1285 self._LinkWorkTree() 1730 os.makedirs(dotgit)
1731
1732 for name in ['config',
1733 'description',
1734 'hooks',
1735 'info',
1736 'logs',
1737 'objects',
1738 'packed-refs',
1739 'refs',
1740 'rr-cache',
1741 'svn']:
1742 try:
1743 src = os.path.join(self.gitdir, name)
1744 dst = os.path.join(dotgit, name)
1745 if os.path.islink(dst) or not os.path.exists(dst):
1746 os.symlink(os.path.relpath(src, os.path.dirname(dst)), dst)
1747 else:
1748 raise GitError('cannot overwrite a local work tree')
1749 except OSError, e:
1750 if e.errno == errno.EPERM:
1751 raise GitError('filesystem must support symlinks')
1752 else:
1753 raise
1286 1754
1287 _lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId()) 1755 _lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId())
1288 1756
@@ -1291,6 +1759,11 @@ class Project(object):
1291 cmd.append(HEAD) 1759 cmd.append(HEAD)
1292 if GitCommand(self, cmd).Wait() != 0: 1760 if GitCommand(self, cmd).Wait() != 0:
1293 raise GitError("cannot initialize work tree") 1761 raise GitError("cannot initialize work tree")
1762
1763 rr_cache = os.path.join(self.gitdir, 'rr-cache')
1764 if not os.path.exists(rr_cache):
1765 os.makedirs(rr_cache)
1766
1294 self._CopyFiles() 1767 self._CopyFiles()
1295 1768
1296 def _gitdir_path(self, path): 1769 def _gitdir_path(self, path):
@@ -1449,6 +1922,22 @@ class Project(object):
1449 return r 1922 return r
1450 1923
1451 def __getattr__(self, name): 1924 def __getattr__(self, name):
1925 """Allow arbitrary git commands using pythonic syntax.
1926
1927 This allows you to do things like:
1928 git_obj.rev_parse('HEAD')
1929
1930 Since we don't have a 'rev_parse' method defined, the __getattr__ will
1931 run. We'll replace the '_' with a '-' and try to run a git command.
1932 Any other arguments will be passed to the git command.
1933
1934 Args:
1935 name: The name of the git command to call. Any '_' characters will
1936 be replaced with '-'.
1937
1938 Returns:
1939 A callable object that will try to call git with the named command.
1940 """
1452 name = name.replace('_', '-') 1941 name = name.replace('_', '-')
1453 def runner(*args): 1942 def runner(*args):
1454 cmdv = [name] 1943 cmdv = [name]
@@ -1580,30 +2069,43 @@ class SyncBuffer(object):
1580class MetaProject(Project): 2069class MetaProject(Project):
1581 """A special project housed under .repo. 2070 """A special project housed under .repo.
1582 """ 2071 """
1583 def __init__(self, manifest, name, gitdir, worktree, relpath=None): 2072 def __init__(self, manifest, name, gitdir, worktree):
1584 repodir = manifest.repodir 2073 repodir = manifest.repodir
1585 if relpath is None:
1586 relpath = '.repo/%s' % name
1587 Project.__init__(self, 2074 Project.__init__(self,
1588 manifest = manifest, 2075 manifest = manifest,
1589 name = name, 2076 name = name,
1590 gitdir = gitdir, 2077 gitdir = gitdir,
1591 worktree = worktree, 2078 worktree = worktree,
1592 remote = RemoteSpec('origin'), 2079 remote = RemoteSpec('origin'),
1593 relpath = relpath, 2080 relpath = '.repo/%s' % name,
1594 revisionExpr = 'refs/heads/master', 2081 revisionExpr = 'refs/heads/master',
1595 revisionId = None) 2082 revisionId = None,
2083 groups = None)
1596 2084
1597 def PreSync(self): 2085 def PreSync(self):
1598 if self.Exists: 2086 if self.Exists:
1599 cb = self.CurrentBranch 2087 cb = self.CurrentBranch
1600 if cb: 2088 if cb:
1601 cb = self.GetBranch(cb) 2089 base = self.GetBranch(cb).merge
1602 if cb.merge: 2090 if base:
1603 self.revisionExpr = cb.merge 2091 self.revisionExpr = base
1604 self.revisionId = None 2092 self.revisionId = None
1605 if cb.remote and cb.remote.name: 2093
1606 self.remote.name = cb.remote.name 2094 def MetaBranchSwitch(self, target):
2095 """ Prepare MetaProject for manifest branch switch
2096 """
2097
2098 # detach and delete manifest branch, allowing a new
2099 # branch to take over
2100 syncbuf = SyncBuffer(self.config, detach_head = True)
2101 self.Sync_LocalHalf(syncbuf)
2102 syncbuf.Finish()
2103
2104 return GitCommand(self,
2105 ['update-ref', '-d', 'refs/heads/default'],
2106 capture_stdout = True,
2107 capture_stderr = True).Wait() == 0
2108
1607 2109
1608 @property 2110 @property
1609 def LastFetch(self): 2111 def LastFetch(self):
diff --git a/repo b/repo
index 773ad825..d6b46c87 100755
--- a/repo
+++ b/repo
@@ -2,7 +2,7 @@
2 2
3## repo default configuration 3## repo default configuration
4## 4##
5REPO_URL='git://android.git.kernel.org/tools/repo.git' 5REPO_URL='https://gerrit.googlesource.com/git-repo'
6REPO_REV='stable' 6REPO_REV='stable'
7 7
8# Copyright (C) 2008 Google Inc. 8# Copyright (C) 2008 Google Inc.
@@ -28,7 +28,7 @@ if __name__ == '__main__':
28del magic 28del magic
29 29
30# increment this whenever we make important changes to this script 30# increment this whenever we make important changes to this script
31VERSION = (1, 10) 31VERSION = (1, 17)
32 32
33# increment this if the MAINTAINER_KEYS block is modified 33# increment this if the MAINTAINER_KEYS block is modified
34KEYRING_VERSION = (1,0) 34KEYRING_VERSION = (1,0)
@@ -91,6 +91,7 @@ import re
91import readline 91import readline
92import subprocess 92import subprocess
93import sys 93import sys
94import urllib2
94 95
95home_dot_repo = os.path.expanduser('~/.repoconfig') 96home_dot_repo = os.path.expanduser('~/.repoconfig')
96gpg_dir = os.path.join(home_dot_repo, 'gnupg') 97gpg_dir = os.path.join(home_dot_repo, 'gnupg')
@@ -109,23 +110,31 @@ group = init_optparse.add_option_group('Manifest options')
109group.add_option('-u', '--manifest-url', 110group.add_option('-u', '--manifest-url',
110 dest='manifest_url', 111 dest='manifest_url',
111 help='manifest repository location', metavar='URL') 112 help='manifest repository location', metavar='URL')
112group.add_option('-o', '--origin',
113 dest='manifest_origin',
114 help="use REMOTE instead of 'origin' to track upstream",
115 metavar='REMOTE')
116group.add_option('-b', '--manifest-branch', 113group.add_option('-b', '--manifest-branch',
117 dest='manifest_branch', 114 dest='manifest_branch',
118 help='manifest branch or revision', metavar='REVISION') 115 help='manifest branch or revision', metavar='REVISION')
119group.add_option('-m', '--manifest-name', 116group.add_option('-m', '--manifest-name',
120 dest='manifest_name', 117 dest='manifest_name',
121 help='initial manifest file (deprecated)', 118 help='initial manifest file', metavar='NAME.xml')
122 metavar='NAME.xml')
123group.add_option('--mirror', 119group.add_option('--mirror',
124 dest='mirror', action='store_true', 120 dest='mirror', action='store_true',
125 help='mirror the forrest') 121 help='mirror the forrest')
126group.add_option('--reference', 122group.add_option('--reference',
127 dest='reference', 123 dest='reference',
128 help='location of mirror directory', metavar='DIR') 124 help='location of mirror directory', metavar='DIR')
125group.add_option('--depth', type='int', default=None,
126 dest='depth',
127 help='create a shallow clone with given depth; see git clone')
128group.add_option('-g', '--groups',
129 dest='groups', default='default',
130 help='restrict manifest projects to ones with a specified group',
131 metavar='GROUP')
132group.add_option('-p', '--platform',
133 dest='platform', default="auto",
134 help='restrict manifest projects to ones with a specified'
135 'platform group [auto|all|none|linux|darwin|...]',
136 metavar='PLATFORM')
137
129 138
130# Tool 139# Tool
131group = init_optparse.add_option_group('repo Version options') 140group = init_optparse.add_option_group('repo Version options')
@@ -139,6 +148,11 @@ group.add_option('--no-repo-verify',
139 dest='no_repo_verify', action='store_true', 148 dest='no_repo_verify', action='store_true',
140 help='do not verify repo source code') 149 help='do not verify repo source code')
141 150
151# Other
152group = init_optparse.add_option_group('Other options')
153group.add_option('--config-name',
154 dest='config_name', action="store_true", default=False,
155 help='Always prompt for name/e-mail')
142 156
143class CloneFailure(Exception): 157class CloneFailure(Exception):
144 """Indicate the remote clone of repo itself failed. 158 """Indicate the remote clone of repo itself failed.
@@ -149,7 +163,7 @@ def _Init(args):
149 """Installs repo by cloning it over the network. 163 """Installs repo by cloning it over the network.
150 """ 164 """
151 opt, args = init_optparse.parse_args(args) 165 opt, args = init_optparse.parse_args(args)
152 if args or not opt.manifest_url: 166 if args:
153 init_optparse.print_usage() 167 init_optparse.print_usage()
154 sys.exit(1) 168 sys.exit(1)
155 169
@@ -188,10 +202,6 @@ def _Init(args):
188 else: 202 else:
189 can_verify = True 203 can_verify = True
190 204
191 if not opt.quiet:
192 print >>sys.stderr, 'Getting repo ...'
193 print >>sys.stderr, ' from %s' % url
194
195 dst = os.path.abspath(os.path.join(repodir, S_repo)) 205 dst = os.path.abspath(os.path.join(repodir, S_repo))
196 _Clone(url, dst, opt.quiet) 206 _Clone(url, dst, opt.quiet)
197 207
@@ -210,7 +220,17 @@ def _Init(args):
210 220
211def _CheckGitVersion(): 221def _CheckGitVersion():
212 cmd = [GIT, '--version'] 222 cmd = [GIT, '--version']
213 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) 223 try:
224 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
225 except OSError, e:
226 print >>sys.stderr
227 print >>sys.stderr, "fatal: '%s' is not available" % GIT
228 print >>sys.stderr, 'fatal: %s' % e
229 print >>sys.stderr
230 print >>sys.stderr, 'Please make sure %s is installed'\
231 ' and in your path.' % GIT
232 raise CloneFailure()
233
214 ver_str = proc.stdout.read().strip() 234 ver_str = proc.stdout.read().strip()
215 proc.stdout.close() 235 proc.stdout.close()
216 proc.wait() 236 proc.wait()
@@ -301,15 +321,43 @@ def _SetConfig(local, name, value):
301 raise CloneFailure() 321 raise CloneFailure()
302 322
303 323
304def _Fetch(local, quiet, *args): 324def _InitHttp():
325 handlers = []
326
327 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
328 try:
329 import netrc
330 n = netrc.netrc()
331 for host in n.hosts:
332 p = n.hosts[host]
333 mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2])
334 mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2])
335 except:
336 pass
337 handlers.append(urllib2.HTTPBasicAuthHandler(mgr))
338 handlers.append(urllib2.HTTPDigestAuthHandler(mgr))
339
340 if 'http_proxy' in os.environ:
341 url = os.environ['http_proxy']
342 handlers.append(urllib2.ProxyHandler({'http': url, 'https': url}))
343 if 'REPO_CURL_VERBOSE' in os.environ:
344 handlers.append(urllib2.HTTPHandler(debuglevel=1))
345 handlers.append(urllib2.HTTPSHandler(debuglevel=1))
346 urllib2.install_opener(urllib2.build_opener(*handlers))
347
348def _Fetch(url, local, src, quiet):
349 if not quiet:
350 print >>sys.stderr, 'Get %s' % url
351
305 cmd = [GIT, 'fetch'] 352 cmd = [GIT, 'fetch']
306 if quiet: 353 if quiet:
307 cmd.append('--quiet') 354 cmd.append('--quiet')
308 err = subprocess.PIPE 355 err = subprocess.PIPE
309 else: 356 else:
310 err = None 357 err = None
311 cmd.extend(args) 358 cmd.append(src)
312 cmd.append('origin') 359 cmd.append('+refs/heads/*:refs/remotes/origin/*')
360 cmd.append('refs/tags/*:refs/tags/*')
313 361
314 proc = subprocess.Popen(cmd, cwd = local, stderr = err) 362 proc = subprocess.Popen(cmd, cwd = local, stderr = err)
315 if err: 363 if err:
@@ -318,6 +366,62 @@ def _Fetch(local, quiet, *args):
318 if proc.wait() != 0: 366 if proc.wait() != 0:
319 raise CloneFailure() 367 raise CloneFailure()
320 368
369def _DownloadBundle(url, local, quiet):
370 if not url.endswith('/'):
371 url += '/'
372 url += 'clone.bundle'
373
374 proc = subprocess.Popen(
375 [GIT, 'config', '--get-regexp', 'url.*.insteadof'],
376 cwd = local,
377 stdout = subprocess.PIPE)
378 for line in proc.stdout:
379 m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line)
380 if m:
381 new_url = m.group(1)
382 old_url = m.group(2)
383 if url.startswith(old_url):
384 url = new_url + url[len(old_url):]
385 break
386 proc.stdout.close()
387 proc.wait()
388
389 if not url.startswith('http:') and not url.startswith('https:'):
390 return False
391
392 dest = open(os.path.join(local, '.git', 'clone.bundle'), 'w+b')
393 try:
394 try:
395 r = urllib2.urlopen(url)
396 except urllib2.HTTPError, e:
397 if e.code == 404:
398 return False
399 print >>sys.stderr, 'fatal: Cannot get %s' % url
400 print >>sys.stderr, 'fatal: HTTP error %s' % e.code
401 raise CloneFailure()
402 except urllib2.URLError, e:
403 print >>sys.stderr, 'fatal: Cannot get %s' % url
404 print >>sys.stderr, 'fatal: error %s' % e.reason
405 raise CloneFailure()
406 try:
407 if not quiet:
408 print >>sys.stderr, 'Get %s' % url
409 while True:
410 buf = r.read(8192)
411 if buf == '':
412 return True
413 dest.write(buf)
414 finally:
415 r.close()
416 finally:
417 dest.close()
418
419def _ImportBundle(local):
420 path = os.path.join(local, '.git', 'clone.bundle')
421 try:
422 _Fetch(local, local, path, True)
423 finally:
424 os.remove(path)
321 425
322def _Clone(url, local, quiet): 426def _Clone(url, local, quiet):
323 """Clones a git repository to a new subdirectory of repodir 427 """Clones a git repository to a new subdirectory of repodir
@@ -345,11 +449,14 @@ def _Clone(url, local, quiet):
345 print >>sys.stderr, 'fatal: could not create %s' % local 449 print >>sys.stderr, 'fatal: could not create %s' % local
346 raise CloneFailure() 450 raise CloneFailure()
347 451
452 _InitHttp()
348 _SetConfig(local, 'remote.origin.url', url) 453 _SetConfig(local, 'remote.origin.url', url)
349 _SetConfig(local, 'remote.origin.fetch', 454 _SetConfig(local, 'remote.origin.fetch',
350 '+refs/heads/*:refs/remotes/origin/*') 455 '+refs/heads/*:refs/remotes/origin/*')
351 _Fetch(local, quiet) 456 if _DownloadBundle(url, local, quiet):
352 _Fetch(local, quiet, '--tags') 457 _ImportBundle(local)
458 else:
459 _Fetch(url, local, 'origin', quiet)
353 460
354 461
355def _Verify(cwd, branch, quiet): 462def _Verify(cwd, branch, quiet):
@@ -601,4 +708,3 @@ def main(orig_args):
601 708
602if __name__ == '__main__': 709if __name__ == '__main__':
603 main(sys.argv[1:]) 710 main(sys.argv[1:])
604
diff --git a/subcmds/abandon.py b/subcmds/abandon.py
index 8af61327..42abb2ff 100644
--- a/subcmds/abandon.py
+++ b/subcmds/abandon.py
@@ -41,21 +41,30 @@ It is equivalent to "git branch -D <branchname>".
41 41
42 nb = args[0] 42 nb = args[0]
43 err = [] 43 err = []
44 success = []
44 all = self.GetProjects(args[1:]) 45 all = self.GetProjects(args[1:])
45 46
46 pm = Progress('Abandon %s' % nb, len(all)) 47 pm = Progress('Abandon %s' % nb, len(all))
47 for project in all: 48 for project in all:
48 pm.update() 49 pm.update()
49 if not project.AbandonBranch(nb): 50
50 err.append(project) 51 status = project.AbandonBranch(nb)
52 if status is not None:
53 if status:
54 success.append(project)
55 else:
56 err.append(project)
51 pm.end() 57 pm.end()
52 58
53 if err: 59 if err:
54 if len(err) == len(all): 60 for p in err:
55 print >>sys.stderr, 'error: no project has branch %s' % nb 61 print >>sys.stderr,\
56 else: 62 "error: %s/: cannot abandon %s" \
57 for p in err: 63 % (p.relpath, nb)
58 print >>sys.stderr,\ 64 sys.exit(1)
59 "error: %s/: cannot abandon %s" \ 65 elif not success:
60 % (p.relpath, nb) 66 print >>sys.stderr, 'error: no project has branch %s' % nb
61 sys.exit(1) 67 sys.exit(1)
68 else:
69 print >>sys.stderr, 'Abandoned in %d project(s):\n %s' % (
70 len(success), '\n '.join(p.relpath for p in success))
diff --git a/subcmds/checkout.py b/subcmds/checkout.py
index 4198acd1..533d20e1 100644
--- a/subcmds/checkout.py
+++ b/subcmds/checkout.py
@@ -38,21 +38,27 @@ The command is equivalent to:
38 38
39 nb = args[0] 39 nb = args[0]
40 err = [] 40 err = []
41 success = []
41 all = self.GetProjects(args[1:]) 42 all = self.GetProjects(args[1:])
42 43
43 pm = Progress('Checkout %s' % nb, len(all)) 44 pm = Progress('Checkout %s' % nb, len(all))
44 for project in all: 45 for project in all:
45 pm.update() 46 pm.update()
46 if not project.CheckoutBranch(nb): 47
47 err.append(project) 48 status = project.CheckoutBranch(nb)
49 if status is not None:
50 if status:
51 success.append(project)
52 else:
53 err.append(project)
48 pm.end() 54 pm.end()
49 55
50 if err: 56 if err:
51 if len(err) == len(all): 57 for p in err:
52 print >>sys.stderr, 'error: no project has branch %s' % nb 58 print >>sys.stderr,\
53 else: 59 "error: %s/: cannot checkout %s" \
54 for p in err: 60 % (p.relpath, nb)
55 print >>sys.stderr,\ 61 sys.exit(1)
56 "error: %s/: cannot checkout %s" \ 62 elif not success:
57 % (p.relpath, nb) 63 print >>sys.stderr, 'error: no project has branch %s' % nb
58 sys.exit(1) 64 sys.exit(1)
diff --git a/subcmds/cherry_pick.py b/subcmds/cherry_pick.py
new file mode 100644
index 00000000..8da3a750
--- /dev/null
+++ b/subcmds/cherry_pick.py
@@ -0,0 +1,114 @@
1#
2# Copyright (C) 2010 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import sys, re, string, random, os
17from command import Command
18from git_command import GitCommand
19
20CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$')
21
22class CherryPick(Command):
23 common = True
24 helpSummary = "Cherry-pick a change."
25 helpUsage = """
26%prog <sha1>
27"""
28 helpDescription = """
29'%prog' cherry-picks a change from one branch to another.
30The change id will be updated, and a reference to the old
31change id will be added.
32"""
33
34 def _Options(self, p):
35 pass
36
37 def Execute(self, opt, args):
38 if len(args) != 1:
39 self.Usage()
40
41 reference = args[0]
42
43 p = GitCommand(None,
44 ['rev-parse', '--verify', reference],
45 capture_stdout = True,
46 capture_stderr = True)
47 if p.Wait() != 0:
48 print >>sys.stderr, p.stderr
49 sys.exit(1)
50 sha1 = p.stdout.strip()
51
52 p = GitCommand(None, ['cat-file', 'commit', sha1], capture_stdout=True)
53 if p.Wait() != 0:
54 print >>sys.stderr, "error: Failed to retrieve old commit message"
55 sys.exit(1)
56 old_msg = self._StripHeader(p.stdout)
57
58 p = GitCommand(None,
59 ['cherry-pick', sha1],
60 capture_stdout = True,
61 capture_stderr = True)
62 status = p.Wait()
63
64 print >>sys.stdout, p.stdout
65 print >>sys.stderr, p.stderr
66
67 if status == 0:
68 # The cherry-pick was applied correctly. We just need to edit the
69 # commit message.
70 new_msg = self._Reformat(old_msg, sha1)
71
72 p = GitCommand(None, ['commit', '--amend', '-F', '-'],
73 provide_stdin = True,
74 capture_stdout = True,
75 capture_stderr = True)
76 p.stdin.write(new_msg)
77 if p.Wait() != 0:
78 print >>sys.stderr, "error: Failed to update commit message"
79 sys.exit(1)
80
81 else:
82 print >>sys.stderr, """\
83NOTE: When committing (please see above) and editing the commit message,
84please remove the old Change-Id-line and add:
85"""
86 print >>sys.stderr, self._GetReference(sha1)
87 print >>sys.stderr
88
89 def _IsChangeId(self, line):
90 return CHANGE_ID_RE.match(line)
91
92 def _GetReference(self, sha1):
93 return "(cherry picked from commit %s)" % sha1
94
95 def _StripHeader(self, commit_msg):
96 lines = commit_msg.splitlines()
97 return "\n".join(lines[lines.index("")+1:])
98
99 def _Reformat(self, old_msg, sha1):
100 new_msg = []
101
102 for line in old_msg.splitlines():
103 if not self._IsChangeId(line):
104 new_msg.append(line)
105
106 # Add a blank line between the message and the change id/reference
107 try:
108 if new_msg[-1].strip() != "":
109 new_msg.append("")
110 except IndexError:
111 pass
112
113 new_msg.append(self._GetReference(sha1))
114 return "\n".join(new_msg)
diff --git a/subcmds/diff.py b/subcmds/diff.py
index e0247140..f233f690 100644
--- a/subcmds/diff.py
+++ b/subcmds/diff.py
@@ -20,8 +20,21 @@ class Diff(PagedCommand):
20 helpSummary = "Show changes between commit and working tree" 20 helpSummary = "Show changes between commit and working tree"
21 helpUsage = """ 21 helpUsage = """
22%prog [<project>...] 22%prog [<project>...]
23
24The -u option causes '%prog' to generate diff output with file paths
25relative to the repository root, so the output can be applied
26to the Unix 'patch' command.
23""" 27"""
24 28
29 def _Options(self, p):
30 def cmd(option, opt_str, value, parser):
31 setattr(parser.values, option.dest, list(parser.rargs))
32 while parser.rargs:
33 del parser.rargs[0]
34 p.add_option('-u', '--absolute',
35 dest='absolute', action='store_true',
36 help='Paths are relative to the repository root')
37
25 def Execute(self, opt, args): 38 def Execute(self, opt, args):
26 for project in self.GetProjects(args): 39 for project in self.GetProjects(args):
27 project.PrintWorkTreeDiff() 40 project.PrintWorkTreeDiff(opt.absolute)
diff --git a/subcmds/download.py b/subcmds/download.py
index 61eadd54..0ea45c3f 100644
--- a/subcmds/download.py
+++ b/subcmds/download.py
@@ -33,7 +33,15 @@ makes it available in your project's local working directory.
33""" 33"""
34 34
35 def _Options(self, p): 35 def _Options(self, p):
36 pass 36 p.add_option('-c','--cherry-pick',
37 dest='cherrypick', action='store_true',
38 help="cherry-pick instead of checkout")
39 p.add_option('-r','--revert',
40 dest='revert', action='store_true',
41 help="revert instead of checkout")
42 p.add_option('-f','--ff-only',
43 dest='ffonly', action='store_true',
44 help="force fast-forward merge")
37 45
38 def _ParseChangeIds(self, args): 46 def _ParseChangeIds(self, args):
39 if not args: 47 if not args:
@@ -66,7 +74,7 @@ makes it available in your project's local working directory.
66 % (project.name, change_id, ps_id) 74 % (project.name, change_id, ps_id)
67 sys.exit(1) 75 sys.exit(1)
68 76
69 if not dl.commits: 77 if not opt.revert and not dl.commits:
70 print >>sys.stderr, \ 78 print >>sys.stderr, \
71 '[%s] change %d/%d has already been merged' \ 79 '[%s] change %d/%d has already been merged' \
72 % (project.name, change_id, ps_id) 80 % (project.name, change_id, ps_id)
@@ -78,4 +86,11 @@ makes it available in your project's local working directory.
78 % (project.name, change_id, ps_id, len(dl.commits)) 86 % (project.name, change_id, ps_id, len(dl.commits))
79 for c in dl.commits: 87 for c in dl.commits:
80 print >>sys.stderr, ' %s' % (c) 88 print >>sys.stderr, ' %s' % (c)
81 project._Checkout(dl.commit) 89 if opt.cherrypick:
90 project._CherryPick(dl.commit)
91 elif opt.revert:
92 project._Revert(dl.commit)
93 elif opt.ffonly:
94 project._FastForward(dl.commit, ffonly=True)
95 else:
96 project._Checkout(dl.commit)
diff --git a/subcmds/forall.py b/subcmds/forall.py
index d3e70ae1..9436f4e5 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -82,6 +82,11 @@ revision to a locally executed git command, use REPO_LREV.
82REPO_RREV is the name of the revision from the manifest, exactly 82REPO_RREV is the name of the revision from the manifest, exactly
83as written in the manifest. 83as written in the manifest.
84 84
85REPO__* are any extra environment variables, specified by the
86"annotation" element under any project element. This can be useful
87for differentiating trees based on user-specific criteria, or simply
88annotating tree details.
89
85shell positional arguments ($1, $2, .., $#) are set to any arguments 90shell positional arguments ($1, $2, .., $#) are set to any arguments
86following <command>. 91following <command>.
87 92
@@ -162,6 +167,8 @@ terminal and are not redirected.
162 setenv('REPO_REMOTE', project.remote.name) 167 setenv('REPO_REMOTE', project.remote.name)
163 setenv('REPO_LREV', project.GetRevisionId()) 168 setenv('REPO_LREV', project.GetRevisionId())
164 setenv('REPO_RREV', project.revisionExpr) 169 setenv('REPO_RREV', project.revisionExpr)
170 for a in project.annotations:
171 setenv("REPO__%s" % (a.name), a.value)
165 172
166 if mirror: 173 if mirror:
167 setenv('GIT_DIR', project.gitdir) 174 setenv('GIT_DIR', project.gitdir)
diff --git a/subcmds/help.py b/subcmds/help.py
index e2f3074c..0df3c14b 100644
--- a/subcmds/help.py
+++ b/subcmds/help.py
@@ -165,7 +165,7 @@ See 'repo help --all' for a complete list of recognized commands.
165 print >>sys.stderr, "repo: '%s' is not a repo command." % name 165 print >>sys.stderr, "repo: '%s' is not a repo command." % name
166 sys.exit(1) 166 sys.exit(1)
167 167
168 cmd.repodir = self.repodir 168 cmd.manifest = self.manifest
169 self._PrintCommandHelp(cmd) 169 self._PrintCommandHelp(cmd)
170 170
171 else: 171 else:
diff --git a/subcmds/init.py b/subcmds/init.py
index 2ca4e163..a758fbb1 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -14,16 +14,17 @@
14# limitations under the License. 14# limitations under the License.
15 15
16import os 16import os
17import platform
18import re
19import shutil
17import sys 20import sys
18 21
19from color import Coloring 22from color import Coloring
20from command import InteractiveCommand, MirrorSafeCommand 23from command import InteractiveCommand, MirrorSafeCommand
21from error import ManifestParseError 24from error import ManifestParseError
22from project import SyncBuffer 25from project import SyncBuffer
26from git_config import GitConfig
23from git_command import git_require, MIN_GIT_VERSION 27from git_command import git_require, MIN_GIT_VERSION
24from manifest_submodule import SubmoduleManifest
25from manifest_xml import XmlManifest
26from subcmds.sync import _ReloadManifest
27 28
28class Init(InteractiveCommand, MirrorSafeCommand): 29class Init(InteractiveCommand, MirrorSafeCommand):
29 common = True 30 common = True
@@ -75,21 +76,27 @@ to update the working directory files.
75 g.add_option('-b', '--manifest-branch', 76 g.add_option('-b', '--manifest-branch',
76 dest='manifest_branch', 77 dest='manifest_branch',
77 help='manifest branch or revision', metavar='REVISION') 78 help='manifest branch or revision', metavar='REVISION')
78 g.add_option('-o', '--origin', 79 g.add_option('-m', '--manifest-name',
79 dest='manifest_origin', 80 dest='manifest_name', default='default.xml',
80 help="use REMOTE instead of 'origin' to track upstream", 81 help='initial manifest file', metavar='NAME.xml')
81 metavar='REMOTE')
82 if isinstance(self.manifest, XmlManifest) \
83 or not self.manifest.manifestProject.Exists:
84 g.add_option('-m', '--manifest-name',
85 dest='manifest_name', default='default.xml',
86 help='initial manifest file', metavar='NAME.xml')
87 g.add_option('--mirror', 82 g.add_option('--mirror',
88 dest='mirror', action='store_true', 83 dest='mirror', action='store_true',
89 help='mirror the forrest') 84 help='mirror the forrest')
90 g.add_option('--reference', 85 g.add_option('--reference',
91 dest='reference', 86 dest='reference',
92 help='location of mirror directory', metavar='DIR') 87 help='location of mirror directory', metavar='DIR')
88 g.add_option('--depth', type='int', default=None,
89 dest='depth',
90 help='create a shallow clone with given depth; see git clone')
91 g.add_option('-g', '--groups',
92 dest='groups', default='default',
93 help='restrict manifest projects to ones with a specified group',
94 metavar='GROUP')
95 g.add_option('-p', '--platform',
96 dest='platform', default='auto',
97 help='restrict manifest projects to ones with a specified'
98 'platform group [auto|all|none|linux|darwin|...]',
99 metavar='PLATFORM')
93 100
94 # Tool 101 # Tool
95 g = p.add_option_group('repo Version options') 102 g = p.add_option_group('repo Version options')
@@ -103,91 +110,94 @@ to update the working directory files.
103 dest='no_repo_verify', action='store_true', 110 dest='no_repo_verify', action='store_true',
104 help='do not verify repo source code') 111 help='do not verify repo source code')
105 112
106 def _ApplyOptions(self, opt, is_new): 113 # Other
114 g = p.add_option_group('Other options')
115 g.add_option('--config-name',
116 dest='config_name', action="store_true", default=False,
117 help='Always prompt for name/e-mail')
118
119 def _SyncManifest(self, opt):
107 m = self.manifest.manifestProject 120 m = self.manifest.manifestProject
121 is_new = not m.Exists
108 122
109 if is_new: 123 if is_new:
110 if opt.manifest_origin: 124 if not opt.manifest_url:
111 m.remote.name = opt.manifest_origin 125 print >>sys.stderr, 'fatal: manifest url (-u) is required.'
126 sys.exit(1)
127
128 if not opt.quiet:
129 print >>sys.stderr, 'Get %s' \
130 % GitConfig.ForUser().UrlInsteadOf(opt.manifest_url)
131 m._InitGitDir()
112 132
113 if opt.manifest_branch: 133 if opt.manifest_branch:
114 m.revisionExpr = opt.manifest_branch 134 m.revisionExpr = opt.manifest_branch
115 else: 135 else:
116 m.revisionExpr = 'refs/heads/master' 136 m.revisionExpr = 'refs/heads/master'
117 else: 137 else:
118 if opt.manifest_origin:
119 print >>sys.stderr, 'fatal: cannot change origin name'
120 sys.exit(1)
121
122 if opt.manifest_branch: 138 if opt.manifest_branch:
123 m.revisionExpr = opt.manifest_branch 139 m.revisionExpr = opt.manifest_branch
124 else: 140 else:
125 m.PreSync() 141 m.PreSync()
126 142
127 def _SyncManifest(self, opt):
128 m = self.manifest.manifestProject
129 is_new = not m.Exists
130
131 if is_new:
132 if not opt.manifest_url:
133 print >>sys.stderr, 'fatal: manifest url (-u) is required.'
134 sys.exit(1)
135
136 if not opt.quiet:
137 print >>sys.stderr, 'Getting manifest ...'
138 print >>sys.stderr, ' from %s' % opt.manifest_url
139 m._InitGitDir()
140
141 self._ApplyOptions(opt, is_new)
142 if opt.manifest_url: 143 if opt.manifest_url:
143 r = m.GetRemote(m.remote.name) 144 r = m.GetRemote(m.remote.name)
144 r.url = opt.manifest_url 145 r.url = opt.manifest_url
145 r.ResetFetch() 146 r.ResetFetch()
146 r.Save() 147 r.Save()
147 148
149 groups = re.split('[,\s]+', opt.groups)
150 all_platforms = ['linux', 'darwin']
151 platformize = lambda x: 'platform-' + x
152 if opt.platform == 'auto':
153 if (not opt.mirror and
154 not m.config.GetString('repo.mirror') == 'true'):
155 groups.append(platformize(platform.system().lower()))
156 elif opt.platform == 'all':
157 groups.extend(map(platformize, all_platforms))
158 elif opt.platform in all_platforms:
159 groups.extend(platformize(opt.platform))
160 elif opt.platform != 'none':
161 print >>sys.stderr, 'fatal: invalid platform flag'
162 sys.exit(1)
163
164 groups = [x for x in groups if x]
165 groupstr = ','.join(groups)
166 if opt.platform == 'auto' and groupstr == 'default,platform-' + platform.system().lower():
167 groupstr = None
168 m.config.SetString('manifest.groups', groupstr)
169
148 if opt.reference: 170 if opt.reference:
149 m.config.SetString('repo.reference', opt.reference) 171 m.config.SetString('repo.reference', opt.reference)
150 172
151 if opt.mirror: 173 if opt.mirror:
152 if is_new: 174 if is_new:
153 m.config.SetString('repo.mirror', 'true') 175 m.config.SetString('repo.mirror', 'true')
154 m.config.ClearCache()
155 else: 176 else:
156 print >>sys.stderr, 'fatal: --mirror not supported on existing client' 177 print >>sys.stderr, 'fatal: --mirror not supported on existing client'
157 sys.exit(1) 178 sys.exit(1)
158 179
159 if not m.Sync_NetworkHalf(): 180 if not m.Sync_NetworkHalf(is_new=is_new):
160 r = m.GetRemote(m.remote.name) 181 r = m.GetRemote(m.remote.name)
161 print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url 182 print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url
162 sys.exit(1)
163 183
164 if is_new and SubmoduleManifest.IsBare(m): 184 # Better delete the manifest git dir if we created it; otherwise next
165 new = self.GetManifest(reparse=True, type=SubmoduleManifest) 185 # time (when user fixes problems) we won't go through the "is_new" logic.
166 if m.gitdir != new.manifestProject.gitdir: 186 if is_new:
167 os.rename(m.gitdir, new.manifestProject.gitdir) 187 shutil.rmtree(m.gitdir)
168 new = self.GetManifest(reparse=True, type=SubmoduleManifest) 188 sys.exit(1)
169 m = new.manifestProject
170 self._ApplyOptions(opt, is_new)
171 189
172 if not is_new: 190 if opt.manifest_branch:
173 # Force the manifest to load if it exists, the old graph 191 m.MetaBranchSwitch(opt.manifest_branch)
174 # may be needed inside of _ReloadManifest().
175 #
176 self.manifest.projects
177 192
178 syncbuf = SyncBuffer(m.config) 193 syncbuf = SyncBuffer(m.config)
179 m.Sync_LocalHalf(syncbuf) 194 m.Sync_LocalHalf(syncbuf)
180 syncbuf.Finish() 195 syncbuf.Finish()
181 196
182 if isinstance(self.manifest, XmlManifest): 197 if is_new or m.CurrentBranch is None:
183 self._LinkManifest(opt.manifest_name) 198 if not m.StartBranch('default'):
184 _ReloadManifest(self) 199 print >>sys.stderr, 'fatal: cannot create default in manifest'
185 200 sys.exit(1)
186 self._ApplyOptions(opt, is_new)
187
188 if not self.manifest.InitBranch():
189 print >>sys.stderr, 'fatal: cannot create branch in manifest'
190 sys.exit(1)
191 201
192 def _LinkManifest(self, name): 202 def _LinkManifest(self, name):
193 if not name: 203 if not name:
@@ -210,6 +220,24 @@ to update the working directory files.
210 return value 220 return value
211 return a 221 return a
212 222
223 def _ShouldConfigureUser(self):
224 gc = self.manifest.globalConfig
225 mp = self.manifest.manifestProject
226
227 # If we don't have local settings, get from global.
228 if not mp.config.Has('user.name') or not mp.config.Has('user.email'):
229 if not gc.Has('user.name') or not gc.Has('user.email'):
230 return True
231
232 mp.config.SetString('user.name', gc.GetString('user.name'))
233 mp.config.SetString('user.email', gc.GetString('user.email'))
234
235 print ''
236 print 'Your identity is: %s <%s>' % (mp.config.GetString('user.name'),
237 mp.config.GetString('user.email'))
238 print 'If you want to change this, please re-run \'repo init\' with --config-name'
239 return False
240
213 def _ConfigureUser(self): 241 def _ConfigureUser(self):
214 mp = self.manifest.manifestProject 242 mp = self.manifest.manifestProject
215 243
@@ -220,7 +248,7 @@ to update the working directory files.
220 248
221 print '' 249 print ''
222 print 'Your identity is: %s <%s>' % (name, email) 250 print 'Your identity is: %s <%s>' % (name, email)
223 sys.stdout.write('is this correct [y/n]? ') 251 sys.stdout.write('is this correct [y/N]? ')
224 a = sys.stdin.readline().strip() 252 a = sys.stdin.readline().strip()
225 if a in ('yes', 'y', 't', 'true'): 253 if a in ('yes', 'y', 't', 'true'):
226 break 254 break
@@ -262,19 +290,42 @@ to update the working directory files.
262 out.printer(fg='black', attr=c)(' %-6s ', c) 290 out.printer(fg='black', attr=c)(' %-6s ', c)
263 out.nl() 291 out.nl()
264 292
265 sys.stdout.write('Enable color display in this user account (y/n)? ') 293 sys.stdout.write('Enable color display in this user account (y/N)? ')
266 a = sys.stdin.readline().strip().lower() 294 a = sys.stdin.readline().strip().lower()
267 if a in ('y', 'yes', 't', 'true', 'on'): 295 if a in ('y', 'yes', 't', 'true', 'on'):
268 gc.SetString('color.ui', 'auto') 296 gc.SetString('color.ui', 'auto')
269 297
298 def _ConfigureDepth(self, opt):
299 """Configure the depth we'll sync down.
300
301 Args:
302 opt: Options from optparse. We care about opt.depth.
303 """
304 # Opt.depth will be non-None if user actually passed --depth to repo init.
305 if opt.depth is not None:
306 if opt.depth > 0:
307 # Positive values will set the depth.
308 depth = str(opt.depth)
309 else:
310 # Negative numbers will clear the depth; passing None to SetString
311 # will do that.
312 depth = None
313
314 # We store the depth in the main manifest project.
315 self.manifest.manifestProject.config.SetString('repo.depth', depth)
316
270 def Execute(self, opt, args): 317 def Execute(self, opt, args):
271 git_require(MIN_GIT_VERSION, fail=True) 318 git_require(MIN_GIT_VERSION, fail=True)
272 self._SyncManifest(opt) 319 self._SyncManifest(opt)
320 self._LinkManifest(opt.manifest_name)
273 321
274 if os.isatty(0) and os.isatty(1) and not self.manifest.IsMirror: 322 if os.isatty(0) and os.isatty(1) and not self.manifest.IsMirror:
275 self._ConfigureUser() 323 if opt.config_name or self._ShouldConfigureUser():
324 self._ConfigureUser()
276 self._ConfigureColor() 325 self._ConfigureColor()
277 326
327 self._ConfigureDepth(opt)
328
278 if self.manifest.IsMirror: 329 if self.manifest.IsMirror:
279 type = 'mirror ' 330 type = 'mirror '
280 else: 331 else:
diff --git a/subcmds/list.py b/subcmds/list.py
new file mode 100644
index 00000000..2be82570
--- /dev/null
+++ b/subcmds/list.py
@@ -0,0 +1,48 @@
1#
2# Copyright (C) 2011 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 command import Command, MirrorSafeCommand
17
18class List(Command, MirrorSafeCommand):
19 common = True
20 helpSummary = "List projects and their associated directories"
21 helpUsage = """
22%prog [<project>...]
23"""
24 helpDescription = """
25List all projects; pass '.' to list the project for the cwd.
26
27This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
28"""
29
30 def Execute(self, opt, args):
31 """List all projects and the associated directories.
32
33 This may be possible to do with 'repo forall', but repo newbies have
34 trouble figuring that out. The idea here is that it should be more
35 discoverable.
36
37 Args:
38 opt: The options. We don't take any.
39 args: Positional args. Can be a list of projects to list, or empty.
40 """
41 projects = self.GetProjects(args)
42
43 lines = []
44 for project in projects:
45 lines.append("%s : %s" % (project.relpath, project.name))
46
47 lines.sort()
48 print '\n'.join(lines)
diff --git a/subcmds/manifest.py b/subcmds/manifest.py
index dcd3df17..4374a9d0 100644
--- a/subcmds/manifest.py
+++ b/subcmds/manifest.py
@@ -17,25 +17,14 @@ import os
17import sys 17import sys
18 18
19from command import PagedCommand 19from command import PagedCommand
20from manifest_submodule import SubmoduleManifest
21from manifest_xml import XmlManifest
22
23def _doc(name):
24 r = os.path.dirname(__file__)
25 r = os.path.dirname(r)
26 fd = open(os.path.join(r, 'docs', name))
27 try:
28 return fd.read()
29 finally:
30 fd.close()
31 20
32class Manifest(PagedCommand): 21class Manifest(PagedCommand):
33 common = False 22 common = False
34 helpSummary = "Manifest inspection utility" 23 helpSummary = "Manifest inspection utility"
35 helpUsage = """ 24 helpUsage = """
36%prog [options] 25%prog [-o {-|NAME.xml} [-r]]
37""" 26"""
38 _xmlHelp = """ 27 _helpDescription = """
39 28
40With the -o option, exports the current manifest for inspection. 29With the -o option, exports the current manifest for inspection.
41The manifest and (if present) local_manifest.xml are combined 30The manifest and (if present) local_manifest.xml are combined
@@ -46,30 +35,23 @@ in a Git repository for use during future 'repo init' invocations.
46 35
47 @property 36 @property
48 def helpDescription(self): 37 def helpDescription(self):
49 help = '' 38 help = self._helpDescription + '\n'
50 if isinstance(self.manifest, XmlManifest): 39 r = os.path.dirname(__file__)
51 help += self._xmlHelp + '\n' + _doc('manifest_xml.txt') 40 r = os.path.dirname(r)
52 if isinstance(self.manifest, SubmoduleManifest): 41 fd = open(os.path.join(r, 'docs', 'manifest-format.txt'))
53 help += _doc('manifest_submodule.txt') 42 for line in fd:
43 help += line
44 fd.close()
54 return help 45 return help
55 46
56 def _Options(self, p): 47 def _Options(self, p):
57 if isinstance(self.manifest, XmlManifest): 48 p.add_option('-r', '--revision-as-HEAD',
58 p.add_option('--upgrade', 49 dest='peg_rev', action='store_true',
59 dest='upgrade', action='store_true', 50 help='Save revisions as current HEAD')
60 help='Upgrade XML manifest to submodule') 51 p.add_option('-o', '--output-file',
61 p.add_option('-r', '--revision-as-HEAD', 52 dest='output_file',
62 dest='peg_rev', action='store_true', 53 help='File to save the manifest to',
63 help='Save revisions as current HEAD') 54 metavar='-|NAME.xml')
64 p.add_option('-o', '--output-file',
65 dest='output_file',
66 help='File to save the manifest to',
67 metavar='-|NAME.xml')
68
69 def WantPager(self, opt):
70 if isinstance(self.manifest, XmlManifest) and opt.upgrade:
71 return False
72 return True
73 55
74 def _Output(self, opt): 56 def _Output(self, opt):
75 if opt.output_file == '-': 57 if opt.output_file == '-':
@@ -82,38 +64,13 @@ in a Git repository for use during future 'repo init' invocations.
82 if opt.output_file != '-': 64 if opt.output_file != '-':
83 print >>sys.stderr, 'Saved manifest to %s' % opt.output_file 65 print >>sys.stderr, 'Saved manifest to %s' % opt.output_file
84 66
85 def _Upgrade(self):
86 old = self.manifest
87
88 if isinstance(old, SubmoduleManifest):
89 print >>sys.stderr, 'error: already upgraded'
90 sys.exit(1)
91
92 old._Load()
93 for p in old.projects.values():
94 if not os.path.exists(p.gitdir) \
95 or not os.path.exists(p.worktree):
96 print >>sys.stderr, 'fatal: project "%s" missing' % p.relpath
97 sys.exit(1)
98
99 new = SubmoduleManifest(old.repodir)
100 new.FromXml_Local_1(old, checkout=False)
101 new.FromXml_Definition(old)
102 new.FromXml_Local_2(old)
103 print >>sys.stderr, 'upgraded manifest; commit result manually'
104
105 def Execute(self, opt, args): 67 def Execute(self, opt, args):
106 if args: 68 if args:
107 self.Usage() 69 self.Usage()
108 70
109 if isinstance(self.manifest, XmlManifest): 71 if opt.output_file is not None:
110 if opt.upgrade: 72 self._Output(opt)
111 self._Upgrade() 73 return
112 return
113
114 if opt.output_file is not None:
115 self._Output(opt)
116 return
117 74
118 print >>sys.stderr, 'error: no operation to perform' 75 print >>sys.stderr, 'error: no operation to perform'
119 print >>sys.stderr, 'error: see repo help manifest' 76 print >>sys.stderr, 'error: see repo help manifest'
diff --git a/subcmds/overview.py b/subcmds/overview.py
new file mode 100644
index 00000000..96fa93d8
--- /dev/null
+++ b/subcmds/overview.py
@@ -0,0 +1,80 @@
1#
2# Copyright (C) 2012 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 color import Coloring
17from command import PagedCommand
18
19
20class Overview(PagedCommand):
21 common = True
22 helpSummary = "Display overview of unmerged project branches"
23 helpUsage = """
24%prog [--current-branch] [<project>...]
25"""
26 helpDescription = """
27The '%prog' command is used to display an overview of the projects branches,
28and list any local commits that have not yet been merged into the project.
29
30The -b/--current-branch option can be used to restrict the output to only
31branches currently checked out in each project. By default, all branches
32are displayed.
33"""
34
35 def _Options(self, p):
36 p.add_option('-b', '--current-branch',
37 dest="current_branch", action="store_true",
38 help="Consider only checked out branches")
39
40 def Execute(self, opt, args):
41 all = []
42 for project in self.GetProjects(args):
43 br = [project.GetUploadableBranch(x)
44 for x in project.GetBranches().keys()]
45 br = [x for x in br if x]
46 if opt.current_branch:
47 br = [x for x in br if x.name == project.CurrentBranch]
48 all.extend(br)
49
50 if not all:
51 return
52
53 class Report(Coloring):
54 def __init__(self, config):
55 Coloring.__init__(self, config, 'status')
56 self.project = self.printer('header', attr='bold')
57
58 out = Report(all[0].project.config)
59 out.project('Projects Overview')
60 out.nl()
61
62 project = None
63
64 for branch in all:
65 if project != branch.project:
66 project = branch.project
67 out.nl()
68 out.project('project %s/' % project.relpath)
69 out.nl()
70
71 commits = branch.commits
72 date = branch.date
73 print '%s %-33s (%2d commit%s, %s)' % (
74 branch.name == project.CurrentBranch and '*' or ' ',
75 branch.name,
76 len(commits),
77 len(commits) != 1 and 's' or ' ',
78 date)
79 for commit in commits:
80 print '%-35s - %s' % ('', commit)
diff --git a/subcmds/rebase.py b/subcmds/rebase.py
index e341296d..20662b11 100644
--- a/subcmds/rebase.py
+++ b/subcmds/rebase.py
@@ -17,7 +17,7 @@ import sys
17 17
18from command import Command 18from command import Command
19from git_command import GitCommand 19from git_command import GitCommand
20from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB 20from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
21from error import GitError 21from error import GitError
22 22
23class Rebase(Command): 23class Rebase(Command):
@@ -52,6 +52,9 @@ branch but need to incorporate new upstream changes "underneath" them.
52 p.add_option('--whitespace', 52 p.add_option('--whitespace',
53 dest='whitespace', action='store', metavar='WS', 53 dest='whitespace', action='store', metavar='WS',
54 help='Pass --whitespace to git rebase') 54 help='Pass --whitespace to git rebase')
55 p.add_option('--auto-stash',
56 dest='auto_stash', action='store_true',
57 help='Stash local modifications before starting')
55 58
56 def Execute(self, opt, args): 59 def Execute(self, opt, args):
57 all = self.GetProjects(args) 60 all = self.GetProjects(args)
@@ -103,5 +106,23 @@ branch but need to incorporate new upstream changes "underneath" them.
103 print >>sys.stderr, '# %s: rebasing %s -> %s' % \ 106 print >>sys.stderr, '# %s: rebasing %s -> %s' % \
104 (project.relpath, cb, upbranch.LocalMerge) 107 (project.relpath, cb, upbranch.LocalMerge)
105 108
109 needs_stash = False
110 if opt.auto_stash:
111 stash_args = ["update-index", "--refresh", "-q"]
112
113 if GitCommand(project, stash_args).Wait() != 0:
114 needs_stash = True
115 # Dirty index, requires stash...
116 stash_args = ["stash"]
117
118 if GitCommand(project, stash_args).Wait() != 0:
119 return -1
120
106 if GitCommand(project, args).Wait() != 0: 121 if GitCommand(project, args).Wait() != 0:
107 return -1 122 return -1
123
124 if needs_stash:
125 stash_args.append('pop')
126 stash_args.append('--quiet')
127 if GitCommand(project, stash_args).Wait() != 0:
128 return -1
diff --git a/subcmds/start.py b/subcmds/start.py
index ae2985d2..00885076 100644
--- a/subcmds/start.py
+++ b/subcmds/start.py
@@ -15,6 +15,7 @@
15 15
16import sys 16import sys
17from command import Command 17from command import Command
18from git_config import IsId
18from git_command import git 19from git_command import git
19from progress import Progress 20from progress import Progress
20 21
@@ -56,6 +57,10 @@ revision specified in the manifest.
56 pm = Progress('Starting %s' % nb, len(all)) 57 pm = Progress('Starting %s' % nb, len(all))
57 for project in all: 58 for project in all:
58 pm.update() 59 pm.update()
60 # If the current revision is a specific SHA1 then we can't push back
61 # to it so substitute the manifest default revision instead.
62 if IsId(project.revisionExpr):
63 project.revisionExpr = self.manifest.default.revisionExpr
59 if not project.StartBranch(nb): 64 if not project.StartBranch(nb):
60 err.append(project) 65 err.append(project)
61 pm.end() 66 pm.end()
diff --git a/subcmds/status.py b/subcmds/status.py
index b0d419a7..69e2dbfc 100644
--- a/subcmds/status.py
+++ b/subcmds/status.py
@@ -15,6 +15,15 @@
15 15
16from command import PagedCommand 16from command import PagedCommand
17 17
18try:
19 import threading as _threading
20except ImportError:
21 import dummy_threading as _threading
22
23import itertools
24import sys
25import StringIO
26
18class Status(PagedCommand): 27class Status(PagedCommand):
19 common = True 28 common = True
20 helpSummary = "Show the working tree status" 29 helpSummary = "Show the working tree status"
@@ -27,6 +36,9 @@ and the most recent commit on this branch (HEAD), in each project
27specified. A summary is displayed, one line per file where there 36specified. A summary is displayed, one line per file where there
28is a difference between these three states. 37is a difference between these three states.
29 38
39The -j/--jobs option can be used to run multiple status queries
40in parallel.
41
30Status Display 42Status Display
31-------------- 43--------------
32 44
@@ -60,26 +72,60 @@ the following meanings:
60 72
61""" 73"""
62 74
75 def _Options(self, p):
76 p.add_option('-j', '--jobs',
77 dest='jobs', action='store', type='int', default=2,
78 help="number of projects to check simultaneously")
79
80 def _StatusHelper(self, project, clean_counter, sem, output):
81 """Obtains the status for a specific project.
82
83 Obtains the status for a project, redirecting the output to
84 the specified object. It will release the semaphore
85 when done.
86
87 Args:
88 project: Project to get status of.
89 clean_counter: Counter for clean projects.
90 sem: Semaphore, will call release() when complete.
91 output: Where to output the status.
92 """
93 try:
94 state = project.PrintWorkTreeStatus(output)
95 if state == 'CLEAN':
96 clean_counter.next()
97 finally:
98 sem.release()
99
63 def Execute(self, opt, args): 100 def Execute(self, opt, args):
64 all = self.GetProjects(args) 101 all = self.GetProjects(args)
65 clean = 0 102 counter = itertools.count()
66 103
67 on = {} 104 if opt.jobs == 1:
68 for project in all: 105 for project in all:
69 cb = project.CurrentBranch 106 state = project.PrintWorkTreeStatus()
70 if cb: 107 if state == 'CLEAN':
71 if cb not in on: 108 counter.next()
72 on[cb] = [] 109 else:
73 on[cb].append(project) 110 sem = _threading.Semaphore(opt.jobs)
74 111 threads_and_output = []
75 branch_names = list(on.keys()) 112 for project in all:
76 branch_names.sort() 113 sem.acquire()
77 for cb in branch_names: 114
78 print '# on branch %s' % cb 115 class BufList(StringIO.StringIO):
79 116 def dump(self, ostream):
80 for project in all: 117 for entry in self.buflist:
81 state = project.PrintWorkTreeStatus() 118 ostream.write(entry)
82 if state == 'CLEAN': 119
83 clean += 1 120 output = BufList()
84 if len(all) == clean: 121
122 t = _threading.Thread(target=self._StatusHelper,
123 args=(project, counter, sem, output))
124 threads_and_output.append((t, output))
125 t.start()
126 for (t, output) in threads_and_output:
127 t.join()
128 output.dump(sys.stdout)
129 output.close()
130 if len(all) == counter.next():
85 print 'nothing to commit (working directory clean)' 131 print 'nothing to commit (working directory clean)'
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 16f1d189..bfe146b6 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -28,6 +28,14 @@ try:
28except ImportError: 28except ImportError:
29 import dummy_threading as _threading 29 import dummy_threading as _threading
30 30
31try:
32 import resource
33 def _rlimit_nofile():
34 return resource.getrlimit(resource.RLIMIT_NOFILE)
35except ImportError:
36 def _rlimit_nofile():
37 return (256, 256)
38
31from git_command import GIT 39from git_command import GIT
32from git_refs import R_HEADS 40from git_refs import R_HEADS
33from project import HEAD 41from project import HEAD
@@ -39,6 +47,10 @@ from project import R_HEADS
39from project import SyncBuffer 47from project import SyncBuffer
40from progress import Progress 48from progress import Progress
41 49
50class _FetchError(Exception):
51 """Internal error thrown in _FetchHelper() when we don't want stack trace."""
52 pass
53
42class Sync(Command, MirrorSafeCommand): 54class Sync(Command, MirrorSafeCommand):
43 jobs = 1 55 jobs = 1
44 common = True 56 common = True
@@ -68,11 +80,18 @@ revision is temporarily needed.
68 80
69The -s/--smart-sync option can be used to sync to a known good 81The -s/--smart-sync option can be used to sync to a known good
70build as specified by the manifest-server element in the current 82build as specified by the manifest-server element in the current
71manifest. 83manifest. The -t/--smart-tag option is similar and allows you to
84specify a custom tag/label.
72 85
73The -f/--force-broken option can be used to proceed with syncing 86The -f/--force-broken option can be used to proceed with syncing
74other projects if a project sync fails. 87other projects if a project sync fails.
75 88
89The --no-clone-bundle option disables any attempt to use
90$URL/clone.bundle to bootstrap a new Git repository from a
91resumeable bundle file on a content delivery network. This
92may be necessary if there are problems with the local Python
93HTTP client or proxy configuration, but the Git binary works.
94
76SSH Connections 95SSH Connections
77--------------- 96---------------
78 97
@@ -104,6 +123,8 @@ later is required to fix a server side protocol bug.
104""" 123"""
105 124
106 def _Options(self, p, show_smart=True): 125 def _Options(self, p, show_smart=True):
126 self.jobs = self.manifest.default.sync_j
127
107 p.add_option('-f', '--force-broken', 128 p.add_option('-f', '--force-broken',
108 dest='force_broken', action='store_true', 129 dest='force_broken', action='store_true',
109 help="continue sync even if a project fails to sync") 130 help="continue sync even if a project fails to sync")
@@ -116,16 +137,28 @@ later is required to fix a server side protocol bug.
116 p.add_option('-d','--detach', 137 p.add_option('-d','--detach',
117 dest='detach_head', action='store_true', 138 dest='detach_head', action='store_true',
118 help='detach projects back to manifest revision') 139 help='detach projects back to manifest revision')
140 p.add_option('-c','--current-branch',
141 dest='current_branch_only', action='store_true',
142 help='fetch only current branch from server')
119 p.add_option('-q','--quiet', 143 p.add_option('-q','--quiet',
120 dest='quiet', action='store_true', 144 dest='quiet', action='store_true',
121 help='be more quiet') 145 help='be more quiet')
122 p.add_option('-j','--jobs', 146 p.add_option('-j','--jobs',
123 dest='jobs', action='store', type='int', 147 dest='jobs', action='store', type='int',
124 help="number of projects to fetch simultaneously") 148 help="projects to fetch simultaneously (default %d)" % self.jobs)
149 p.add_option('-m', '--manifest-name',
150 dest='manifest_name',
151 help='temporary manifest to use for this sync', metavar='NAME.xml')
152 p.add_option('--no-clone-bundle',
153 dest='no_clone_bundle', action='store_true',
154 help='disable use of /clone.bundle on HTTP/HTTPS')
125 if show_smart: 155 if show_smart:
126 p.add_option('-s', '--smart-sync', 156 p.add_option('-s', '--smart-sync',
127 dest='smart_sync', action='store_true', 157 dest='smart_sync', action='store_true',
128 help='smart sync using manifest from a known good build') 158 help='smart sync using manifest from a known good build')
159 p.add_option('-t', '--smart-tag',
160 dest='smart_tag', action='store',
161 help='smart sync using manifest from a known tag')
129 162
130 g = p.add_option_group('repo Version options') 163 g = p.add_option_group('repo Version options')
131 g.add_option('--no-repo-verify', 164 g.add_option('--no-repo-verify',
@@ -135,20 +168,60 @@ later is required to fix a server side protocol bug.
135 dest='repo_upgraded', action='store_true', 168 dest='repo_upgraded', action='store_true',
136 help=SUPPRESS_HELP) 169 help=SUPPRESS_HELP)
137 170
138 def _FetchHelper(self, opt, project, lock, fetched, pm, sem): 171 def _FetchHelper(self, opt, project, lock, fetched, pm, sem, err_event):
139 if not project.Sync_NetworkHalf(quiet=opt.quiet): 172 """Main function of the fetch threads when jobs are > 1.
140 print >>sys.stderr, 'error: Cannot fetch %s' % project.name 173
141 if opt.force_broken: 174 Args:
142 print >>sys.stderr, 'warn: --force-broken, continuing to sync' 175 opt: Program options returned from optparse. See _Options().
143 else: 176 project: Project object for the project to fetch.
144 sem.release() 177 lock: Lock for accessing objects that are shared amongst multiple
145 sys.exit(1) 178 _FetchHelper() threads.
179 fetched: set object that we will add project.gitdir to when we're done
180 (with our lock held).
181 pm: Instance of a Project object. We will call pm.update() (with our
182 lock held).
183 sem: We'll release() this semaphore when we exit so that another thread
184 can be started up.
185 err_event: We'll set this event in the case of an error (after printing
186 out info about the error).
187 """
188 # We'll set to true once we've locked the lock.
189 did_lock = False
190
191 # Encapsulate everything in a try/except/finally so that:
192 # - We always set err_event in the case of an exception.
193 # - We always make sure we call sem.release().
194 # - We always make sure we unlock the lock if we locked it.
195 try:
196 try:
197 success = project.Sync_NetworkHalf(
198 quiet=opt.quiet,
199 current_branch_only=opt.current_branch_only,
200 clone_bundle=not opt.no_clone_bundle)
201
202 # Lock around all the rest of the code, since printing, updating a set
203 # and Progress.update() are not thread safe.
204 lock.acquire()
205 did_lock = True
206
207 if not success:
208 print >>sys.stderr, 'error: Cannot fetch %s' % project.name
209 if opt.force_broken:
210 print >>sys.stderr, 'warn: --force-broken, continuing to sync'
211 else:
212 raise _FetchError()
146 213
147 lock.acquire() 214 fetched.add(project.gitdir)
148 fetched.add(project.gitdir) 215 pm.update()
149 pm.update() 216 except _FetchError:
150 lock.release() 217 err_event.set()
151 sem.release() 218 except:
219 err_event.set()
220 raise
221 finally:
222 if did_lock:
223 lock.release()
224 sem.release()
152 225
153 def _Fetch(self, projects, opt): 226 def _Fetch(self, projects, opt):
154 fetched = set() 227 fetched = set()
@@ -157,7 +230,10 @@ later is required to fix a server side protocol bug.
157 if self.jobs == 1: 230 if self.jobs == 1:
158 for project in projects: 231 for project in projects:
159 pm.update() 232 pm.update()
160 if project.Sync_NetworkHalf(quiet=opt.quiet): 233 if project.Sync_NetworkHalf(
234 quiet=opt.quiet,
235 current_branch_only=opt.current_branch_only,
236 clone_bundle=not opt.no_clone_bundle):
161 fetched.add(project.gitdir) 237 fetched.add(project.gitdir)
162 else: 238 else:
163 print >>sys.stderr, 'error: Cannot fetch %s' % project.name 239 print >>sys.stderr, 'error: Cannot fetch %s' % project.name
@@ -169,7 +245,13 @@ later is required to fix a server side protocol bug.
169 threads = set() 245 threads = set()
170 lock = _threading.Lock() 246 lock = _threading.Lock()
171 sem = _threading.Semaphore(self.jobs) 247 sem = _threading.Semaphore(self.jobs)
248 err_event = _threading.Event()
172 for project in projects: 249 for project in projects:
250 # Check for any errors before starting any new threads.
251 # ...we'll let existing threads finish, though.
252 if err_event.isSet():
253 break
254
173 sem.acquire() 255 sem.acquire()
174 t = _threading.Thread(target = self._FetchHelper, 256 t = _threading.Thread(target = self._FetchHelper,
175 args = (opt, 257 args = (opt,
@@ -177,13 +259,19 @@ later is required to fix a server side protocol bug.
177 lock, 259 lock,
178 fetched, 260 fetched,
179 pm, 261 pm,
180 sem)) 262 sem,
263 err_event))
181 threads.add(t) 264 threads.add(t)
182 t.start() 265 t.start()
183 266
184 for t in threads: 267 for t in threads:
185 t.join() 268 t.join()
186 269
270 # If we saw an error, exit with code 1 so that other scripts can check.
271 if err_event.isSet():
272 print >>sys.stderr, '\nerror: Exited sync due to fetch errors'
273 sys.exit(1)
274
187 pm.end() 275 pm.end()
188 for project in projects: 276 for project in projects:
189 project.bare_git.gc('--auto') 277 project.bare_git.gc('--auto')
@@ -191,7 +279,7 @@ later is required to fix a server side protocol bug.
191 279
192 def UpdateProjectList(self): 280 def UpdateProjectList(self):
193 new_project_paths = [] 281 new_project_paths = []
194 for project in self.manifest.projects.values(): 282 for project in self.GetProjects(None, missing_ok=True):
195 if project.relpath: 283 if project.relpath:
196 new_project_paths.append(project.relpath) 284 new_project_paths.append(project.relpath)
197 file_name = 'project.list' 285 file_name = 'project.list'
@@ -220,7 +308,8 @@ later is required to fix a server side protocol bug.
220 worktree = os.path.join(self.manifest.topdir, path), 308 worktree = os.path.join(self.manifest.topdir, path),
221 relpath = path, 309 relpath = path,
222 revisionExpr = 'HEAD', 310 revisionExpr = 'HEAD',
223 revisionId = None) 311 revisionId = None,
312 groups = None)
224 313
225 if project.IsDirty(): 314 if project.IsDirty():
226 print >>sys.stderr, 'error: Cannot remove project "%s": \ 315 print >>sys.stderr, 'error: Cannot remove project "%s": \
@@ -251,34 +340,51 @@ uncommitted changes are present' % project.relpath
251 def Execute(self, opt, args): 340 def Execute(self, opt, args):
252 if opt.jobs: 341 if opt.jobs:
253 self.jobs = opt.jobs 342 self.jobs = opt.jobs
343 if self.jobs > 1:
344 soft_limit, _ = _rlimit_nofile()
345 self.jobs = min(self.jobs, (soft_limit - 5) / 3)
346
254 if opt.network_only and opt.detach_head: 347 if opt.network_only and opt.detach_head:
255 print >>sys.stderr, 'error: cannot combine -n and -d' 348 print >>sys.stderr, 'error: cannot combine -n and -d'
256 sys.exit(1) 349 sys.exit(1)
257 if opt.network_only and opt.local_only: 350 if opt.network_only and opt.local_only:
258 print >>sys.stderr, 'error: cannot combine -n and -l' 351 print >>sys.stderr, 'error: cannot combine -n and -l'
259 sys.exit(1) 352 sys.exit(1)
353 if opt.manifest_name and opt.smart_sync:
354 print >>sys.stderr, 'error: cannot combine -m and -s'
355 sys.exit(1)
356 if opt.manifest_name and opt.smart_tag:
357 print >>sys.stderr, 'error: cannot combine -m and -t'
358 sys.exit(1)
260 359
261 if opt.smart_sync: 360 if opt.manifest_name:
361 self.manifest.Override(opt.manifest_name)
362
363 if opt.smart_sync or opt.smart_tag:
262 if not self.manifest.manifest_server: 364 if not self.manifest.manifest_server:
263 print >>sys.stderr, \ 365 print >>sys.stderr, \
264 'error: cannot smart sync: no manifest server defined in manifest' 366 'error: cannot smart sync: no manifest server defined in manifest'
265 sys.exit(1) 367 sys.exit(1)
266 try: 368 try:
267 server = xmlrpclib.Server(self.manifest.manifest_server) 369 server = xmlrpclib.Server(self.manifest.manifest_server)
268 p = self.manifest.manifestProject 370 if opt.smart_sync:
269 b = p.GetBranch(p.CurrentBranch) 371 p = self.manifest.manifestProject
270 branch = b.merge 372 b = p.GetBranch(p.CurrentBranch)
271 if branch.startswith(R_HEADS): 373 branch = b.merge
272 branch = branch[len(R_HEADS):] 374 if branch.startswith(R_HEADS):
273 375 branch = branch[len(R_HEADS):]
274 env = os.environ.copy() 376
275 if (env.has_key('TARGET_PRODUCT') and 377 env = os.environ.copy()
276 env.has_key('TARGET_BUILD_VARIANT')): 378 if (env.has_key('TARGET_PRODUCT') and
277 target = '%s-%s' % (env['TARGET_PRODUCT'], 379 env.has_key('TARGET_BUILD_VARIANT')):
278 env['TARGET_BUILD_VARIANT']) 380 target = '%s-%s' % (env['TARGET_PRODUCT'],
279 [success, manifest_str] = server.GetApprovedManifest(branch, target) 381 env['TARGET_BUILD_VARIANT'])
382 [success, manifest_str] = server.GetApprovedManifest(branch, target)
383 else:
384 [success, manifest_str] = server.GetApprovedManifest(branch)
280 else: 385 else:
281 [success, manifest_str] = server.GetApprovedManifest(branch) 386 assert(opt.smart_tag)
387 [success, manifest_str] = server.GetManifest(opt.smart_tag)
282 388
283 if success: 389 if success:
284 manifest_name = "smart_sync_override.xml" 390 manifest_name = "smart_sync_override.xml"
@@ -313,7 +419,8 @@ uncommitted changes are present' % project.relpath
313 _PostRepoUpgrade(self.manifest) 419 _PostRepoUpgrade(self.manifest)
314 420
315 if not opt.local_only: 421 if not opt.local_only:
316 mp.Sync_NetworkHalf(quiet=opt.quiet) 422 mp.Sync_NetworkHalf(quiet=opt.quiet,
423 current_branch_only=opt.current_branch_only)
317 424
318 if mp.HasChanges: 425 if mp.HasChanges:
319 syncbuf = SyncBuffer(mp.config) 426 syncbuf = SyncBuffer(mp.config)
@@ -321,6 +428,8 @@ uncommitted changes are present' % project.relpath
321 if not syncbuf.Finish(): 428 if not syncbuf.Finish():
322 sys.exit(1) 429 sys.exit(1)
323 self.manifest._Unload() 430 self.manifest._Unload()
431 if opt.jobs is None:
432 self.jobs = self.manifest.default.sync_j
324 all = self.GetProjects(args, missing_ok=True) 433 all = self.GetProjects(args, missing_ok=True)
325 434
326 if not opt.local_only: 435 if not opt.local_only:
@@ -336,14 +445,7 @@ uncommitted changes are present' % project.relpath
336 # bail out now; the rest touches the working tree 445 # bail out now; the rest touches the working tree
337 return 446 return
338 447
339 if mp.HasChanges: 448 self.manifest._Unload()
340 syncbuf = SyncBuffer(mp.config)
341 mp.Sync_LocalHalf(syncbuf)
342 if not syncbuf.Finish():
343 sys.exit(1)
344 _ReloadManifest(self)
345 mp = self.manifest.manifestProject
346
347 all = self.GetProjects(args, missing_ok=True) 449 all = self.GetProjects(args, missing_ok=True)
348 missing = [] 450 missing = []
349 for project in all: 451 for project in all:
@@ -370,16 +472,10 @@ uncommitted changes are present' % project.relpath
370 if not syncbuf.Finish(): 472 if not syncbuf.Finish():
371 sys.exit(1) 473 sys.exit(1)
372 474
373def _ReloadManifest(cmd): 475 # If there's a notice that's supposed to print at the end of the sync, print
374 old = cmd.manifest 476 # it now...
375 new = cmd.GetManifest(reparse=True) 477 if self.manifest.notice:
376 478 print self.manifest.notice
377 if old.__class__ != new.__class__:
378 print >>sys.stderr, 'NOTICE: manifest format has changed ***'
379 new.Upgrade_Local(old)
380 else:
381 if new.notice:
382 print new.notice
383 479
384def _PostRepoUpgrade(manifest): 480def _PostRepoUpgrade(manifest):
385 for project in manifest.projects.values(): 481 for project in manifest.projects.values():
diff --git a/subcmds/upload.py b/subcmds/upload.py
index 20822096..c9312973 100644
--- a/subcmds/upload.py
+++ b/subcmds/upload.py
@@ -19,7 +19,8 @@ import sys
19 19
20from command import InteractiveCommand 20from command import InteractiveCommand
21from editor import Editor 21from editor import Editor
22from error import UploadError 22from error import HookError, UploadError
23from project import RepoHook
23 24
24UNUSUAL_COMMIT_THRESHOLD = 5 25UNUSUAL_COMMIT_THRESHOLD = 5
25 26
@@ -72,7 +73,7 @@ Configuration
72 73
73review.URL.autoupload: 74review.URL.autoupload:
74 75
75To disable the "Upload ... (y/n)?" prompt, you can set a per-project 76To disable the "Upload ... (y/N)?" prompt, you can set a per-project
76or global Git configuration option. If review.URL.autoupload is set 77or global Git configuration option. If review.URL.autoupload is set
77to "true" then repo will assume you always answer "y" at the prompt, 78to "true" then repo will assume you always answer "y" at the prompt,
78and will not prompt you further. If it is set to "false" then repo 79and will not prompt you further. If it is set to "false" then repo
@@ -102,6 +103,14 @@ or in the .git/config within the project. For example:
102 autoupload = true 103 autoupload = true
103 autocopy = johndoe@company.com,my-team-alias@company.com 104 autocopy = johndoe@company.com,my-team-alias@company.com
104 105
106review.URL.uploadtopic:
107
108To add a topic branch whenever uploading a commit, you can set a
109per-project or global Git option to do so. If review.URL.uploadtopic
110is set to "true" then repo will assume you always want the equivalent
111of the -t option to the repo command. If unset or set to "false" then
112repo will make use of only the command line option.
113
105References 114References
106---------- 115----------
107 116
@@ -119,6 +128,38 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
119 p.add_option('--cc', 128 p.add_option('--cc',
120 type='string', action='append', dest='cc', 129 type='string', action='append', dest='cc',
121 help='Also send email to these email addresses.') 130 help='Also send email to these email addresses.')
131 p.add_option('--br',
132 type='string', action='store', dest='branch',
133 help='Branch to upload.')
134 p.add_option('--cbr', '--current-branch',
135 dest='current_branch', action='store_true',
136 help='Upload current git branch.')
137 p.add_option('-d', '--draft',
138 action='store_true', dest='draft', default=False,
139 help='If specified, upload as a draft.')
140
141 # Options relating to upload hook. Note that verify and no-verify are NOT
142 # opposites of each other, which is why they store to different locations.
143 # We are using them to match 'git commit' syntax.
144 #
145 # Combinations:
146 # - no-verify=False, verify=False (DEFAULT):
147 # If stdout is a tty, can prompt about running upload hooks if needed.
148 # If user denies running hooks, the upload is cancelled. If stdout is
149 # not a tty and we would need to prompt about upload hooks, upload is
150 # cancelled.
151 # - no-verify=False, verify=True:
152 # Always run upload hooks with no prompt.
153 # - no-verify=True, verify=False:
154 # Never run upload hooks, but upload anyway (AKA bypass hooks).
155 # - no-verify=True, verify=True:
156 # Invalid
157 p.add_option('--no-verify',
158 dest='bypass_hooks', action='store_true',
159 help='Do not run the upload hook.')
160 p.add_option('--verify',
161 dest='allow_all_hooks', action='store_true',
162 help='Run the upload hook without prompting.')
122 163
123 def _SingleBranch(self, opt, branch, people): 164 def _SingleBranch(self, opt, branch, people):
124 project = branch.project 165 project = branch.project
@@ -135,7 +176,7 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
135 date = branch.date 176 date = branch.date
136 list = branch.commits 177 list = branch.commits
137 178
138 print 'Upload project %s/:' % project.relpath 179 print 'Upload project %s/ to remote branch %s:' % (project.relpath, project.revisionExpr)
139 print ' branch %s (%2d commit%s, %s):' % ( 180 print ' branch %s (%2d commit%s, %s):' % (
140 name, 181 name,
141 len(list), 182 len(list),
@@ -144,7 +185,7 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
144 for commit in list: 185 for commit in list:
145 print ' %s' % commit 186 print ' %s' % commit
146 187
147 sys.stdout.write('to %s (y/n)? ' % remote.review) 188 sys.stdout.write('to %s (y/N)? ' % remote.review)
148 answer = sys.stdin.readline().strip() 189 answer = sys.stdin.readline().strip()
149 answer = answer in ('y', 'Y', 'yes', '1', 'true', 't') 190 answer = answer in ('y', 'Y', 'yes', '1', 'true', 't')
150 191
@@ -175,11 +216,12 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
175 216
176 if b: 217 if b:
177 script.append('#') 218 script.append('#')
178 script.append('# branch %s (%2d commit%s, %s):' % ( 219 script.append('# branch %s (%2d commit%s, %s) to remote branch %s:' % (
179 name, 220 name,
180 len(list), 221 len(list),
181 len(list) != 1 and 's' or '', 222 len(list) != 1 and 's' or '',
182 date)) 223 date,
224 project.revisionExpr))
183 for commit in list: 225 for commit in list:
184 script.append('# %s' % commit) 226 script.append('# %s' % commit)
185 b[name] = branch 227 b[name] = branch
@@ -188,6 +230,11 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
188 branches[project.name] = b 230 branches[project.name] = b
189 script.append('') 231 script.append('')
190 232
233 script = [ x.encode('utf-8')
234 if issubclass(type(x), unicode)
235 else x
236 for x in script ]
237
191 script = Editor.EditString("\n".join(script)).split("\n") 238 script = Editor.EditString("\n".join(script)).split("\n")
192 239
193 project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$') 240 project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$')
@@ -267,7 +314,7 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
267 314
268 # if they want to auto upload, let's not ask because it could be automated 315 # if they want to auto upload, let's not ask because it could be automated
269 if answer is None: 316 if answer is None:
270 sys.stdout.write('Uncommitted changes in ' + branch.project.name + ' (did you forget to amend?). Continue uploading? (y/n) ') 317 sys.stdout.write('Uncommitted changes in ' + branch.project.name + ' (did you forget to amend?). Continue uploading? (y/N) ')
271 a = sys.stdin.readline().strip().lower() 318 a = sys.stdin.readline().strip().lower()
272 if a not in ('y', 'yes', 't', 'true', 'on'): 319 if a not in ('y', 'yes', 't', 'true', 'on'):
273 print >>sys.stderr, "skipping upload" 320 print >>sys.stderr, "skipping upload"
@@ -275,7 +322,12 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
275 branch.error = 'User aborted' 322 branch.error = 'User aborted'
276 continue 323 continue
277 324
278 branch.UploadForReview(people, auto_topic=opt.auto_topic) 325 # Check if topic branches should be sent to the server during upload
326 if opt.auto_topic is not True:
327 key = 'review.%s.uploadtopic' % branch.project.remote.review
328 opt.auto_topic = branch.project.config.GetBoolean(key)
329
330 branch.UploadForReview(people, auto_topic=opt.auto_topic, draft=opt.draft)
279 branch.uploaded = True 331 branch.uploaded = True
280 except UploadError, e: 332 except UploadError, e:
281 branch.error = e 333 branch.error = e
@@ -312,6 +364,29 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
312 pending = [] 364 pending = []
313 reviewers = [] 365 reviewers = []
314 cc = [] 366 cc = []
367 branch = None
368
369 if opt.branch:
370 branch = opt.branch
371
372 for project in project_list:
373 if opt.current_branch:
374 cbr = project.CurrentBranch
375 avail = [project.GetUploadableBranch(cbr)] if cbr else None
376 else:
377 avail = project.GetUploadableBranches(branch)
378 if avail:
379 pending.append((project, avail))
380
381 if pending and (not opt.bypass_hooks):
382 hook = RepoHook('pre-upload', self.manifest.repo_hooks_project,
383 self.manifest.topdir, abort_if_user_denies=True)
384 pending_proj_names = [project.name for (project, avail) in pending]
385 try:
386 hook.Run(opt.allow_all_hooks, project_list=pending_proj_names)
387 except HookError, e:
388 print >>sys.stderr, "ERROR: %s" % str(e)
389 return
315 390
316 if opt.reviewers: 391 if opt.reviewers:
317 reviewers = _SplitEmails(opt.reviewers) 392 reviewers = _SplitEmails(opt.reviewers)
@@ -319,11 +394,6 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
319 cc = _SplitEmails(opt.cc) 394 cc = _SplitEmails(opt.cc)
320 people = (reviewers,cc) 395 people = (reviewers,cc)
321 396
322 for project in project_list:
323 avail = project.GetUploadableBranches()
324 if avail:
325 pending.append((project, avail))
326
327 if not pending: 397 if not pending:
328 print >>sys.stdout, "no branches ready for upload" 398 print >>sys.stdout, "no branches ready for upload"
329 elif len(pending) == 1 and len(pending[0][1]) == 1: 399 elif len(pending) == 1 and len(pending[0][1]) == 1:
diff --git a/subcmds/version.py b/subcmds/version.py
index 83e77d0b..03195f88 100644
--- a/subcmds/version.py
+++ b/subcmds/version.py
@@ -19,6 +19,9 @@ from git_command import git
19from project import HEAD 19from project import HEAD
20 20
21class Version(Command, MirrorSafeCommand): 21class Version(Command, MirrorSafeCommand):
22 wrapper_version = None
23 wrapper_path = None
24
22 common = False 25 common = False
23 helpSummary = "Display the version of repo" 26 helpSummary = "Display the version of repo"
24 helpUsage = """ 27 helpUsage = """
@@ -31,5 +34,10 @@ class Version(Command, MirrorSafeCommand):
31 34
32 print 'repo version %s' % rp.work_git.describe(HEAD) 35 print 'repo version %s' % rp.work_git.describe(HEAD)
33 print ' (from %s)' % rem.url 36 print ' (from %s)' % rem.url
37
38 if Version.wrapper_path is not None:
39 print 'repo launcher version %s' % Version.wrapper_version
40 print ' (from %s)' % Version.wrapper_path
41
34 print git.version().strip() 42 print git.version().strip()
35 print 'Python %s' % sys.version 43 print 'Python %s' % sys.version