summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.flake83
-rw-r--r--.mailmap5
-rw-r--r--.pylintrc298
-rw-r--r--README.md14
-rw-r--r--SUBMITTING_PATCHES.md (renamed from SUBMITTING_PATCHES)52
-rw-r--r--command.py5
-rw-r--r--docs/manifest-format.txt7
-rw-r--r--git_config.py11
-rw-r--r--gitc_utils.py10
-rw-r--r--manifest_xml.py24
-rw-r--r--project.py161
-rwxr-xr-xrepo33
-rw-r--r--subcmds/abandon.py71
-rw-r--r--subcmds/init.py11
-rw-r--r--subcmds/start.py3
-rw-r--r--subcmds/status.py12
-rw-r--r--subcmds/sync.py110
-rw-r--r--subcmds/upload.py14
18 files changed, 407 insertions, 437 deletions
diff --git a/.flake8 b/.flake8
new file mode 100644
index 00000000..45ab6562
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,3 @@
1[flake8]
2max-line-length=80
3ignore=E111,E114,E402
diff --git a/.mailmap b/.mailmap
index f070b494..eb64bd21 100644
--- a/.mailmap
+++ b/.mailmap
@@ -1,8 +1,11 @@
1Anthony Newnam <anthony.newnam@garmin.com> Anthony <anthony@bnovc.com> 1Anthony Newnam <anthony.newnam@garmin.com> Anthony <anthony@bnovc.com>
2Shawn Pearce <sop@google.com> Shawn O. Pearce <sop@google.com> 2Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu xiuyun <xiuyun.hu@hisilicon.com>
3Hu Xiuyun <xiuyun.hu@hisilicon.com> Hu Xiuyun <clouds08@qq.com>
4Jelly Chen <chenguodong@huawei.com> chenguodong <chenguodong@huawei.com>
3Jia Bi <bijia@xiaomi.com> bijia <bijia@xiaomi.com> 5Jia Bi <bijia@xiaomi.com> bijia <bijia@xiaomi.com>
4JoonCheol Park <jooncheol@gmail.com> Jooncheol Park <jooncheol@gmail.com> 6JoonCheol Park <jooncheol@gmail.com> Jooncheol Park <jooncheol@gmail.com>
5Sergii Pylypenko <x.pelya.x@gmail.com> pelya <x.pelya.x@gmail.com> 7Sergii Pylypenko <x.pelya.x@gmail.com> pelya <x.pelya.x@gmail.com>
8Shawn Pearce <sop@google.com> Shawn O. Pearce <sop@google.com>
6Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@gmail.com> 9Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@gmail.com>
7Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@sonyericsson.com> 10Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@sonyericsson.com>
8Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> 11Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>
diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index 413d66a1..00000000
--- a/.pylintrc
+++ /dev/null
@@ -1,298 +0,0 @@
1# lint Python modules using external checkers.
2#
3# This is the main checker controling the other ones and the reports
4# generation. It is itself both a raw checker and an astng checker in order
5# to:
6# * handle message activation / deactivation at the module level
7# * handle some basic but necessary stats'data (number of classes, methods...)
8#
9[MASTER]
10
11# Specify a configuration file.
12#rcfile=
13
14# Python code to execute, usually for sys.path manipulation such as
15# pygtk.require().
16#init-hook=
17
18# Profiled execution.
19profile=no
20
21# Add <file or directory> to the black list. It should be a base name, not a
22# path. You may set this option multiple times.
23ignore=SVN
24
25# Pickle collected data for later comparisons.
26persistent=yes
27
28# Set the cache size for astng objects.
29cache-size=500
30
31# List of plugins (as comma separated values of python modules names) to load,
32# usually to register additional checkers.
33load-plugins=
34
35
36[MESSAGES CONTROL]
37
38# Enable only checker(s) with the given id(s). This option conflicts with the
39# disable-checker option
40#enable-checker=
41
42# Enable all checker(s) except those with the given id(s). This option
43# conflicts with the enable-checker option
44#disable-checker=
45
46# Enable all messages in the listed categories.
47#enable-msg-cat=
48
49# Disable all messages in the listed categories.
50#disable-msg-cat=
51
52# Enable the message(s) with the given id(s).
53enable=RP0004
54
55# Disable the message(s) with the given id(s).
56disable=C0326,R0903,R0912,R0913,R0914,R0915,W0141,C0111,C0103,W0603,W0703,R0911,C0301,C0302,R0902,R0904,W0142,W0212,E1101,E1103,R0201,W0201,W0122,W0232,RP0001,RP0003,RP0101,RP0002,RP0401,RP0701,RP0801,F0401,E0611,R0801,I0011
57
58[REPORTS]
59
60# set the output format. Available formats are text, parseable, colorized, msvs
61# (visual studio) and html
62output-format=text
63
64# Put messages in a separate file for each module / package specified on the
65# command line instead of printing them on stdout. Reports (if any) will be
66# written in a file name "pylint_global.[txt|html]".
67files-output=no
68
69# Tells whether to display a full report or only the messages
70reports=yes
71
72# Python expression which should return a note less than 10 (10 is the highest
73# note).You have access to the variables errors warning, statement which
74# respectivly contain the number of errors / warnings messages and the total
75# number of statements analyzed. This is used by the global evaluation report
76# (R0004).
77evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
78
79# Add a comment according to your evaluation note. This is used by the global
80# evaluation report (R0004).
81comment=no
82
83# checks for
84# * unused variables / imports
85# * undefined variables
86# * redefinition of variable from builtins or from an outer scope
87# * use of variable before assigment
88#
89[VARIABLES]
90
91# Tells whether we should check for unused import in __init__ files.
92init-import=no
93
94# A regular expression matching names used for dummy variables (i.e. not used).
95dummy-variables-rgx=_|dummy
96
97# List of additional names supposed to be defined in builtins. Remember that
98# you should avoid to define new builtins when possible.
99additional-builtins=
100
101
102# try to find bugs in the code using type inference
103#
104[TYPECHECK]
105
106# Tells whether missing members accessed in mixin class should be ignored. A
107# mixin class is detected if its name ends with "mixin" (case insensitive).
108ignore-mixin-members=yes
109
110# List of classes names for which member attributes should not be checked
111# (useful for classes with attributes dynamicaly set).
112ignored-classes=SQLObject
113
114# When zope mode is activated, consider the acquired-members option to ignore
115# access to some undefined attributes.
116zope=no
117
118# List of members which are usually get through zope's acquisition mecanism and
119# so shouldn't trigger E0201 when accessed (need zope=yes to be considered).
120acquired-members=REQUEST,acl_users,aq_parent
121
122
123# checks for :
124# * doc strings
125# * modules / classes / functions / methods / arguments / variables name
126# * number of arguments, local variables, branchs, returns and statements in
127# functions, methods
128# * required module attributes
129# * dangerous default values as arguments
130# * redefinition of function / method / class
131# * uses of the global statement
132#
133[BASIC]
134
135# Required attributes for module, separated by a comma
136required-attributes=
137
138# Regular expression which should only match functions or classes name which do
139# not require a docstring
140no-docstring-rgx=_main|__.*__
141
142# Regular expression which should only match correct module names
143module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
144
145# Regular expression which should only match correct module level names
146const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))|(log)$
147
148# Regular expression which should only match correct class names
149class-rgx=[A-Z_][a-zA-Z0-9]+$
150
151# Regular expression which should only match correct function names
152function-rgx=[a-z_][a-z0-9_]{2,30}$
153
154# Regular expression which should only match correct method names
155method-rgx=[a-z_][a-z0-9_]{2,30}$
156
157# Regular expression which should only match correct instance attribute names
158attr-rgx=[a-z_][a-z0-9_]{2,30}$
159
160# Regular expression which should only match correct argument names
161argument-rgx=[a-z_][a-z0-9_]{2,30}$
162
163# Regular expression which should only match correct variable names
164variable-rgx=[a-z_][a-z0-9_]{2,30}$
165
166# Regular expression which should only match correct list comprehension /
167# generator expression variable names
168inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
169
170# Good variable names which should always be accepted, separated by a comma
171good-names=i,j,k,ex,Run,_,e,d1,d2,v,f,l,d
172
173# Bad variable names which should always be refused, separated by a comma
174bad-names=foo,bar,baz,toto,tutu,tata
175
176# List of builtins function names that should not be used, separated by a comma
177bad-functions=map,filter,apply,input
178
179
180# checks for sign of poor/misdesign:
181# * number of methods, attributes, local variables...
182# * size, complexity of functions, methods
183#
184[DESIGN]
185
186# Maximum number of arguments for function / method
187max-args=5
188
189# Maximum number of locals for function / method body
190max-locals=15
191
192# Maximum number of return / yield for function / method body
193max-returns=6
194
195# Maximum number of branch for function / method body
196max-branchs=12
197
198# Maximum number of statements in function / method body
199max-statements=50
200
201# Maximum number of parents for a class (see R0901).
202max-parents=7
203
204# Maximum number of attributes for a class (see R0902).
205max-attributes=20
206
207# Minimum number of public methods for a class (see R0903).
208min-public-methods=2
209
210# Maximum number of public methods for a class (see R0904).
211max-public-methods=30
212
213
214# checks for
215# * external modules dependencies
216# * relative / wildcard imports
217# * cyclic imports
218# * uses of deprecated modules
219#
220[IMPORTS]
221
222# Deprecated modules which should not be used, separated by a comma
223deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
224
225# Create a graph of every (i.e. internal and external) dependencies in the
226# given file (report R0402 must not be disabled)
227import-graph=
228
229# Create a graph of external dependencies in the given file (report R0402 must
230# not be disabled)
231ext-import-graph=
232
233# Create a graph of internal dependencies in the given file (report R0402 must
234# not be disabled)
235int-import-graph=
236
237
238# checks for :
239# * methods without self as first argument
240# * overridden methods signature
241# * access only to existant members via self
242# * attributes not defined in the __init__ method
243# * supported interfaces implementation
244# * unreachable code
245#
246[CLASSES]
247
248# List of interface methods to ignore, separated by a comma. This is used for
249# instance to not check methods defines in Zope's Interface base class.
250ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
251
252# List of method names used to declare (i.e. assign) instance attributes.
253defining-attr-methods=__init__,__new__,setUp
254
255
256# checks for similarities and duplicated code. This computation may be
257# memory / CPU intensive, so you should disable it if you experiments some
258# problems.
259#
260[SIMILARITIES]
261
262# Minimum lines number of a similarity.
263min-similarity-lines=4
264
265# Ignore comments when computing similarities.
266ignore-comments=yes
267
268# Ignore docstrings when computing similarities.
269ignore-docstrings=yes
270
271
272# checks for:
273# * warning notes in the code like FIXME, XXX
274# * PEP 263: source code with non ascii character but no encoding declaration
275#
276[MISCELLANEOUS]
277
278# List of note tags to take in consideration, separated by a comma.
279notes=FIXME,XXX,TODO
280
281
282# checks for :
283# * unauthorized constructions
284# * strict indentation
285# * line length
286# * use of <> instead of !=
287#
288[FORMAT]
289
290# Maximum number of characters on a single line.
291max-line-length=80
292
293# Maximum number of lines in a module
294max-module-lines=1000
295
296# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
297# tab). In repo it is 2 spaces.
298indent-string=' '
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..e35f8e99
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
1# repo
2
3Repo is a tool built on top of Git. Repo helps manage many Git repositories,
4does the uploads to revision control systems, and automates parts of the
5development workflow. Repo is not meant to replace Git, only to make it
6easier to work with Git. The repo command is an executable Python script
7that you can put anywhere in your path.
8
9* Homepage: https://code.google.com/p/git-repo/
10* Bug reports: https://code.google.com/p/git-repo/issues/
11* Source: https://code.google.com/p/git-repo/
12* Overview: https://source.android.com/source/developing.html
13* Docs: https://source.android.com/source/using-repo.html
14* [Submitting patches](./SUBMITTING_PATCHES.md)
diff --git a/SUBMITTING_PATCHES b/SUBMITTING_PATCHES.md
index 8656ee7d..07f76616 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES.md
@@ -1,17 +1,17 @@
1Short Version: 1# Short Version
2 2
3 - Make small logical changes. 3 - Make small logical changes.
4 - Provide a meaningful commit message. 4 - Provide a meaningful commit message.
5 - Check for coding errors with pylint 5 - Check for coding errors and style nits with pyflakes and flake8
6 - Make sure all code is under the Apache License, 2.0. 6 - Make sure all code is under the Apache License, 2.0.
7 - Publish your changes for review. 7 - Publish your changes for review.
8 - Make corrections if requested. 8 - Make corrections if requested.
9 - Verify your changes on gerrit so they can be submitted. 9 - Verify your changes on gerrit so they can be submitted.
10 10
11 git push https://gerrit-review.googlesource.com/git-repo HEAD:refs/for/master 11 `git push https://gerrit-review.googlesource.com/git-repo HEAD:refs/for/master`
12 12
13 13
14Long Version: 14# Long Version
15 15
16I wanted a file describing how to submit patches for repo, 16I wanted a file describing how to submit patches for repo,
17so I started with the one found in the core Git distribution 17so I started with the one found in the core Git distribution
@@ -19,10 +19,10 @@ so I started with the one found in the core Git distribution
19patch submission guidelines for the Linux kernel. 19patch submission guidelines for the Linux kernel.
20 20
21However there are some differences, so please review and familiarize 21However there are some differences, so please review and familiarize
22yourself with the following relevant bits: 22yourself with the following relevant bits.
23 23
24 24
25(1) Make separate commits for logically separate changes. 25## Make separate commits for logically separate changes.
26 26
27Unless your patch is really trivial, you should not be sending 27Unless your patch is really trivial, you should not be sending
28out a patch that was generated between your working tree and your 28out a patch that was generated between your working tree and your
@@ -36,14 +36,34 @@ If your description starts to get too long, that's a sign that you
36probably need to split up your commit to finer grained pieces. 36probably need to split up your commit to finer grained pieces.
37 37
38 38
39(2) Check for coding errors with pylint 39## Check for coding errors and style nits with pyflakes and flake8
40 40
41Run pylint on changed modules using the provided configuration: 41### Coding errors
42 42
43 pylint --rcfile=.pylintrc file.py 43Run `pyflakes` on changed modules:
44 44
45 pyflakes file.py
45 46
46(3) Check the license 47Ideally there should be no new errors or warnings introduced.
48
49### Style violations
50
51Run `flake8` on changes modules:
52
53 flake8 file.py
54
55Note that repo generally follows [Google's python style guide]
56(https://google.github.io/styleguide/pyguide.html) rather than [PEP 8]
57(https://www.python.org/dev/peps/pep-0008/), so it's possible that
58the output of `flake8` will be quite noisy. It's not mandatory to
59avoid all warnings, but at least the maximum line length should be
60followed.
61
62If there are many occurrences of the same warning that cannot be
63avoided without going against the Google style guide, these may be
64suppressed in the included `.flake8` file.
65
66## Check the license
47 67
48repo is licensed under the Apache License, 2.0. 68repo is licensed under the Apache License, 2.0.
49 69
@@ -59,7 +79,7 @@ your patch. It is virtually impossible to remove a patch once it
59has been applied and pushed out. 79has been applied and pushed out.
60 80
61 81
62(4) Sending your patches. 82## Sending your patches.
63 83
64Do not email your patches to anyone. 84Do not email your patches to anyone.
65 85
@@ -91,23 +111,23 @@ to get the ChangeId added.
91Push your patches over HTTPS to the review server, possibly through 111Push your patches over HTTPS to the review server, possibly through
92a remembered remote to make this easier in the future: 112a remembered remote to make this easier in the future:
93 113
94 git config remote.review.url https://gerrit-review.googlesource.com/git-repo 114 git config remote.review.url https://gerrit-review.googlesource.com/git-repo
95 git config remote.review.push HEAD:refs/for/master 115 git config remote.review.push HEAD:refs/for/master
96 116
97 git push review 117 git push review
98 118
99You will be automatically emailed a copy of your commits, and any 119You will be automatically emailed a copy of your commits, and any
100comments made by the project maintainers. 120comments made by the project maintainers.
101 121
102 122
103(5) Make changes if requested 123## Make changes if requested
104 124
105The project maintainer who reviews your changes might request changes to your 125The project maintainer who reviews your changes might request changes to your
106commit. If you make the requested changes you will need to amend your commit 126commit. If you make the requested changes you will need to amend your commit
107and push it to the review server again. 127and push it to the review server again.
108 128
109 129
110(6) Verify your changes on gerrit 130## Verify your changes on gerrit
111 131
112After you receive a Code-Review+2 from the maintainer, select the Verified 132After you receive a Code-Review+2 from the maintainer, select the Verified
113button on the gerrit page for the change. This verifies that you have tested 133button on the gerrit page for the change. This verifies that you have tested
diff --git a/command.py b/command.py
index bc2f9501..2ff0a344 100644
--- a/command.py
+++ b/command.py
@@ -119,6 +119,11 @@ class Command(object):
119 except KeyError: 119 except KeyError:
120 oldpath = path 120 oldpath = path
121 path = os.path.dirname(path) 121 path = os.path.dirname(path)
122 if not project and path == manifest.topdir:
123 try:
124 project = self._by_path[path]
125 except KeyError:
126 pass
122 else: 127 else:
123 try: 128 try:
124 project = self._by_path[path] 129 project = self._by_path[path]
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt
index 8fd9137c..2a07f199 100644
--- a/docs/manifest-format.txt
+++ b/docs/manifest-format.txt
@@ -35,6 +35,7 @@ following DTD:
35 <!ATTLIST remote name ID #REQUIRED> 35 <!ATTLIST remote name ID #REQUIRED>
36 <!ATTLIST remote alias CDATA #IMPLIED> 36 <!ATTLIST remote alias CDATA #IMPLIED>
37 <!ATTLIST remote fetch CDATA #REQUIRED> 37 <!ATTLIST remote fetch CDATA #REQUIRED>
38 <!ATTLIST remote pushurl CDATA #IMPLIED>
38 <!ATTLIST remote review CDATA #IMPLIED> 39 <!ATTLIST remote review CDATA #IMPLIED>
39 <!ATTLIST remote revision CDATA #IMPLIED> 40 <!ATTLIST remote revision CDATA #IMPLIED>
40 41
@@ -125,6 +126,12 @@ Attribute `fetch`: The Git URL prefix for all projects which use
125this remote. Each project's name is appended to this prefix to 126this remote. Each project's name is appended to this prefix to
126form the actual URL used to clone the project. 127form the actual URL used to clone the project.
127 128
129Attribute `pushurl`: The Git "push" URL prefix for all projects
130which use this remote. Each project's name is appended to this
131prefix to form the actual URL used to "git push" the project.
132This attribute is optional; if not specified then "git push"
133will use the same URL as the `fetch` attribute.
134
128Attribute `review`: Hostname of the Gerrit server where reviews 135Attribute `review`: Hostname of the Gerrit server where reviews
129are uploaded to by `repo upload`. This attribute is optional; 136are uploaded to by `repo upload`. This attribute is optional;
130if not specified then `repo upload` will not function. 137if not specified then `repo upload` will not function.
diff --git a/git_config.py b/git_config.py
index 0379181a..e2236785 100644
--- a/git_config.py
+++ b/git_config.py
@@ -464,9 +464,13 @@ def _open_ssh(host, port=None):
464 % (host,port, str(e)), file=sys.stderr) 464 % (host,port, str(e)), file=sys.stderr)
465 return False 465 return False
466 466
467 time.sleep(1)
468 ssh_died = (p.poll() is not None)
469 if ssh_died:
470 return False
471
467 _master_processes.append(p) 472 _master_processes.append(p)
468 _master_keys.add(key) 473 _master_keys.add(key)
469 time.sleep(1)
470 return True 474 return True
471 finally: 475 finally:
472 _master_keys_lock.release() 476 _master_keys_lock.release()
@@ -568,6 +572,7 @@ class Remote(object):
568 self._config = config 572 self._config = config
569 self.name = name 573 self.name = name
570 self.url = self._Get('url') 574 self.url = self._Get('url')
575 self.pushUrl = self._Get('pushurl')
571 self.review = self._Get('review') 576 self.review = self._Get('review')
572 self.projectname = self._Get('projectname') 577 self.projectname = self._Get('projectname')
573 self.fetch = list(map(RefSpec.FromString, 578 self.fetch = list(map(RefSpec.FromString,
@@ -694,6 +699,10 @@ class Remote(object):
694 """Save this remote to the configuration. 699 """Save this remote to the configuration.
695 """ 700 """
696 self._Set('url', self.url) 701 self._Set('url', self.url)
702 if self.pushUrl is not None:
703 self._Set('pushurl', self.pushUrl + '/' + self.projectname)
704 else:
705 self._Set('pushurl', self.pushUrl)
697 self._Set('review', self.review) 706 self._Set('review', self.review)
698 self._Set('projectname', self.projectname) 707 self._Set('projectname', self.projectname)
699 self._Set('fetch', list(map(str, self.fetch))) 708 self._Set('fetch', list(map(str, self.fetch)))
diff --git a/gitc_utils.py b/gitc_utils.py
index a388dc27..0d4a5c38 100644
--- a/gitc_utils.py
+++ b/gitc_utils.py
@@ -24,7 +24,9 @@ import git_command
24import git_config 24import git_config
25import wrapper 25import wrapper
26 26
27NUM_BATCH_RETRIEVE_REVISIONID = 300 27from error import ManifestParseError
28
29NUM_BATCH_RETRIEVE_REVISIONID = 32
28 30
29def get_gitc_manifest_dir(): 31def get_gitc_manifest_dir():
30 return wrapper.Wrapper().get_gitc_manifest_dir() 32 return wrapper.Wrapper().get_gitc_manifest_dir()
@@ -54,7 +56,11 @@ def _set_project_revisions(projects):
54 if gitcmd.Wait(): 56 if gitcmd.Wait():
55 print('FATAL: Failed to retrieve revisionExpr for %s' % proj) 57 print('FATAL: Failed to retrieve revisionExpr for %s' % proj)
56 sys.exit(1) 58 sys.exit(1)
57 proj.revisionExpr = gitcmd.stdout.split('\t')[0] 59 revisionExpr = gitcmd.stdout.split('\t')[0]
60 if not revisionExpr:
61 raise(ManifestParseError('Invalid SHA-1 revision project %s (%s)' %
62 (proj.remote.url, proj.revisionExpr)))
63 proj.revisionExpr = revisionExpr
58 64
59def _manifest_groups(manifest): 65def _manifest_groups(manifest):
60 """Returns the manifest group string that should be synced 66 """Returns the manifest group string that should be synced
diff --git a/manifest_xml.py b/manifest_xml.py
index 295493de..0859e1fb 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -40,8 +40,18 @@ LOCAL_MANIFEST_NAME = 'local_manifest.xml'
40LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' 40LOCAL_MANIFESTS_DIR_NAME = 'local_manifests'
41 41
42# urljoin gets confused if the scheme is not known. 42# urljoin gets confused if the scheme is not known.
43urllib.parse.uses_relative.extend(['ssh', 'git', 'persistent-https', 'rpc']) 43urllib.parse.uses_relative.extend([
44urllib.parse.uses_netloc.extend(['ssh', 'git', 'persistent-https', 'rpc']) 44 'ssh',
45 'git',
46 'persistent-https',
47 'sso',
48 'rpc'])
49urllib.parse.uses_netloc.extend([
50 'ssh',
51 'git',
52 'persistent-https',
53 'sso',
54 'rpc'])
45 55
46class _Default(object): 56class _Default(object):
47 """Project defaults within the manifest.""" 57 """Project defaults within the manifest."""
@@ -64,11 +74,13 @@ class _XmlRemote(object):
64 name, 74 name,
65 alias=None, 75 alias=None,
66 fetch=None, 76 fetch=None,
77 pushUrl=None,
67 manifestUrl=None, 78 manifestUrl=None,
68 review=None, 79 review=None,
69 revision=None): 80 revision=None):
70 self.name = name 81 self.name = name
71 self.fetchUrl = fetch 82 self.fetchUrl = fetch
83 self.pushUrl = pushUrl
72 self.manifestUrl = manifestUrl 84 self.manifestUrl = manifestUrl
73 self.remoteAlias = alias 85 self.remoteAlias = alias
74 self.reviewUrl = review 86 self.reviewUrl = review
@@ -104,6 +116,7 @@ class _XmlRemote(object):
104 remoteName = self.remoteAlias 116 remoteName = self.remoteAlias
105 return RemoteSpec(remoteName, 117 return RemoteSpec(remoteName,
106 url=url, 118 url=url,
119 pushUrl=self.pushUrl,
107 review=self.reviewUrl, 120 review=self.reviewUrl,
108 orig_name=self.name) 121 orig_name=self.name)
109 122
@@ -160,6 +173,8 @@ class XmlManifest(object):
160 root.appendChild(e) 173 root.appendChild(e)
161 e.setAttribute('name', r.name) 174 e.setAttribute('name', r.name)
162 e.setAttribute('fetch', r.fetchUrl) 175 e.setAttribute('fetch', r.fetchUrl)
176 if r.pushUrl is not None:
177 e.setAttribute('pushurl', r.pushUrl)
163 if r.remoteAlias is not None: 178 if r.remoteAlias is not None:
164 e.setAttribute('alias', r.remoteAlias) 179 e.setAttribute('alias', r.remoteAlias)
165 if r.reviewUrl is not None: 180 if r.reviewUrl is not None:
@@ -639,6 +654,9 @@ class XmlManifest(object):
639 if alias == '': 654 if alias == '':
640 alias = None 655 alias = None
641 fetch = self._reqatt(node, 'fetch') 656 fetch = self._reqatt(node, 'fetch')
657 pushUrl = node.getAttribute('pushurl')
658 if pushUrl == '':
659 pushUrl = None
642 review = node.getAttribute('review') 660 review = node.getAttribute('review')
643 if review == '': 661 if review == '':
644 review = None 662 review = None
@@ -646,7 +664,7 @@ class XmlManifest(object):
646 if revision == '': 664 if revision == '':
647 revision = None 665 revision = None
648 manifestUrl = self.manifestProject.config.GetString('remote.origin.url') 666 manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
649 return _XmlRemote(name, alias, fetch, manifestUrl, review, revision) 667 return _XmlRemote(name, alias, fetch, pushUrl, manifestUrl, review, revision)
650 668
651 def _ParseDefault(self, node): 669 def _ParseDefault(self, node):
652 """ 670 """
diff --git a/project.py b/project.py
index 9a7128af..0d60fc6e 100644
--- a/project.py
+++ b/project.py
@@ -40,7 +40,13 @@ from trace import IsTrace, Trace
40from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M 40from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
41 41
42from pyversion import is_python3 42from pyversion import is_python3
43if not is_python3(): 43if is_python3():
44 import urllib.parse
45else:
46 import imp
47 import urlparse
48 urllib = imp.new_module('urllib')
49 urllib.parse = urlparse
44 # pylint:disable=W0622 50 # pylint:disable=W0622
45 input = raw_input 51 input = raw_input
46 # pylint:enable=W0622 52 # pylint:enable=W0622
@@ -314,11 +320,13 @@ class RemoteSpec(object):
314 def __init__(self, 320 def __init__(self,
315 name, 321 name,
316 url=None, 322 url=None,
323 pushUrl=None,
317 review=None, 324 review=None,
318 revision=None, 325 revision=None,
319 orig_name=None): 326 orig_name=None):
320 self.name = name 327 self.name = name
321 self.url = url 328 self.url = url
329 self.pushUrl = pushUrl
322 self.review = review 330 self.review = review
323 self.revision = revision 331 self.revision = revision
324 self.orig_name = orig_name 332 self.orig_name = orig_name
@@ -343,6 +351,7 @@ class RepoHook(object):
343 hook_type, 351 hook_type,
344 hooks_project, 352 hooks_project,
345 topdir, 353 topdir,
354 manifest_url,
346 abort_if_user_denies=False): 355 abort_if_user_denies=False):
347 """RepoHook constructor. 356 """RepoHook constructor.
348 357
@@ -356,11 +365,13 @@ class RepoHook(object):
356 topdir: Repo's top directory (the one containing the .repo directory). 365 topdir: Repo's top directory (the one containing the .repo directory).
357 Scripts will run with CWD as this directory. If you have a manifest, 366 Scripts will run with CWD as this directory. If you have a manifest,
358 this is manifest.topdir 367 this is manifest.topdir
368 manifest_url: The URL to the manifest git repo.
359 abort_if_user_denies: If True, we'll throw a HookError() if the user 369 abort_if_user_denies: If True, we'll throw a HookError() if the user
360 doesn't allow us to run the hook. 370 doesn't allow us to run the hook.
361 """ 371 """
362 self._hook_type = hook_type 372 self._hook_type = hook_type
363 self._hooks_project = hooks_project 373 self._hooks_project = hooks_project
374 self._manifest_url = manifest_url
364 self._topdir = topdir 375 self._topdir = topdir
365 self._abort_if_user_denies = abort_if_user_denies 376 self._abort_if_user_denies = abort_if_user_denies
366 377
@@ -409,9 +420,9 @@ class RepoHook(object):
409 def _CheckForHookApproval(self): 420 def _CheckForHookApproval(self):
410 """Check to see whether this hook has been approved. 421 """Check to see whether this hook has been approved.
411 422
412 We'll look at the hash of all of the hooks. If this matches the hash that 423 We'll accept approval of manifest URLs if they're using secure transports.
413 the user last approved, we're done. If it doesn't, we'll ask the user 424 This way the user can say they trust the manifest hoster. For insecure
414 about approval. 425 hosts, we fall back to checking the hash of the hooks repo.
415 426
416 Note that we ask permission for each individual hook even though we use 427 Note that we ask permission for each individual hook even though we use
417 the hash of all hooks when detecting changes. We'd like the user to be 428 the hash of all hooks when detecting changes. We'd like the user to be
@@ -425,44 +436,58 @@ class RepoHook(object):
425 HookError: Raised if the user doesn't approve and abort_if_user_denies 436 HookError: Raised if the user doesn't approve and abort_if_user_denies
426 was passed to the consturctor. 437 was passed to the consturctor.
427 """ 438 """
428 hooks_config = self._hooks_project.config 439 if self._ManifestUrlHasSecureScheme():
429 git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type 440 return self._CheckForHookApprovalManifest()
441 else:
442 return self._CheckForHookApprovalHash()
443
444 def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
445 changed_prompt):
446 """Check for approval for a particular attribute and hook.
447
448 Args:
449 subkey: The git config key under [repo.hooks.<hook_type>] to store the
450 last approved string.
451 new_val: The new value to compare against the last approved one.
452 main_prompt: Message to display to the user to ask for approval.
453 changed_prompt: Message explaining why we're re-asking for approval.
430 454
431 # Get the last hash that the user approved for this hook; may be None. 455 Returns:
432 old_hash = hooks_config.GetString(git_approval_key) 456 True if this hook is approved to run; False otherwise.
457
458 Raises:
459 HookError: Raised if the user doesn't approve and abort_if_user_denies
460 was passed to the consturctor.
461 """
462 hooks_config = self._hooks_project.config
463 git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
433 464
434 # Get the current hash so we can tell if scripts changed since approval. 465 # Get the last value that the user approved for this hook; may be None.
435 new_hash = self._GetHash() 466 old_val = hooks_config.GetString(git_approval_key)
436 467
437 if old_hash is not None: 468 if old_val is not None:
438 # User previously approved hook and asked not to be prompted again. 469 # User previously approved hook and asked not to be prompted again.
439 if new_hash == old_hash: 470 if new_val == old_val:
440 # Approval matched. We're done. 471 # Approval matched. We're done.
441 return True 472 return True
442 else: 473 else:
443 # Give the user a reason why we're prompting, since they last told 474 # Give the user a reason why we're prompting, since they last told
444 # us to "never ask again". 475 # us to "never ask again".
445 prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % ( 476 prompt = 'WARNING: %s\n\n' % (changed_prompt,)
446 self._hook_type)
447 else: 477 else:
448 prompt = '' 478 prompt = ''
449 479
450 # Prompt the user if we're not on a tty; on a tty we'll assume "no". 480 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
451 if sys.stdout.isatty(): 481 if sys.stdout.isatty():
452 prompt += ('Repo %s run the script:\n' 482 prompt += main_prompt + ' (yes/always/NO)? '
453 ' %s\n'
454 '\n'
455 'Do you want to allow this script to run '
456 '(yes/yes-never-ask-again/NO)? ') % (self._GetMustVerb(),
457 self._script_fullpath)
458 response = input(prompt).lower() 483 response = input(prompt).lower()
459 print() 484 print()
460 485
461 # User is doing a one-time approval. 486 # User is doing a one-time approval.
462 if response in ('y', 'yes'): 487 if response in ('y', 'yes'):
463 return True 488 return True
464 elif response == 'yes-never-ask-again': 489 elif response == 'always':
465 hooks_config.SetString(git_approval_key, new_hash) 490 hooks_config.SetString(git_approval_key, new_val)
466 return True 491 return True
467 492
468 # For anything else, we'll assume no approval. 493 # For anything else, we'll assume no approval.
@@ -472,6 +497,40 @@ class RepoHook(object):
472 497
473 return False 498 return False
474 499
500 def _ManifestUrlHasSecureScheme(self):
501 """Check if the URI for the manifest is a secure transport."""
502 secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
503 parse_results = urllib.parse.urlparse(self._manifest_url)
504 return parse_results.scheme in secure_schemes
505
506 def _CheckForHookApprovalManifest(self):
507 """Check whether the user has approved this manifest host.
508
509 Returns:
510 True if this hook is approved to run; False otherwise.
511 """
512 return self._CheckForHookApprovalHelper(
513 'approvedmanifest',
514 self._manifest_url,
515 'Run hook scripts from %s' % (self._manifest_url,),
516 'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
517
518 def _CheckForHookApprovalHash(self):
519 """Check whether the user has approved the hooks repo.
520
521 Returns:
522 True if this hook is approved to run; False otherwise.
523 """
524 prompt = ('Repo %s run the script:\n'
525 ' %s\n'
526 '\n'
527 'Do you want to allow this script to run')
528 return self._CheckForHookApprovalHelper(
529 'approvedhash',
530 self._GetHash(),
531 prompt % (self._GetMustVerb(), self._script_fullpath),
532 'Scripts have changed since %s was allowed.' % (self._hook_type,))
533
475 def _ExecuteHook(self, **kwargs): 534 def _ExecuteHook(self, **kwargs):
476 """Actually execute the given hook. 535 """Actually execute the given hook.
477 536
@@ -628,7 +687,7 @@ class Project(object):
628 self.gitdir = gitdir.replace('\\', '/') 687 self.gitdir = gitdir.replace('\\', '/')
629 self.objdir = objdir.replace('\\', '/') 688 self.objdir = objdir.replace('\\', '/')
630 if worktree: 689 if worktree:
631 self.worktree = worktree.replace('\\', '/') 690 self.worktree = os.path.normpath(worktree.replace('\\', '/'))
632 else: 691 else:
633 self.worktree = None 692 self.worktree = None
634 self.relpath = relpath 693 self.relpath = relpath
@@ -852,11 +911,13 @@ class Project(object):
852 else: 911 else:
853 return False 912 return False
854 913
855 def PrintWorkTreeStatus(self, output_redir=None): 914 def PrintWorkTreeStatus(self, output_redir=None, quiet=False):
856 """Prints the status of the repository to stdout. 915 """Prints the status of the repository to stdout.
857 916
858 Args: 917 Args:
859 output: If specified, redirect the output to this object. 918 output: If specified, redirect the output to this object.
919 quiet: If True then only print the project name. Do not print
920 the modified files, branch name, etc.
860 """ 921 """
861 if not os.path.isdir(self.worktree): 922 if not os.path.isdir(self.worktree):
862 if output_redir is None: 923 if output_redir is None:
@@ -882,6 +943,10 @@ class Project(object):
882 out.redirect(output_redir) 943 out.redirect(output_redir)
883 out.project('project %-40s', self.relpath + '/ ') 944 out.project('project %-40s', self.relpath + '/ ')
884 945
946 if quiet:
947 out.nl()
948 return 'DIRTY'
949
885 branch = self.CurrentBranch 950 branch = self.CurrentBranch
886 if branch is None: 951 if branch is None:
887 out.nobranch('(*** NO BRANCH ***)') 952 out.nobranch('(*** NO BRANCH ***)')
@@ -1199,13 +1264,18 @@ class Project(object):
1199 elif self.manifest.default.sync_c: 1264 elif self.manifest.default.sync_c:
1200 current_branch_only = True 1265 current_branch_only = True
1201 1266
1267 if self.clone_depth:
1268 depth = self.clone_depth
1269 else:
1270 depth = self.manifest.manifestProject.config.GetString('repo.depth')
1271
1202 need_to_fetch = not (optimized_fetch and 1272 need_to_fetch = not (optimized_fetch and
1203 (ID_RE.match(self.revisionExpr) and 1273 (ID_RE.match(self.revisionExpr) and
1204 self._CheckForSha1())) 1274 self._CheckForSha1()))
1205 if (need_to_fetch and 1275 if (need_to_fetch and
1206 not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir, 1276 not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir,
1207 current_branch_only=current_branch_only, 1277 current_branch_only=current_branch_only,
1208 no_tags=no_tags, prune=prune)): 1278 no_tags=no_tags, prune=prune, depth=depth)):
1209 return False 1279 return False
1210 1280
1211 if self.worktree: 1281 if self.worktree:
@@ -1768,6 +1838,7 @@ class Project(object):
1768 1838
1769 remote = RemoteSpec(self.remote.name, 1839 remote = RemoteSpec(self.remote.name,
1770 url=url, 1840 url=url,
1841 pushUrl=self.remote.pushUrl,
1771 review=self.remote.review, 1842 review=self.remote.review,
1772 revision=self.remote.revision) 1843 revision=self.remote.revision)
1773 subproject = Project(manifest=self.manifest, 1844 subproject = Project(manifest=self.manifest,
@@ -1777,7 +1848,7 @@ class Project(object):
1777 objdir=objdir, 1848 objdir=objdir,
1778 worktree=worktree, 1849 worktree=worktree,
1779 relpath=relpath, 1850 relpath=relpath,
1780 revisionExpr=self.revisionExpr, 1851 revisionExpr=rev,
1781 revisionId=rev, 1852 revisionId=rev,
1782 rebase=self.rebase, 1853 rebase=self.rebase,
1783 groups=self.groups, 1854 groups=self.groups,
@@ -1820,23 +1891,17 @@ class Project(object):
1820 quiet=False, 1891 quiet=False,
1821 alt_dir=None, 1892 alt_dir=None,
1822 no_tags=False, 1893 no_tags=False,
1823 prune=False): 1894 prune=False,
1895 depth=None):
1824 1896
1825 is_sha1 = False 1897 is_sha1 = False
1826 tag_name = None 1898 tag_name = None
1827 depth = None
1828
1829 # The depth should not be used when fetching to a mirror because 1899 # The depth should not be used when fetching to a mirror because
1830 # it will result in a shallow repository that cannot be cloned or 1900 # it will result in a shallow repository that cannot be cloned or
1831 # fetched from. 1901 # fetched from.
1832 if not self.manifest.IsMirror: 1902 # The repo project should also never be synced with partial depth.
1833 if self.clone_depth: 1903 if self.manifest.IsMirror or self.relpath == '.repo/repo':
1834 depth = self.clone_depth 1904 depth = None
1835 else:
1836 depth = self.manifest.manifestProject.config.GetString('repo.depth')
1837 # The repo project should never be synced with partial depth
1838 if self.relpath == '.repo/repo':
1839 depth = None
1840 1905
1841 if depth: 1906 if depth:
1842 current_branch_only = True 1907 current_branch_only = True
@@ -1997,21 +2062,22 @@ class Project(object):
1997 os.remove(packed_refs) 2062 os.remove(packed_refs)
1998 self.bare_git.pack_refs('--all', '--prune') 2063 self.bare_git.pack_refs('--all', '--prune')
1999 2064
2000 if is_sha1 and current_branch_only and self.upstream: 2065 if is_sha1 and current_branch_only:
2001 # We just synced the upstream given branch; verify we 2066 # We just synced the upstream given branch; verify we
2002 # got what we wanted, else trigger a second run of all 2067 # got what we wanted, else trigger a second run of all
2003 # refs. 2068 # refs.
2004 if not self._CheckForSha1(): 2069 if not self._CheckForSha1():
2005 if not depth: 2070 if current_branch_only and depth:
2006 # Avoid infinite recursion when depth is True (since depth implies 2071 # Sync the current branch only with depth set to None
2007 # current_branch_only)
2008 return self._RemoteFetch(name=name, current_branch_only=False,
2009 initial=False, quiet=quiet, alt_dir=alt_dir)
2010 if self.clone_depth:
2011 self.clone_depth = None
2012 return self._RemoteFetch(name=name, 2072 return self._RemoteFetch(name=name,
2013 current_branch_only=current_branch_only, 2073 current_branch_only=current_branch_only,
2014 initial=False, quiet=quiet, alt_dir=alt_dir) 2074 initial=False, quiet=quiet, alt_dir=alt_dir,
2075 depth=None)
2076 else:
2077 # Avoid infinite recursion: sync all branches with depth set to None
2078 return self._RemoteFetch(name=name, current_branch_only=False,
2079 initial=False, quiet=quiet, alt_dir=alt_dir,
2080 depth=None)
2015 2081
2016 return ok 2082 return ok
2017 2083
@@ -2235,6 +2301,7 @@ class Project(object):
2235 for key in ['user.name', 'user.email']: 2301 for key in ['user.name', 'user.email']:
2236 if m.Has(key, include_defaults=False): 2302 if m.Has(key, include_defaults=False):
2237 self.config.SetString(key, m.GetString(key)) 2303 self.config.SetString(key, m.GetString(key))
2304 self.config.SetString('filter.lfs.smudge', 'git-lfs smudge --skip -- %f')
2238 if self.manifest.IsMirror: 2305 if self.manifest.IsMirror:
2239 self.config.SetString('core.bare', 'true') 2306 self.config.SetString('core.bare', 'true')
2240 else: 2307 else:
@@ -2288,6 +2355,7 @@ class Project(object):
2288 if self.remote.url: 2355 if self.remote.url:
2289 remote = self.GetRemote(self.remote.name) 2356 remote = self.GetRemote(self.remote.name)
2290 remote.url = self.remote.url 2357 remote.url = self.remote.url
2358 remote.pushUrl = self.remote.pushUrl
2291 remote.review = self.remote.review 2359 remote.review = self.remote.review
2292 remote.projectname = self.name 2360 remote.projectname = self.name
2293 2361
@@ -2332,6 +2400,7 @@ class Project(object):
2332 src = os.path.realpath(os.path.join(srcdir, name)) 2400 src = os.path.realpath(os.path.join(srcdir, name))
2333 # Fail if the links are pointing to the wrong place 2401 # Fail if the links are pointing to the wrong place
2334 if src != dst: 2402 if src != dst:
2403 _error('%s is different in %s vs %s', name, destdir, srcdir)
2335 raise GitError('--force-sync not enabled; cannot overwrite a local ' 2404 raise GitError('--force-sync not enabled; cannot overwrite a local '
2336 'work tree. If you\'re comfortable with the ' 2405 'work tree. If you\'re comfortable with the '
2337 'possibility of losing the work tree\'s git metadata,' 2406 'possibility of losing the work tree\'s git metadata,'
diff --git a/repo b/repo
index e5cb8904..4293c792 100755
--- a/repo
+++ b/repo
@@ -23,10 +23,13 @@ REPO_REV = 'stable'
23# limitations under the License. 23# limitations under the License.
24 24
25# increment this whenever we make important changes to this script 25# increment this whenever we make important changes to this script
26VERSION = (1, 22) 26VERSION = (1, 23)
27 27
28# increment this if the MAINTAINER_KEYS block is modified 28# increment this if the MAINTAINER_KEYS block is modified
29KEYRING_VERSION = (1, 2) 29KEYRING_VERSION = (1, 2)
30
31# Each individual key entry is created by using:
32# gpg --armor --export keyid
30MAINTAINER_KEYS = """ 33MAINTAINER_KEYS = """
31 34
32 Repo Maintainer <repo@android.kernel.org> 35 Repo Maintainer <repo@android.kernel.org>
@@ -196,6 +199,9 @@ group.add_option('-p', '--platform',
196 help='restrict manifest projects to ones with a specified ' 199 help='restrict manifest projects to ones with a specified '
197 'platform group [auto|all|none|linux|darwin|...]', 200 'platform group [auto|all|none|linux|darwin|...]',
198 metavar='PLATFORM') 201 metavar='PLATFORM')
202group.add_option('--no-clone-bundle',
203 dest='no_clone_bundle', action='store_true',
204 help='disable use of /clone.bundle on HTTP/HTTPS')
199 205
200 206
201# Tool 207# Tool
@@ -339,7 +345,11 @@ def _Init(args, gitc_init=False):
339 can_verify = True 345 can_verify = True
340 346
341 dst = os.path.abspath(os.path.join(repodir, S_repo)) 347 dst = os.path.abspath(os.path.join(repodir, S_repo))
342 _Clone(url, dst, opt.quiet) 348 _Clone(url, dst, opt.quiet, not opt.no_clone_bundle)
349
350 if not os.path.isfile('%s/repo' % dst):
351 _print("warning: '%s' does not look like a git-repo repository, is "
352 "REPO_URL set correctly?" % url, file=sys.stderr)
343 353
344 if can_verify and not opt.no_repo_verify: 354 if can_verify and not opt.no_repo_verify:
345 rev = _Verify(dst, branch, opt.quiet) 355 rev = _Verify(dst, branch, opt.quiet)
@@ -432,7 +442,10 @@ def SetupGnuPG(quiet):
432 sys.exit(1) 442 sys.exit(1)
433 443
434 env = os.environ.copy() 444 env = os.environ.copy()
435 env['GNUPGHOME'] = gpg_dir.encode() 445 try:
446 env['GNUPGHOME'] = gpg_dir
447 except UnicodeEncodeError:
448 env['GNUPGHOME'] = gpg_dir.encode()
436 449
437 cmd = ['gpg', '--import'] 450 cmd = ['gpg', '--import']
438 try: 451 try:
@@ -574,7 +587,7 @@ def _ImportBundle(local):
574 os.remove(path) 587 os.remove(path)
575 588
576 589
577def _Clone(url, local, quiet): 590def _Clone(url, local, quiet, clone_bundle):
578 """Clones a git repository to a new subdirectory of repodir 591 """Clones a git repository to a new subdirectory of repodir
579 """ 592 """
580 try: 593 try:
@@ -604,7 +617,7 @@ def _Clone(url, local, quiet):
604 _SetConfig(local, 617 _SetConfig(local,
605 'remote.origin.fetch', 618 'remote.origin.fetch',
606 '+refs/heads/*:refs/remotes/origin/*') 619 '+refs/heads/*:refs/remotes/origin/*')
607 if _DownloadBundle(url, local, quiet): 620 if clone_bundle and _DownloadBundle(url, local, quiet):
608 _ImportBundle(local) 621 _ImportBundle(local)
609 _Fetch(url, local, 'origin', quiet) 622 _Fetch(url, local, 'origin', quiet)
610 623
@@ -638,7 +651,10 @@ def _Verify(cwd, branch, quiet):
638 _print(file=sys.stderr) 651 _print(file=sys.stderr)
639 652
640 env = os.environ.copy() 653 env = os.environ.copy()
641 env['GNUPGHOME'] = gpg_dir.encode() 654 try:
655 env['GNUPGHOME'] = gpg_dir
656 except UnicodeEncodeError:
657 env['GNUPGHOME'] = gpg_dir.encode()
642 658
643 cmd = [GIT, 'tag', '-v', cur] 659 cmd = [GIT, 'tag', '-v', cur]
644 proc = subprocess.Popen(cmd, 660 proc = subprocess.Popen(cmd,
@@ -841,7 +857,10 @@ def main(orig_args):
841 try: 857 try:
842 _Init(args, gitc_init=(cmd == 'gitc-init')) 858 _Init(args, gitc_init=(cmd == 'gitc-init'))
843 except CloneFailure: 859 except CloneFailure:
844 shutil.rmtree(os.path.join(repodir, S_repo), ignore_errors=True) 860 path = os.path.join(repodir, S_repo)
861 _print("fatal: cloning the git-repo repository failed, will remove "
862 "'%s' " % path, file=sys.stderr)
863 shutil.rmtree(path, ignore_errors=True)
845 sys.exit(1) 864 sys.exit(1)
846 repo_main, rel_repo_dir = _FindRepo() 865 repo_main, rel_repo_dir = _FindRepo()
847 else: 866 else:
diff --git a/subcmds/abandon.py b/subcmds/abandon.py
index b94ccdd3..6f78da74 100644
--- a/subcmds/abandon.py
+++ b/subcmds/abandon.py
@@ -16,6 +16,7 @@
16from __future__ import print_function 16from __future__ import print_function
17import sys 17import sys
18from command import Command 18from command import Command
19from collections import defaultdict
19from git_command import git 20from git_command import git
20from progress import Progress 21from progress import Progress
21 22
@@ -23,49 +24,75 @@ class Abandon(Command):
23 common = True 24 common = True
24 helpSummary = "Permanently abandon a development branch" 25 helpSummary = "Permanently abandon a development branch"
25 helpUsage = """ 26 helpUsage = """
26%prog <branchname> [<project>...] 27%prog [--all | <branchname>] [<project>...]
27 28
28This subcommand permanently abandons a development branch by 29This subcommand permanently abandons a development branch by
29deleting it (and all its history) from your local repository. 30deleting it (and all its history) from your local repository.
30 31
31It is equivalent to "git branch -D <branchname>". 32It is equivalent to "git branch -D <branchname>".
32""" 33"""
34 def _Options(self, p):
35 p.add_option('--all',
36 dest='all', action='store_true',
37 help='delete all branches in all projects')
33 38
34 def Execute(self, opt, args): 39 def Execute(self, opt, args):
35 if not args: 40 if not opt.all and not args:
36 self.Usage() 41 self.Usage()
37 42
38 nb = args[0] 43 if not opt.all:
39 if not git.check_ref_format('heads/%s' % nb): 44 nb = args[0]
40 print("error: '%s' is not a valid name" % nb, file=sys.stderr) 45 if not git.check_ref_format('heads/%s' % nb):
41 sys.exit(1) 46 print("error: '%s' is not a valid name" % nb, file=sys.stderr)
47 sys.exit(1)
48 else:
49 args.insert(0,None)
50 nb = "'All local branches'"
42 51
43 nb = args[0] 52 err = defaultdict(list)
44 err = [] 53 success = defaultdict(list)
45 success = []
46 all_projects = self.GetProjects(args[1:]) 54 all_projects = self.GetProjects(args[1:])
47 55
48 pm = Progress('Abandon %s' % nb, len(all_projects)) 56 pm = Progress('Abandon %s' % nb, len(all_projects))
49 for project in all_projects: 57 for project in all_projects:
50 pm.update() 58 pm.update()
51 59
52 status = project.AbandonBranch(nb) 60 if opt.all:
53 if status is not None: 61 branches = project.GetBranches().keys()
54 if status: 62 else:
55 success.append(project) 63 branches = [nb]
56 else: 64
57 err.append(project) 65 for name in branches:
66 status = project.AbandonBranch(name)
67 if status is not None:
68 if status:
69 success[name].append(project)
70 else:
71 err[name].append(project)
58 pm.end() 72 pm.end()
59 73
74 width = 25
75 for name in branches:
76 if width < len(name):
77 width = len(name)
78
60 if err: 79 if err:
61 for p in err: 80 for br in err.keys():
62 print("error: %s/: cannot abandon %s" % (p.relpath, nb), 81 err_msg = "error: cannot abandon %s" %br
63 file=sys.stderr) 82 print(err_msg, file=sys.stderr)
83 for proj in err[br]:
84 print(' '*len(err_msg) + " | %s" % p.relpath, file=sys.stderr)
64 sys.exit(1) 85 sys.exit(1)
65 elif not success: 86 elif not success:
66 print('error: no project has branch %s' % nb, file=sys.stderr) 87 print('error: no project has local branch(es) : %s' % nb,
88 file=sys.stderr)
67 sys.exit(1) 89 sys.exit(1)
68 else: 90 else:
69 print('Abandoned in %d project(s):\n %s' 91 print('Abandoned branches:', file=sys.stderr)
70 % (len(success), '\n '.join(p.relpath for p in success)), 92 for br in success.keys():
71 file=sys.stderr) 93 if len(all_projects) > 1 and len(all_projects) == len(success[br]):
94 result = "all project"
95 else:
96 result = "%s" % (
97 ('\n'+' '*width + '| ').join(p.relpath for p in success[br]))
98 print("%s%s| %s\n" % (br,' '*(width-len(br)), result),file=sys.stderr)
diff --git a/subcmds/init.py b/subcmds/init.py
index b8e3de5a..45d69b79 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -61,6 +61,11 @@ directory use as much data as possible from the local reference
61directory when fetching from the server. This will make the sync 61directory when fetching from the server. This will make the sync
62go a lot faster by reducing data traffic on the network. 62go a lot faster by reducing data traffic on the network.
63 63
64The --no-clone-bundle option disables any attempt to use
65$URL/clone.bundle to bootstrap a new Git repository from a
66resumeable bundle file on a content delivery network. This
67may be necessary if there are problems with the local Python
68HTTP client or proxy configuration, but the Git binary works.
64 69
65Switching Manifest Branches 70Switching Manifest Branches
66--------------------------- 71---------------------------
@@ -113,6 +118,9 @@ to update the working directory files.
113 help='restrict manifest projects to ones with a specified ' 118 help='restrict manifest projects to ones with a specified '
114 'platform group [auto|all|none|linux|darwin|...]', 119 'platform group [auto|all|none|linux|darwin|...]',
115 metavar='PLATFORM') 120 metavar='PLATFORM')
121 g.add_option('--no-clone-bundle',
122 dest='no_clone_bundle', action='store_true',
123 help='disable use of /clone.bundle on HTTP/HTTPS')
116 124
117 # Tool 125 # Tool
118 g = p.add_option_group('repo Version options') 126 g = p.add_option_group('repo Version options')
@@ -222,7 +230,8 @@ to update the working directory files.
222 'in another location.', file=sys.stderr) 230 'in another location.', file=sys.stderr)
223 sys.exit(1) 231 sys.exit(1)
224 232
225 if not m.Sync_NetworkHalf(is_new=is_new): 233 if not m.Sync_NetworkHalf(is_new=is_new, quiet=opt.quiet,
234 clone_bundle=not opt.no_clone_bundle):
226 r = m.GetRemote(m.remote.name) 235 r = m.GetRemote(m.remote.name)
227 print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr) 236 print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr)
228 237
diff --git a/subcmds/start.py b/subcmds/start.py
index d1430a9d..290b6897 100644
--- a/subcmds/start.py
+++ b/subcmds/start.py
@@ -54,8 +54,7 @@ revision specified in the manifest.
54 if not opt.all: 54 if not opt.all:
55 projects = args[1:] 55 projects = args[1:]
56 if len(projects) < 1: 56 if len(projects) < 1:
57 print("error: at least one project must be specified", file=sys.stderr) 57 projects = ['.',] # start it in the local project by default
58 sys.exit(1)
59 58
60 all_projects = self.GetProjects(projects, 59 all_projects = self.GetProjects(projects,
61 missing_ok=bool(self.gitc_manifest)) 60 missing_ok=bool(self.gitc_manifest))
diff --git a/subcmds/status.py b/subcmds/status.py
index 38c229b1..60e26ff4 100644
--- a/subcmds/status.py
+++ b/subcmds/status.py
@@ -89,8 +89,10 @@ the following meanings:
89 p.add_option('-o', '--orphans', 89 p.add_option('-o', '--orphans',
90 dest='orphans', action='store_true', 90 dest='orphans', action='store_true',
91 help="include objects in working directory outside of repo projects") 91 help="include objects in working directory outside of repo projects")
92 p.add_option('-q', '--quiet', action='store_true',
93 help="only print the name of modified projects")
92 94
93 def _StatusHelper(self, project, clean_counter, sem): 95 def _StatusHelper(self, project, clean_counter, sem, quiet):
94 """Obtains the status for a specific project. 96 """Obtains the status for a specific project.
95 97
96 Obtains the status for a project, redirecting the output to 98 Obtains the status for a project, redirecting the output to
@@ -104,7 +106,7 @@ the following meanings:
104 output: Where to output the status. 106 output: Where to output the status.
105 """ 107 """
106 try: 108 try:
107 state = project.PrintWorkTreeStatus() 109 state = project.PrintWorkTreeStatus(quiet=quiet)
108 if state == 'CLEAN': 110 if state == 'CLEAN':
109 next(clean_counter) 111 next(clean_counter)
110 finally: 112 finally:
@@ -132,7 +134,7 @@ the following meanings:
132 134
133 if opt.jobs == 1: 135 if opt.jobs == 1:
134 for project in all_projects: 136 for project in all_projects:
135 state = project.PrintWorkTreeStatus() 137 state = project.PrintWorkTreeStatus(quiet=opt.quiet)
136 if state == 'CLEAN': 138 if state == 'CLEAN':
137 next(counter) 139 next(counter)
138 else: 140 else:
@@ -142,13 +144,13 @@ the following meanings:
142 sem.acquire() 144 sem.acquire()
143 145
144 t = _threading.Thread(target=self._StatusHelper, 146 t = _threading.Thread(target=self._StatusHelper,
145 args=(project, counter, sem)) 147 args=(project, counter, sem, opt.quiet))
146 threads.append(t) 148 threads.append(t)
147 t.daemon = True 149 t.daemon = True
148 t.start() 150 t.start()
149 for t in threads: 151 for t in threads:
150 t.join() 152 t.join()
151 if len(all_projects) == next(counter): 153 if not opt.quiet and len(all_projects) == next(counter):
152 print('nothing to commit (working directory clean)') 154 print('nothing to commit (working directory clean)')
153 155
154 if opt.orphans: 156 if opt.orphans:
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 9124a653..bbb166c0 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -255,7 +255,7 @@ later is required to fix a server side protocol bug.
255 dest='repo_upgraded', action='store_true', 255 dest='repo_upgraded', action='store_true',
256 help=SUPPRESS_HELP) 256 help=SUPPRESS_HELP)
257 257
258 def _FetchProjectList(self, opt, projects, *args, **kwargs): 258 def _FetchProjectList(self, opt, projects, sem, *args, **kwargs):
259 """Main function of the fetch threads when jobs are > 1. 259 """Main function of the fetch threads when jobs are > 1.
260 260
261 Delegates most of the work to _FetchHelper. 261 Delegates most of the work to _FetchHelper.
@@ -263,15 +263,20 @@ later is required to fix a server side protocol bug.
263 Args: 263 Args:
264 opt: Program options returned from optparse. See _Options(). 264 opt: Program options returned from optparse. See _Options().
265 projects: Projects to fetch. 265 projects: Projects to fetch.
266 sem: We'll release() this semaphore when we exit so that another thread
267 can be started up.
266 *args, **kwargs: Remaining arguments to pass to _FetchHelper. See the 268 *args, **kwargs: Remaining arguments to pass to _FetchHelper. See the
267 _FetchHelper docstring for details. 269 _FetchHelper docstring for details.
268 """ 270 """
269 for project in projects: 271 try:
270 success = self._FetchHelper(opt, project, *args, **kwargs) 272 for project in projects:
271 if not success and not opt.force_broken: 273 success = self._FetchHelper(opt, project, *args, **kwargs)
272 break 274 if not success and not opt.force_broken:
275 break
276 finally:
277 sem.release()
273 278
274 def _FetchHelper(self, opt, project, lock, fetched, pm, sem, err_event): 279 def _FetchHelper(self, opt, project, lock, fetched, pm, err_event):
275 """Fetch git objects for a single project. 280 """Fetch git objects for a single project.
276 281
277 Args: 282 Args:
@@ -283,8 +288,6 @@ later is required to fix a server side protocol bug.
283 (with our lock held). 288 (with our lock held).
284 pm: Instance of a Project object. We will call pm.update() (with our 289 pm: Instance of a Project object. We will call pm.update() (with our
285 lock held). 290 lock held).
286 sem: We'll release() this semaphore when we exit so that another thread
287 can be started up.
288 err_event: We'll set this event in the case of an error (after printing 291 err_event: We'll set this event in the case of an error (after printing
289 out info about the error). 292 out info about the error).
290 293
@@ -340,7 +343,6 @@ later is required to fix a server side protocol bug.
340 finally: 343 finally:
341 if did_lock: 344 if did_lock:
342 lock.release() 345 lock.release()
343 sem.release()
344 346
345 return success 347 return success
346 348
@@ -365,10 +367,10 @@ later is required to fix a server side protocol bug.
365 sem.acquire() 367 sem.acquire()
366 kwargs = dict(opt=opt, 368 kwargs = dict(opt=opt,
367 projects=project_list, 369 projects=project_list,
370 sem=sem,
368 lock=lock, 371 lock=lock,
369 fetched=fetched, 372 fetched=fetched,
370 pm=pm, 373 pm=pm,
371 sem=sem,
372 err_event=err_event) 374 err_event=err_event)
373 if self.jobs > 1: 375 if self.jobs > 1:
374 t = _threading.Thread(target = self._FetchProjectList, 376 t = _threading.Thread(target = self._FetchProjectList,
@@ -397,9 +399,12 @@ later is required to fix a server side protocol bug.
397 return fetched 399 return fetched
398 400
399 def _GCProjects(self, projects): 401 def _GCProjects(self, projects):
400 gitdirs = {} 402 gc_gitdirs = {}
401 for project in projects: 403 for project in projects:
402 gitdirs[project.gitdir] = project.bare_git 404 if len(project.manifest.GetProjectsWithName(project.name)) > 1:
405 print('Shared project %s found, disabling pruning.' % project.name)
406 project.bare_git.config('--replace-all', 'gc.pruneExpire', 'never')
407 gc_gitdirs[project.gitdir] = project.bare_git
403 408
404 has_dash_c = git_require((1, 7, 2)) 409 has_dash_c = git_require((1, 7, 2))
405 if multiprocessing and has_dash_c: 410 if multiprocessing and has_dash_c:
@@ -409,7 +414,7 @@ later is required to fix a server side protocol bug.
409 jobs = min(self.jobs, cpu_count) 414 jobs = min(self.jobs, cpu_count)
410 415
411 if jobs < 2: 416 if jobs < 2:
412 for bare_git in gitdirs.values(): 417 for bare_git in gc_gitdirs.values():
413 bare_git.gc('--auto') 418 bare_git.gc('--auto')
414 return 419 return
415 420
@@ -431,7 +436,7 @@ later is required to fix a server side protocol bug.
431 finally: 436 finally:
432 sem.release() 437 sem.release()
433 438
434 for bare_git in gitdirs.values(): 439 for bare_git in gc_gitdirs.values():
435 if err_event.isSet(): 440 if err_event.isSet():
436 break 441 break
437 sem.acquire() 442 sem.acquire()
@@ -454,6 +459,65 @@ later is required to fix a server side protocol bug.
454 else: 459 else:
455 self.manifest._Unload() 460 self.manifest._Unload()
456 461
462 def _DeleteProject(self, path):
463 print('Deleting obsolete path %s' % path, file=sys.stderr)
464
465 # Delete the .git directory first, so we're less likely to have a partially
466 # working git repository around. There shouldn't be any git projects here,
467 # so rmtree works.
468 try:
469 shutil.rmtree(os.path.join(path, '.git'))
470 except OSError:
471 print('Failed to remove %s' % os.path.join(path, '.git'), file=sys.stderr)
472 print('error: Failed to delete obsolete path %s' % path, file=sys.stderr)
473 print(' remove manually, then run sync again', file=sys.stderr)
474 return -1
475
476 # Delete everything under the worktree, except for directories that contain
477 # another git project
478 dirs_to_remove = []
479 failed = False
480 for root, dirs, files in os.walk(path):
481 for f in files:
482 try:
483 os.remove(os.path.join(root, f))
484 except OSError:
485 print('Failed to remove %s' % os.path.join(root, f), file=sys.stderr)
486 failed = True
487 dirs[:] = [d for d in dirs
488 if not os.path.lexists(os.path.join(root, d, '.git'))]
489 dirs_to_remove += [os.path.join(root, d) for d in dirs
490 if os.path.join(root, d) not in dirs_to_remove]
491 for d in reversed(dirs_to_remove):
492 if os.path.islink(d):
493 try:
494 os.remove(d)
495 except OSError:
496 print('Failed to remove %s' % os.path.join(root, d), file=sys.stderr)
497 failed = True
498 elif len(os.listdir(d)) == 0:
499 try:
500 os.rmdir(d)
501 except OSError:
502 print('Failed to remove %s' % os.path.join(root, d), file=sys.stderr)
503 failed = True
504 continue
505 if failed:
506 print('error: Failed to delete obsolete path %s' % path, file=sys.stderr)
507 print(' remove manually, then run sync again', file=sys.stderr)
508 return -1
509
510 # Try deleting parent dirs if they are empty
511 project_dir = path
512 while project_dir != self.manifest.topdir:
513 if len(os.listdir(project_dir)) == 0:
514 os.rmdir(project_dir)
515 else:
516 break
517 project_dir = os.path.dirname(project_dir)
518
519 return 0
520
457 def UpdateProjectList(self): 521 def UpdateProjectList(self):
458 new_project_paths = [] 522 new_project_paths = []
459 for project in self.GetProjects(None, missing_ok=True): 523 for project in self.GetProjects(None, missing_ok=True):
@@ -474,8 +538,8 @@ later is required to fix a server side protocol bug.
474 continue 538 continue
475 if path not in new_project_paths: 539 if path not in new_project_paths:
476 # If the path has already been deleted, we don't need to do it 540 # If the path has already been deleted, we don't need to do it
477 if os.path.exists(self.manifest.topdir + '/' + path): 541 gitdir = os.path.join(self.manifest.topdir, path, '.git')
478 gitdir = os.path.join(self.manifest.topdir, path, '.git') 542 if os.path.exists(gitdir):
479 project = Project( 543 project = Project(
480 manifest = self.manifest, 544 manifest = self.manifest,
481 name = path, 545 name = path,
@@ -494,18 +558,8 @@ later is required to fix a server side protocol bug.
494 print(' commit changes, then run sync again', 558 print(' commit changes, then run sync again',
495 file=sys.stderr) 559 file=sys.stderr)
496 return -1 560 return -1
497 else: 561 elif self._DeleteProject(project.worktree):
498 print('Deleting obsolete path %s' % project.worktree, 562 return -1
499 file=sys.stderr)
500 shutil.rmtree(project.worktree)
501 # Try deleting parent subdirs if they are empty
502 project_dir = os.path.dirname(project.worktree)
503 while project_dir != self.manifest.topdir:
504 try:
505 os.rmdir(project_dir)
506 except OSError:
507 break
508 project_dir = os.path.dirname(project_dir)
509 563
510 new_project_paths.sort() 564 new_project_paths.sort()
511 fd = open(file_path, 'w') 565 fd = open(file_path, 'w')
diff --git a/subcmds/upload.py b/subcmds/upload.py
index 674fc17d..1172dadc 100644
--- a/subcmds/upload.py
+++ b/subcmds/upload.py
@@ -454,9 +454,15 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
454 if avail: 454 if avail:
455 pending.append((project, avail)) 455 pending.append((project, avail))
456 456
457 if pending and (not opt.bypass_hooks): 457 if not pending:
458 print("no branches ready for upload", file=sys.stderr)
459 return
460
461 if not opt.bypass_hooks:
458 hook = RepoHook('pre-upload', self.manifest.repo_hooks_project, 462 hook = RepoHook('pre-upload', self.manifest.repo_hooks_project,
459 self.manifest.topdir, abort_if_user_denies=True) 463 self.manifest.topdir,
464 self.manifest.manifestProject.GetRemote('origin').url,
465 abort_if_user_denies=True)
460 pending_proj_names = [project.name for (project, avail) in pending] 466 pending_proj_names = [project.name for (project, avail) in pending]
461 pending_worktrees = [project.worktree for (project, avail) in pending] 467 pending_worktrees = [project.worktree for (project, avail) in pending]
462 try: 468 try:
@@ -472,9 +478,7 @@ Gerrit Code Review: http://code.google.com/p/gerrit/
472 cc = _SplitEmails(opt.cc) 478 cc = _SplitEmails(opt.cc)
473 people = (reviewers, cc) 479 people = (reviewers, cc)
474 480
475 if not pending: 481 if len(pending) == 1 and len(pending[0][1]) == 1:
476 print("no branches ready for upload", file=sys.stderr)
477 elif len(pending) == 1 and len(pending[0][1]) == 1:
478 self._SingleBranch(opt, pending[0][1][0], people) 482 self._SingleBranch(opt, pending[0][1][0], people)
479 else: 483 else:
480 self._MultipleBranches(opt, pending, people) 484 self._MultipleBranches(opt, pending, people)