summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitattributes2
-rw-r--r--.mailmap11
-rw-r--r--.pylintrc2
-rw-r--r--README.md14
-rw-r--r--SUBMITTING_PATCHES.md (renamed from SUBMITTING_PATCHES)28
-rw-r--r--command.py43
-rw-r--r--docs/manifest-format.txt6
-rw-r--r--gitc_utils.py12
-rwxr-xr-xhooks/commit-msg19
-rwxr-xr-xmain.py2
-rw-r--r--manifest_xml.py15
-rw-r--r--project.py391
-rwxr-xr-xrepo23
-rw-r--r--subcmds/diffmanifests.py21
-rw-r--r--subcmds/forall.py11
-rw-r--r--subcmds/init.py11
-rw-r--r--subcmds/start.py3
-rw-r--r--subcmds/sync.py2
-rw-r--r--subcmds/upload.py14
19 files changed, 417 insertions, 213 deletions
diff --git a/.gitattributes b/.gitattributes
index d65028a4..cdd8546d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,4 @@
1# Prevent /bin/sh scripts from being clobbered by autocrlf=true 1# Prevent /bin/sh scripts from being clobbered by autocrlf=true
2git_ssh text eol=lf 2git_ssh text eol=lf
3main.py text eol=lf
4repo text eol=lf 3repo text eol=lf
4hooks/* text eol=lf
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 00000000..eb64bd21
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,11 @@
1Anthony Newnam <anthony.newnam@garmin.com> Anthony <anthony@bnovc.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>
5Jia Bi <bijia@xiaomi.com> bijia <bijia@xiaomi.com>
6JoonCheol Park <jooncheol@gmail.com> Jooncheol Park <jooncheol@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>
9Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@gmail.com>
10Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@sonyericsson.com>
11Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>
diff --git a/.pylintrc b/.pylintrc
index c6be7436..413d66a1 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -53,7 +53,7 @@ load-plugins=
53enable=RP0004 53enable=RP0004
54 54
55# Disable the message(s) with the given id(s). 55# Disable the message(s) with the given id(s).
56disable=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 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 57
58[REPORTS] 58[REPORTS]
59 59
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..085ae06a 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES.md
@@ -1,4 +1,4 @@
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.
@@ -8,10 +8,10 @@ Short Version:
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,14 @@ 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 with pylint
40 40
41Run pylint on changed modules using the provided configuration: 41Run pylint on changed modules using the provided configuration:
42 42
43 pylint --rcfile=.pylintrc file.py 43 pylint --rcfile=.pylintrc file.py
44 44
45 45
46(3) Check the license 46## Check the license
47 47
48repo is licensed under the Apache License, 2.0. 48repo is licensed under the Apache License, 2.0.
49 49
@@ -59,7 +59,7 @@ your patch. It is virtually impossible to remove a patch once it
59has been applied and pushed out. 59has been applied and pushed out.
60 60
61 61
62(4) Sending your patches. 62## Sending your patches.
63 63
64Do not email your patches to anyone. 64Do not email your patches to anyone.
65 65
@@ -91,23 +91,23 @@ to get the ChangeId added.
91Push your patches over HTTPS to the review server, possibly through 91Push your patches over HTTPS to the review server, possibly through
92a remembered remote to make this easier in the future: 92a remembered remote to make this easier in the future:
93 93
94 git config remote.review.url https://gerrit-review.googlesource.com/git-repo 94 git config remote.review.url https://gerrit-review.googlesource.com/git-repo
95 git config remote.review.push HEAD:refs/for/master 95 git config remote.review.push HEAD:refs/for/master
96 96
97 git push review 97 git push review
98 98
99You will be automatically emailed a copy of your commits, and any 99You will be automatically emailed a copy of your commits, and any
100comments made by the project maintainers. 100comments made by the project maintainers.
101 101
102 102
103(5) Make changes if requested 103## Make changes if requested
104 104
105The project maintainer who reviews your changes might request changes to your 105The 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 106commit. If you make the requested changes you will need to amend your commit
107and push it to the review server again. 107and push it to the review server again.
108 108
109 109
110(6) Verify your changes on gerrit 110## Verify your changes on gerrit
111 111
112After you receive a Code-Review+2 from the maintainer, select the Verified 112After 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 113button on the gerrit page for the change. This verifies that you have tested
diff --git a/command.py b/command.py
index cd5e3c3e..2ff0a344 100644
--- a/command.py
+++ b/command.py
@@ -31,7 +31,7 @@ class Command(object):
31 manifest = None 31 manifest = None
32 _optparse = None 32 _optparse = None
33 33
34 def WantPager(self, opt): 34 def WantPager(self, _opt):
35 return False 35 return False
36 36
37 def ReadEnvironmentOptions(self, opts): 37 def ReadEnvironmentOptions(self, opts):
@@ -63,7 +63,7 @@ class Command(object):
63 usage = self.helpUsage.strip().replace('%prog', me) 63 usage = self.helpUsage.strip().replace('%prog', me)
64 except AttributeError: 64 except AttributeError:
65 usage = 'repo %s' % self.NAME 65 usage = 'repo %s' % self.NAME
66 self._optparse = optparse.OptionParser(usage = usage) 66 self._optparse = optparse.OptionParser(usage=usage)
67 self._Options(self._optparse) 67 self._Options(self._optparse)
68 return self._optparse 68 return self._optparse
69 69
@@ -110,15 +110,20 @@ class Command(object):
110 project = None 110 project = None
111 if os.path.exists(path): 111 if os.path.exists(path):
112 oldpath = None 112 oldpath = None
113 while path \ 113 while path and \
114 and path != oldpath \ 114 path != oldpath and \
115 and path != manifest.topdir: 115 path != manifest.topdir:
116 try: 116 try:
117 project = self._by_path[path] 117 project = self._by_path[path]
118 break 118 break
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]
@@ -138,7 +143,7 @@ class Command(object):
138 mp = manifest.manifestProject 143 mp = manifest.manifestProject
139 144
140 if not groups: 145 if not groups:
141 groups = mp.config.GetString('manifest.groups') 146 groups = mp.config.GetString('manifest.groups')
142 if not groups: 147 if not groups:
143 groups = 'default,platform-' + platform.system().lower() 148 groups = 'default,platform-' + platform.system().lower()
144 groups = [x for x in re.split(r'[,\s]+', groups) if x] 149 groups = [x for x in re.split(r'[,\s]+', groups) if x]
@@ -151,8 +156,7 @@ class Command(object):
151 for p in project.GetDerivedSubprojects()) 156 for p in project.GetDerivedSubprojects())
152 all_projects_list.extend(derived_projects.values()) 157 all_projects_list.extend(derived_projects.values())
153 for project in all_projects_list: 158 for project in all_projects_list:
154 if ((missing_ok or project.Exists) and 159 if (missing_ok or project.Exists) and project.MatchesGroups(groups):
155 project.MatchesGroups(groups)):
156 result.append(project) 160 result.append(project)
157 else: 161 else:
158 self._ResetPathToProjectMap(all_projects_list) 162 self._ResetPathToProjectMap(all_projects_list)
@@ -166,8 +170,8 @@ class Command(object):
166 170
167 # If it's not a derived project, update path->project mapping and 171 # If it's not a derived project, update path->project mapping and
168 # search again, as arg might actually point to a derived subproject. 172 # search again, as arg might actually point to a derived subproject.
169 if (project and not project.Derived and 173 if (project and not project.Derived and (submodules_ok or
170 (submodules_ok or project.sync_s)): 174 project.sync_s)):
171 search_again = False 175 search_again = False
172 for subproject in project.GetDerivedSubprojects(): 176 for subproject in project.GetDerivedSubprojects():
173 self._UpdatePathToProjectMap(subproject) 177 self._UpdatePathToProjectMap(subproject)
@@ -194,17 +198,24 @@ class Command(object):
194 result.sort(key=_getpath) 198 result.sort(key=_getpath)
195 return result 199 return result
196 200
197 def FindProjects(self, args): 201 def FindProjects(self, args, inverse=False):
198 result = [] 202 result = []
199 patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args] 203 patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args]
200 for project in self.GetProjects(''): 204 for project in self.GetProjects(''):
201 for pattern in patterns: 205 for pattern in patterns:
202 if pattern.search(project.name) or pattern.search(project.relpath): 206 match = pattern.search(project.name) or pattern.search(project.relpath)
207 if not inverse and match:
203 result.append(project) 208 result.append(project)
204 break 209 break
210 if inverse and match:
211 break
212 else:
213 if inverse:
214 result.append(project)
205 result.sort(key=lambda project: project.relpath) 215 result.sort(key=lambda project: project.relpath)
206 return result 216 return result
207 217
218
208# pylint: disable=W0223 219# pylint: disable=W0223
209# Pylint warns that the `InteractiveCommand` and `PagedCommand` classes do not 220# Pylint warns that the `InteractiveCommand` and `PagedCommand` classes do not
210# override method `Execute` which is abstract in `Command`. Since that method 221# override method `Execute` which is abstract in `Command`. Since that method
@@ -214,28 +225,32 @@ class InteractiveCommand(Command):
214 """Command which requires user interaction on the tty and 225 """Command which requires user interaction on the tty and
215 must not run within a pager, even if the user asks to. 226 must not run within a pager, even if the user asks to.
216 """ 227 """
217 def WantPager(self, opt): 228 def WantPager(self, _opt):
218 return False 229 return False
219 230
231
220class PagedCommand(Command): 232class PagedCommand(Command):
221 """Command which defaults to output in a pager, as its 233 """Command which defaults to output in a pager, as its
222 display tends to be larger than one screen full. 234 display tends to be larger than one screen full.
223 """ 235 """
224 def WantPager(self, opt): 236 def WantPager(self, _opt):
225 return True 237 return True
226 238
227# pylint: enable=W0223 239# pylint: enable=W0223
228 240
241
229class MirrorSafeCommand(object): 242class MirrorSafeCommand(object):
230 """Command permits itself to run within a mirror, 243 """Command permits itself to run within a mirror,
231 and does not require a working directory. 244 and does not require a working directory.
232 """ 245 """
233 246
247
234class GitcAvailableCommand(object): 248class GitcAvailableCommand(object):
235 """Command that requires GITC to be available, but does 249 """Command that requires GITC to be available, but does
236 not require the local client to be a GITC client. 250 not require the local client to be a GITC client.
237 """ 251 """
238 252
253
239class GitcClientCommand(object): 254class GitcClientCommand(object):
240 """Command that requires the local client to be a GITC 255 """Command that requires the local client to be a GITC
241 client. 256 client.
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt
index 140a782f..8fd9137c 100644
--- a/docs/manifest-format.txt
+++ b/docs/manifest-format.txt
@@ -175,7 +175,8 @@ The manifest server should implement the following RPC methods:
175 GetApprovedManifest(branch, target) 175 GetApprovedManifest(branch, target)
176 176
177Return a manifest in which each project is pegged to a known good revision 177Return a manifest in which each project is pegged to a known good revision
178for the current branch and target. 178for the current branch and target. This is used by repo sync when the
179--smart-sync option is given.
179 180
180The target to use is defined by environment variables TARGET_PRODUCT 181The target to use is defined by environment variables TARGET_PRODUCT
181and TARGET_BUILD_VARIANT. These variables are used to create a string 182and TARGET_BUILD_VARIANT. These variables are used to create a string
@@ -187,7 +188,8 @@ should choose a reasonable default target.
187 GetManifest(tag) 188 GetManifest(tag)
188 189
189Return a manifest in which each project is pegged to the revision at 190Return a manifest in which each project is pegged to the revision at
190the specified tag. 191the specified tag. This is used by repo sync when the --smart-tag option
192is given.
191 193
192 194
193Element project 195Element project
diff --git a/gitc_utils.py b/gitc_utils.py
index 0f3e1818..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
@@ -127,7 +133,7 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
127 repo_proj.revisionExpr = None 133 repo_proj.revisionExpr = None
128 134
129 # Convert URLs from relative to absolute. 135 # Convert URLs from relative to absolute.
130 for name, remote in manifest.remotes.iteritems(): 136 for _name, remote in manifest.remotes.iteritems():
131 remote.fetchUrl = remote.resolvedFetchUrl 137 remote.fetchUrl = remote.resolvedFetchUrl
132 138
133 # Save the manifest. 139 # Save the manifest.
diff --git a/hooks/commit-msg b/hooks/commit-msg
index d8f009b6..40ac237a 100755
--- a/hooks/commit-msg
+++ b/hooks/commit-msg
@@ -1,6 +1,7 @@
1#!/bin/sh 1#!/bin/sh
2# From Gerrit Code Review 2.12.1
2# 3#
3# Part of Gerrit Code Review (http://code.google.com/p/gerrit/) 4# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
4# 5#
5# Copyright (C) 2009 The Android Open Source Project 6# Copyright (C) 2009 The Android Open Source Project
6# 7#
@@ -19,7 +20,7 @@
19 20
20unset GREP_OPTIONS 21unset GREP_OPTIONS
21 22
22CHANGE_ID_AFTER="Bug|Issue" 23CHANGE_ID_AFTER="Bug|Issue|Test"
23MSG="$1" 24MSG="$1"
24 25
25# Check for, and add if missing, a unique Change-Id 26# Check for, and add if missing, a unique Change-Id
@@ -38,6 +39,12 @@ add_ChangeId() {
38 return 39 return
39 fi 40 fi
40 41
42 # Do not add Change-Id to temp commits
43 if echo "$clean_message" | head -1 | grep -q '^\(fixup\|squash\)!'
44 then
45 return
46 fi
47
41 if test "false" = "`git config --bool --get gerrit.createChangeId`" 48 if test "false" = "`git config --bool --get gerrit.createChangeId`"
42 then 49 then
43 return 50 return
@@ -57,6 +64,10 @@ add_ChangeId() {
57 AWK=/usr/xpg4/bin/awk 64 AWK=/usr/xpg4/bin/awk
58 fi 65 fi
59 66
67 # Get core.commentChar from git config or use default symbol
68 commentChar=`git config --get core.commentChar`
69 commentChar=${commentChar:-#}
70
60 # How this works: 71 # How this works:
61 # - parse the commit message as (textLine+ blankLine*)* 72 # - parse the commit message as (textLine+ blankLine*)*
62 # - assume textLine+ to be a footer until proven otherwise 73 # - assume textLine+ to be a footer until proven otherwise
@@ -75,8 +86,8 @@ add_ChangeId() {
75 blankLines = 0 86 blankLines = 0
76 } 87 }
77 88
78 # Skip lines starting with "#" without any spaces before it. 89 # Skip lines starting with commentChar without any spaces before it.
79 /^#/ { next } 90 /^'"$commentChar"'/ { next }
80 91
81 # Skip the line starting with the diff command and everything after it, 92 # Skip the line starting with the diff command and everything after it,
82 # up to the end of the file, assuming it is only patch data. 93 # up to the end of the file, assuming it is only patch data.
diff --git a/main.py b/main.py
index 4f4eb9fc..c5f1e9c3 100755
--- a/main.py
+++ b/main.py
@@ -379,7 +379,7 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
379 self.context = None 379 self.context = None
380 self.handler_order = urllib.request.BaseHandler.handler_order - 50 380 self.handler_order = urllib.request.BaseHandler.handler_order - 50
381 381
382 def http_error_401(self, req, fp, code, msg, headers): 382 def http_error_401(self, req, fp, code, msg, headers): # pylint:disable=unused-argument
383 host = req.get_host() 383 host = req.get_host()
384 retry = self.http_error_auth_reqed('www-authenticate', host, req, headers) 384 retry = self.http_error_auth_reqed('www-authenticate', host, req, headers)
385 return retry 385 return retry
diff --git a/manifest_xml.py b/manifest_xml.py
index 3ac607ec..295493de 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -102,7 +102,10 @@ class _XmlRemote(object):
102 remoteName = self.name 102 remoteName = self.name
103 if self.remoteAlias: 103 if self.remoteAlias:
104 remoteName = self.remoteAlias 104 remoteName = self.remoteAlias
105 return RemoteSpec(remoteName, url, self.reviewUrl) 105 return RemoteSpec(remoteName,
106 url=url,
107 review=self.reviewUrl,
108 orig_name=self.name)
106 109
107class XmlManifest(object): 110class XmlManifest(object):
108 """manages the repo configuration file""" 111 """manages the repo configuration file"""
@@ -249,9 +252,9 @@ class XmlManifest(object):
249 e.setAttribute('path', relpath) 252 e.setAttribute('path', relpath)
250 remoteName = None 253 remoteName = None
251 if d.remote: 254 if d.remote:
252 remoteName = d.remote.remoteAlias or d.remote.name 255 remoteName = d.remote.name
253 if not d.remote or p.remote.name != remoteName: 256 if not d.remote or p.remote.orig_name != remoteName:
254 remoteName = p.remote.name 257 remoteName = p.remote.orig_name
255 e.setAttribute('remote', remoteName) 258 e.setAttribute('remote', remoteName)
256 if peg_rev: 259 if peg_rev:
257 if self.IsMirror: 260 if self.IsMirror:
@@ -267,7 +270,7 @@ class XmlManifest(object):
267 # isn't our value 270 # isn't our value
268 e.setAttribute('upstream', p.revisionExpr) 271 e.setAttribute('upstream', p.revisionExpr)
269 else: 272 else:
270 revision = self.remotes[remoteName].revision or d.revisionExpr 273 revision = self.remotes[p.remote.orig_name].revision or d.revisionExpr
271 if not revision or revision != p.revisionExpr: 274 if not revision or revision != p.revisionExpr:
272 e.setAttribute('revision', p.revisionExpr) 275 e.setAttribute('revision', p.revisionExpr)
273 if p.upstream and p.upstream != p.revisionExpr: 276 if p.upstream and p.upstream != p.revisionExpr:
@@ -969,5 +972,5 @@ class GitcManifest(XmlManifest):
969 def _output_manifest_project_extras(self, p, e): 972 def _output_manifest_project_extras(self, p, e):
970 """Output GITC Specific Project attributes""" 973 """Output GITC Specific Project attributes"""
971 if p.old_revision: 974 if p.old_revision:
972 e.setAttribute('old-revision', str(p.old_revision)) 975 e.setAttribute('old-revision', str(p.old_revision))
973 976
diff --git a/project.py b/project.py
index fa4e7cea..0a86a718 100644
--- a/project.py
+++ b/project.py
@@ -30,7 +30,8 @@ import traceback
30 30
31from color import Coloring 31from color import Coloring
32from git_command import GitCommand, git_require 32from git_command import GitCommand, git_require
33from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, ID_RE 33from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
34 ID_RE
34from error import GitError, HookError, UploadError, DownloadError 35from error import GitError, HookError, UploadError, DownloadError
35from error import ManifestInvalidRevisionError 36from error import ManifestInvalidRevisionError
36from error import NoManifestException 37from error import NoManifestException
@@ -39,11 +40,18 @@ from trace import IsTrace, Trace
39from 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
40 41
41from pyversion import is_python3 42from pyversion import is_python3
42if 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
43 # pylint:disable=W0622 50 # pylint:disable=W0622
44 input = raw_input 51 input = raw_input
45 # pylint:enable=W0622 52 # pylint:enable=W0622
46 53
54
47def _lwrite(path, content): 55def _lwrite(path, content):
48 lock = '%s.lock' % path 56 lock = '%s.lock' % path
49 57
@@ -59,21 +67,27 @@ def _lwrite(path, content):
59 os.remove(lock) 67 os.remove(lock)
60 raise 68 raise
61 69
70
62def _error(fmt, *args): 71def _error(fmt, *args):
63 msg = fmt % args 72 msg = fmt % args
64 print('error: %s' % msg, file=sys.stderr) 73 print('error: %s' % msg, file=sys.stderr)
65 74
75
66def _warn(fmt, *args): 76def _warn(fmt, *args):
67 msg = fmt % args 77 msg = fmt % args
68 print('warn: %s' % msg, file=sys.stderr) 78 print('warn: %s' % msg, file=sys.stderr)
69 79
80
70def not_rev(r): 81def not_rev(r):
71 return '^' + r 82 return '^' + r
72 83
84
73def sq(r): 85def sq(r):
74 return "'" + r.replace("'", "'\''") + "'" 86 return "'" + r.replace("'", "'\''") + "'"
75 87
76_project_hook_list = None 88_project_hook_list = None
89
90
77def _ProjectHooks(): 91def _ProjectHooks():
78 """List the hooks present in the 'hooks' directory. 92 """List the hooks present in the 'hooks' directory.
79 93
@@ -107,15 +121,14 @@ class DownloadedChange(object):
107 @property 121 @property
108 def commits(self): 122 def commits(self):
109 if self._commit_cache is None: 123 if self._commit_cache is None:
110 self._commit_cache = self.project.bare_git.rev_list( 124 self._commit_cache = self.project.bare_git.rev_list('--abbrev=8',
111 '--abbrev=8', 125 '--abbrev-commit',
112 '--abbrev-commit', 126 '--pretty=oneline',
113 '--pretty=oneline', 127 '--reverse',
114 '--reverse', 128 '--date-order',
115 '--date-order', 129 not_rev(self.base),
116 not_rev(self.base), 130 self.commit,
117 self.commit, 131 '--')
118 '--')
119 return self._commit_cache 132 return self._commit_cache
120 133
121 134
@@ -134,36 +147,36 @@ class ReviewableBranch(object):
134 @property 147 @property
135 def commits(self): 148 def commits(self):
136 if self._commit_cache is None: 149 if self._commit_cache is None:
137 self._commit_cache = self.project.bare_git.rev_list( 150 self._commit_cache = self.project.bare_git.rev_list('--abbrev=8',
138 '--abbrev=8', 151 '--abbrev-commit',
139 '--abbrev-commit', 152 '--pretty=oneline',
140 '--pretty=oneline', 153 '--reverse',
141 '--reverse', 154 '--date-order',
142 '--date-order', 155 not_rev(self.base),
143 not_rev(self.base), 156 R_HEADS + self.name,
144 R_HEADS + self.name, 157 '--')
145 '--')
146 return self._commit_cache 158 return self._commit_cache
147 159
148 @property 160 @property
149 def unabbrev_commits(self): 161 def unabbrev_commits(self):
150 r = dict() 162 r = dict()
151 for commit in self.project.bare_git.rev_list( 163 for commit in self.project.bare_git.rev_list(not_rev(self.base),
152 not_rev(self.base), 164 R_HEADS + self.name,
153 R_HEADS + self.name, 165 '--'):
154 '--'):
155 r[commit[0:8]] = commit 166 r[commit[0:8]] = commit
156 return r 167 return r
157 168
158 @property 169 @property
159 def date(self): 170 def date(self):
160 return self.project.bare_git.log( 171 return self.project.bare_git.log('--pretty=format:%cd',
161 '--pretty=format:%cd', 172 '-n', '1',
162 '-n', '1', 173 R_HEADS + self.name,
163 R_HEADS + self.name, 174 '--')
164 '--')
165 175
166 def UploadForReview(self, people, auto_topic=False, draft=False, dest_branch=None): 176 def UploadForReview(self, people,
177 auto_topic=False,
178 draft=False,
179 dest_branch=None):
167 self.project.UploadForReview(self.name, 180 self.project.UploadForReview(self.name,
168 people, 181 people,
169 auto_topic=auto_topic, 182 auto_topic=auto_topic,
@@ -173,8 +186,8 @@ class ReviewableBranch(object):
173 def GetPublishedRefs(self): 186 def GetPublishedRefs(self):
174 refs = {} 187 refs = {}
175 output = self.project.bare_git.ls_remote( 188 output = self.project.bare_git.ls_remote(
176 self.branch.remote.SshReviewUrl(self.project.UserEmail), 189 self.branch.remote.SshReviewUrl(self.project.UserEmail),
177 'refs/changes/*') 190 'refs/changes/*')
178 for line in output.split('\n'): 191 for line in output.split('\n'):
179 try: 192 try:
180 (sha, ref) = line.split() 193 (sha, ref) = line.split()
@@ -184,7 +197,9 @@ class ReviewableBranch(object):
184 197
185 return refs 198 return refs
186 199
200
187class StatusColoring(Coloring): 201class StatusColoring(Coloring):
202
188 def __init__(self, config): 203 def __init__(self, config):
189 Coloring.__init__(self, config, 'status') 204 Coloring.__init__(self, config, 'status')
190 self.project = self.printer('header', attr='bold') 205 self.project = self.printer('header', attr='bold')
@@ -198,17 +213,22 @@ class StatusColoring(Coloring):
198 213
199 214
200class DiffColoring(Coloring): 215class DiffColoring(Coloring):
216
201 def __init__(self, config): 217 def __init__(self, config):
202 Coloring.__init__(self, config, 'diff') 218 Coloring.__init__(self, config, 'diff')
203 self.project = self.printer('header', attr='bold') 219 self.project = self.printer('header', attr='bold')
204 220
221
205class _Annotation(object): 222class _Annotation(object):
223
206 def __init__(self, name, value, keep): 224 def __init__(self, name, value, keep):
207 self.name = name 225 self.name = name
208 self.value = value 226 self.value = value
209 self.keep = keep 227 self.keep = keep
210 228
229
211class _CopyFile(object): 230class _CopyFile(object):
231
212 def __init__(self, src, dest, abssrc, absdest): 232 def __init__(self, src, dest, abssrc, absdest):
213 self.src = src 233 self.src = src
214 self.dest = dest 234 self.dest = dest
@@ -236,7 +256,9 @@ class _CopyFile(object):
236 except IOError: 256 except IOError:
237 _error('Cannot copy file %s to %s', src, dest) 257 _error('Cannot copy file %s to %s', src, dest)
238 258
259
239class _LinkFile(object): 260class _LinkFile(object):
261
240 def __init__(self, git_worktree, src, dest, relsrc, absdest): 262 def __init__(self, git_worktree, src, dest, relsrc, absdest):
241 self.git_worktree = git_worktree 263 self.git_worktree = git_worktree
242 self.src = src 264 self.src = src
@@ -275,7 +297,7 @@ class _LinkFile(object):
275 absDestDir = self.abs_dest 297 absDestDir = self.abs_dest
276 if os.path.exists(absDestDir) and not os.path.isdir(absDestDir): 298 if os.path.exists(absDestDir) and not os.path.isdir(absDestDir):
277 _error('Link error: src with wildcard, %s must be a directory', 299 _error('Link error: src with wildcard, %s must be a directory',
278 absDestDir) 300 absDestDir)
279 else: 301 else:
280 absSrcFiles = glob.glob(absSrc) 302 absSrcFiles = glob.glob(absSrc)
281 for absSrcFile in absSrcFiles: 303 for absSrcFile in absSrcFiles:
@@ -292,18 +314,24 @@ class _LinkFile(object):
292 relSrc = os.path.join(relSrcDir, srcFile) 314 relSrc = os.path.join(relSrcDir, srcFile)
293 self.__linkIt(relSrc, absDest) 315 self.__linkIt(relSrc, absDest)
294 316
317
295class RemoteSpec(object): 318class RemoteSpec(object):
319
296 def __init__(self, 320 def __init__(self,
297 name, 321 name,
298 url=None, 322 url=None,
299 review=None, 323 review=None,
300 revision=None): 324 revision=None,
325 orig_name=None):
301 self.name = name 326 self.name = name
302 self.url = url 327 self.url = url
303 self.review = review 328 self.review = review
304 self.revision = revision 329 self.revision = revision
330 self.orig_name = orig_name
331
305 332
306class RepoHook(object): 333class RepoHook(object):
334
307 """A RepoHook contains information about a script to run as a hook. 335 """A RepoHook contains information about a script to run as a hook.
308 336
309 Hooks are used to run a python script before running an upload (for instance, 337 Hooks are used to run a python script before running an upload (for instance,
@@ -316,10 +344,12 @@ class RepoHook(object):
316 Hooks are always python. When a hook is run, we will load the hook into the 344 Hooks are always python. When a hook is run, we will load the hook into the
317 interpreter and execute its main() function. 345 interpreter and execute its main() function.
318 """ 346 """
347
319 def __init__(self, 348 def __init__(self,
320 hook_type, 349 hook_type,
321 hooks_project, 350 hooks_project,
322 topdir, 351 topdir,
352 manifest_url,
323 abort_if_user_denies=False): 353 abort_if_user_denies=False):
324 """RepoHook constructor. 354 """RepoHook constructor.
325 355
@@ -333,11 +363,13 @@ class RepoHook(object):
333 topdir: Repo's top directory (the one containing the .repo directory). 363 topdir: Repo's top directory (the one containing the .repo directory).
334 Scripts will run with CWD as this directory. If you have a manifest, 364 Scripts will run with CWD as this directory. If you have a manifest,
335 this is manifest.topdir 365 this is manifest.topdir
366 manifest_url: The URL to the manifest git repo.
336 abort_if_user_denies: If True, we'll throw a HookError() if the user 367 abort_if_user_denies: If True, we'll throw a HookError() if the user
337 doesn't allow us to run the hook. 368 doesn't allow us to run the hook.
338 """ 369 """
339 self._hook_type = hook_type 370 self._hook_type = hook_type
340 self._hooks_project = hooks_project 371 self._hooks_project = hooks_project
372 self._manifest_url = manifest_url
341 self._topdir = topdir 373 self._topdir = topdir
342 self._abort_if_user_denies = abort_if_user_denies 374 self._abort_if_user_denies = abort_if_user_denies
343 375
@@ -386,9 +418,9 @@ class RepoHook(object):
386 def _CheckForHookApproval(self): 418 def _CheckForHookApproval(self):
387 """Check to see whether this hook has been approved. 419 """Check to see whether this hook has been approved.
388 420
389 We'll look at the hash of all of the hooks. If this matches the hash that 421 We'll accept approval of manifest URLs if they're using secure transports.
390 the user last approved, we're done. If it doesn't, we'll ask the user 422 This way the user can say they trust the manifest hoster. For insecure
391 about approval. 423 hosts, we fall back to checking the hash of the hooks repo.
392 424
393 Note that we ask permission for each individual hook even though we use 425 Note that we ask permission for each individual hook even though we use
394 the hash of all hooks when detecting changes. We'd like the user to be 426 the hash of all hooks when detecting changes. We'd like the user to be
@@ -402,44 +434,58 @@ class RepoHook(object):
402 HookError: Raised if the user doesn't approve and abort_if_user_denies 434 HookError: Raised if the user doesn't approve and abort_if_user_denies
403 was passed to the consturctor. 435 was passed to the consturctor.
404 """ 436 """
405 hooks_config = self._hooks_project.config 437 if self._ManifestUrlHasSecureScheme():
406 git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type 438 return self._CheckForHookApprovalManifest()
439 else:
440 return self._CheckForHookApprovalHash()
441
442 def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
443 changed_prompt):
444 """Check for approval for a particular attribute and hook.
407 445
408 # Get the last hash that the user approved for this hook; may be None. 446 Args:
409 old_hash = hooks_config.GetString(git_approval_key) 447 subkey: The git config key under [repo.hooks.<hook_type>] to store the
448 last approved string.
449 new_val: The new value to compare against the last approved one.
450 main_prompt: Message to display to the user to ask for approval.
451 changed_prompt: Message explaining why we're re-asking for approval.
410 452
411 # Get the current hash so we can tell if scripts changed since approval. 453 Returns:
412 new_hash = self._GetHash() 454 True if this hook is approved to run; False otherwise.
413 455
414 if old_hash is not None: 456 Raises:
457 HookError: Raised if the user doesn't approve and abort_if_user_denies
458 was passed to the consturctor.
459 """
460 hooks_config = self._hooks_project.config
461 git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
462
463 # Get the last value that the user approved for this hook; may be None.
464 old_val = hooks_config.GetString(git_approval_key)
465
466 if old_val is not None:
415 # User previously approved hook and asked not to be prompted again. 467 # User previously approved hook and asked not to be prompted again.
416 if new_hash == old_hash: 468 if new_val == old_val:
417 # Approval matched. We're done. 469 # Approval matched. We're done.
418 return True 470 return True
419 else: 471 else:
420 # Give the user a reason why we're prompting, since they last told 472 # Give the user a reason why we're prompting, since they last told
421 # us to "never ask again". 473 # us to "never ask again".
422 prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % ( 474 prompt = 'WARNING: %s\n\n' % (changed_prompt,)
423 self._hook_type)
424 else: 475 else:
425 prompt = '' 476 prompt = ''
426 477
427 # Prompt the user if we're not on a tty; on a tty we'll assume "no". 478 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
428 if sys.stdout.isatty(): 479 if sys.stdout.isatty():
429 prompt += ('Repo %s run the script:\n' 480 prompt += main_prompt + ' (yes/always/NO)? '
430 ' %s\n'
431 '\n'
432 'Do you want to allow this script to run '
433 '(yes/yes-never-ask-again/NO)? ') % (
434 self._GetMustVerb(), self._script_fullpath)
435 response = input(prompt).lower() 481 response = input(prompt).lower()
436 print() 482 print()
437 483
438 # User is doing a one-time approval. 484 # User is doing a one-time approval.
439 if response in ('y', 'yes'): 485 if response in ('y', 'yes'):
440 return True 486 return True
441 elif response == 'yes-never-ask-again': 487 elif response == 'always':
442 hooks_config.SetString(git_approval_key, new_hash) 488 hooks_config.SetString(git_approval_key, new_val)
443 return True 489 return True
444 490
445 # For anything else, we'll assume no approval. 491 # For anything else, we'll assume no approval.
@@ -449,6 +495,40 @@ class RepoHook(object):
449 495
450 return False 496 return False
451 497
498 def _ManifestUrlHasSecureScheme(self):
499 """Check if the URI for the manifest is a secure transport."""
500 secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
501 parse_results = urllib.parse.urlparse(self._manifest_url)
502 return parse_results.scheme in secure_schemes
503
504 def _CheckForHookApprovalManifest(self):
505 """Check whether the user has approved this manifest host.
506
507 Returns:
508 True if this hook is approved to run; False otherwise.
509 """
510 return self._CheckForHookApprovalHelper(
511 'approvedmanifest',
512 self._manifest_url,
513 'Run hook scripts from %s' % (self._manifest_url,),
514 'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
515
516 def _CheckForHookApprovalHash(self):
517 """Check whether the user has approved the hooks repo.
518
519 Returns:
520 True if this hook is approved to run; False otherwise.
521 """
522 prompt = ('Repo %s run the script:\n'
523 ' %s\n'
524 '\n'
525 'Do you want to allow this script to run')
526 return self._CheckForHookApprovalHelper(
527 'approvedhash',
528 self._GetHash(),
529 prompt % (self._GetMustVerb(), self._script_fullpath),
530 'Scripts have changed since %s was allowed.' % (self._hook_type,))
531
452 def _ExecuteHook(self, **kwargs): 532 def _ExecuteHook(self, **kwargs):
453 """Actually execute the given hook. 533 """Actually execute the given hook.
454 534
@@ -475,19 +555,18 @@ class RepoHook(object):
475 555
476 # Exec, storing global context in the context dict. We catch exceptions 556 # Exec, storing global context in the context dict. We catch exceptions
477 # and convert to a HookError w/ just the failing traceback. 557 # and convert to a HookError w/ just the failing traceback.
478 context = {} 558 context = {'__file__': self._script_fullpath}
479 try: 559 try:
480 exec(compile(open(self._script_fullpath).read(), 560 exec(compile(open(self._script_fullpath).read(),
481 self._script_fullpath, 'exec'), context) 561 self._script_fullpath, 'exec'), context)
482 except Exception: 562 except Exception:
483 raise HookError('%s\nFailed to import %s hook; see traceback above.' % ( 563 raise HookError('%s\nFailed to import %s hook; see traceback above.' %
484 traceback.format_exc(), self._hook_type)) 564 (traceback.format_exc(), self._hook_type))
485 565
486 # Running the script should have defined a main() function. 566 # Running the script should have defined a main() function.
487 if 'main' not in context: 567 if 'main' not in context:
488 raise HookError('Missing main() in: "%s"' % self._script_fullpath) 568 raise HookError('Missing main() in: "%s"' % self._script_fullpath)
489 569
490
491 # Add 'hook_should_take_kwargs' to the arguments to be passed to main. 570 # Add 'hook_should_take_kwargs' to the arguments to be passed to main.
492 # We don't actually want hooks to define their main with this argument-- 571 # We don't actually want hooks to define their main with this argument--
493 # it's there to remind them that their hook should always take **kwargs. 572 # it's there to remind them that their hook should always take **kwargs.
@@ -505,8 +584,8 @@ class RepoHook(object):
505 context['main'](**kwargs) 584 context['main'](**kwargs)
506 except Exception: 585 except Exception:
507 raise HookError('%s\nFailed to run main() for %s hook; see traceback ' 586 raise HookError('%s\nFailed to run main() for %s hook; see traceback '
508 'above.' % ( 587 'above.' % (traceback.format_exc(),
509 traceback.format_exc(), self._hook_type)) 588 self._hook_type))
510 finally: 589 finally:
511 # Restore sys.path and CWD. 590 # Restore sys.path and CWD.
512 sys.path = orig_syspath 591 sys.path = orig_syspath
@@ -530,8 +609,8 @@ class RepoHook(object):
530 to run a required hook (from _CheckForHookApproval). 609 to run a required hook (from _CheckForHookApproval).
531 """ 610 """
532 # No-op if there is no hooks project or if hook is disabled. 611 # No-op if there is no hooks project or if hook is disabled.
533 if ((not self._hooks_project) or 612 if ((not self._hooks_project) or (self._hook_type not in
534 (self._hook_type not in self._hooks_project.enabled_repo_hooks)): 613 self._hooks_project.enabled_repo_hooks)):
535 return 614 return
536 615
537 # Bail with a nice error if we can't find the hook. 616 # Bail with a nice error if we can't find the hook.
@@ -553,6 +632,7 @@ class Project(object):
553 # These objects can only be used by a single working tree. 632 # These objects can only be used by a single working tree.
554 working_tree_files = ['config', 'packed-refs', 'shallow'] 633 working_tree_files = ['config', 'packed-refs', 'shallow']
555 working_tree_dirs = ['logs', 'refs'] 634 working_tree_dirs = ['logs', 'refs']
635
556 def __init__(self, 636 def __init__(self,
557 manifest, 637 manifest,
558 name, 638 name,
@@ -605,15 +685,15 @@ class Project(object):
605 self.gitdir = gitdir.replace('\\', '/') 685 self.gitdir = gitdir.replace('\\', '/')
606 self.objdir = objdir.replace('\\', '/') 686 self.objdir = objdir.replace('\\', '/')
607 if worktree: 687 if worktree:
608 self.worktree = worktree.replace('\\', '/') 688 self.worktree = os.path.normpath(worktree.replace('\\', '/'))
609 else: 689 else:
610 self.worktree = None 690 self.worktree = None
611 self.relpath = relpath 691 self.relpath = relpath
612 self.revisionExpr = revisionExpr 692 self.revisionExpr = revisionExpr
613 693
614 if revisionId is None \ 694 if revisionId is None \
615 and revisionExpr \ 695 and revisionExpr \
616 and IsId(revisionExpr): 696 and IsId(revisionExpr):
617 self.revisionId = revisionExpr 697 self.revisionId = revisionExpr
618 else: 698 else:
619 self.revisionId = revisionId 699 self.revisionId = revisionId
@@ -633,9 +713,8 @@ class Project(object):
633 self.copyfiles = [] 713 self.copyfiles = []
634 self.linkfiles = [] 714 self.linkfiles = []
635 self.annotations = [] 715 self.annotations = []
636 self.config = GitConfig.ForRepository( 716 self.config = GitConfig.ForRepository(gitdir=self.gitdir,
637 gitdir=self.gitdir, 717 defaults=self.manifest.globalConfig)
638 defaults=self.manifest.globalConfig)
639 718
640 if self.worktree: 719 if self.worktree:
641 self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir) 720 self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir)
@@ -773,7 +852,7 @@ class Project(object):
773 """ 852 """
774 expanded_manifest_groups = manifest_groups or ['default'] 853 expanded_manifest_groups = manifest_groups or ['default']
775 expanded_project_groups = ['all'] + (self.groups or []) 854 expanded_project_groups = ['all'] + (self.groups or [])
776 if not 'notdefault' in expanded_project_groups: 855 if 'notdefault' not in expanded_project_groups:
777 expanded_project_groups += ['default'] 856 expanded_project_groups += ['default']
778 857
779 matched = False 858 matched = False
@@ -785,7 +864,7 @@ class Project(object):
785 864
786 return matched 865 return matched
787 866
788## Status Display ## 867# Status Display ##
789 def UncommitedFiles(self, get_all=True): 868 def UncommitedFiles(self, get_all=True):
790 """Returns a list of strings, uncommitted files in the git tree. 869 """Returns a list of strings, uncommitted files in the git tree.
791 870
@@ -837,7 +916,7 @@ class Project(object):
837 output: If specified, redirect the output to this object. 916 output: If specified, redirect the output to this object.
838 """ 917 """
839 if not os.path.isdir(self.worktree): 918 if not os.path.isdir(self.worktree):
840 if output_redir == None: 919 if output_redir is None:
841 output_redir = sys.stdout 920 output_redir = sys.stdout
842 print(file=output_redir) 921 print(file=output_redir)
843 print('project %s/' % self.relpath, file=output_redir) 922 print('project %s/' % self.relpath, file=output_redir)
@@ -856,7 +935,7 @@ class Project(object):
856 return 'CLEAN' 935 return 'CLEAN'
857 936
858 out = StatusColoring(self.config) 937 out = StatusColoring(self.config)
859 if not output_redir == None: 938 if output_redir is not None:
860 out.redirect(output_redir) 939 out.redirect(output_redir)
861 out.project('project %-40s', self.relpath + '/ ') 940 out.project('project %-40s', self.relpath + '/ ')
862 941
@@ -899,7 +978,7 @@ class Project(object):
899 978
900 if i and i.src_path: 979 if i and i.src_path:
901 line = ' %s%s\t%s => %s (%s%%)' % (i_status, f_status, 980 line = ' %s%s\t%s => %s (%s%%)' % (i_status, f_status,
902 i.src_path, p, i.level) 981 i.src_path, p, i.level)
903 else: 982 else:
904 line = ' %s%s\t%s' % (i_status, f_status, p) 983 line = ' %s%s\t%s' % (i_status, f_status, p)
905 984
@@ -942,7 +1021,7 @@ class Project(object):
942 p.Wait() 1021 p.Wait()
943 1022
944 1023
945## Publish / Upload ## 1024# Publish / Upload ##
946 1025
947 def WasPublished(self, branch, all_refs=None): 1026 def WasPublished(self, branch, all_refs=None):
948 """Was the branch published (uploaded) for code review? 1027 """Was the branch published (uploaded) for code review?
@@ -1085,7 +1164,7 @@ class Project(object):
1085 message=msg) 1164 message=msg)
1086 1165
1087 1166
1088## Sync ## 1167# Sync ##
1089 1168
1090 def _ExtractArchive(self, tarpath, path=None): 1169 def _ExtractArchive(self, tarpath, path=None):
1091 """Extract the given tar on its current location 1170 """Extract the given tar on its current location
@@ -1103,15 +1182,15 @@ class Project(object):
1103 return False 1182 return False
1104 1183
1105 def Sync_NetworkHalf(self, 1184 def Sync_NetworkHalf(self,
1106 quiet=False, 1185 quiet=False,
1107 is_new=None, 1186 is_new=None,
1108 current_branch_only=False, 1187 current_branch_only=False,
1109 force_sync=False, 1188 force_sync=False,
1110 clone_bundle=True, 1189 clone_bundle=True,
1111 no_tags=False, 1190 no_tags=False,
1112 archive=False, 1191 archive=False,
1113 optimized_fetch=False, 1192 optimized_fetch=False,
1114 prune=False): 1193 prune=False):
1115 """Perform only the network IO portion of the sync process. 1194 """Perform only the network IO portion of the sync process.
1116 Local working directory/branch state is not affected. 1195 Local working directory/branch state is not affected.
1117 """ 1196 """
@@ -1164,8 +1243,8 @@ class Project(object):
1164 alt_dir = None 1243 alt_dir = None
1165 1244
1166 if clone_bundle \ 1245 if clone_bundle \
1167 and alt_dir is None \ 1246 and alt_dir is None \
1168 and self._ApplyCloneBundle(initial=is_new, quiet=quiet): 1247 and self._ApplyCloneBundle(initial=is_new, quiet=quiet):
1169 is_new = False 1248 is_new = False
1170 1249
1171 if not current_branch_only: 1250 if not current_branch_only:
@@ -1177,12 +1256,13 @@ class Project(object):
1177 elif self.manifest.default.sync_c: 1256 elif self.manifest.default.sync_c:
1178 current_branch_only = True 1257 current_branch_only = True
1179 1258
1180 need_to_fetch = not (optimized_fetch and \ 1259 need_to_fetch = not (optimized_fetch and
1181 (ID_RE.match(self.revisionExpr) and self._CheckForSha1())) 1260 (ID_RE.match(self.revisionExpr) and
1182 if (need_to_fetch 1261 self._CheckForSha1()))
1183 and not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir, 1262 if (need_to_fetch and
1184 current_branch_only=current_branch_only, 1263 not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir,
1185 no_tags=no_tags, prune=prune)): 1264 current_branch_only=current_branch_only,
1265 no_tags=no_tags, prune=prune)):
1186 return False 1266 return False
1187 1267
1188 if self.worktree: 1268 if self.worktree:
@@ -1219,9 +1299,8 @@ class Project(object):
1219 try: 1299 try:
1220 return self.bare_git.rev_list(self.revisionExpr, '-1')[0] 1300 return self.bare_git.rev_list(self.revisionExpr, '-1')[0]
1221 except GitError: 1301 except GitError:
1222 raise ManifestInvalidRevisionError( 1302 raise ManifestInvalidRevisionError('revision %s in %s not found' %
1223 'revision %s in %s not found' % (self.revisionExpr, 1303 (self.revisionExpr, self.name))
1224 self.name))
1225 1304
1226 def GetRevisionId(self, all_refs=None): 1305 def GetRevisionId(self, all_refs=None):
1227 if self.revisionId: 1306 if self.revisionId:
@@ -1236,9 +1315,8 @@ class Project(object):
1236 try: 1315 try:
1237 return self.bare_git.rev_parse('--verify', '%s^0' % rev) 1316 return self.bare_git.rev_parse('--verify', '%s^0' % rev)
1238 except GitError: 1317 except GitError:
1239 raise ManifestInvalidRevisionError( 1318 raise ManifestInvalidRevisionError('revision %s in %s not found' %
1240 'revision %s in %s not found' % (self.revisionExpr, 1319 (self.revisionExpr, self.name))
1241 self.name))
1242 1320
1243 def Sync_LocalHalf(self, syncbuf, force_sync=False): 1321 def Sync_LocalHalf(self, syncbuf, force_sync=False):
1244 """Perform only the local IO portion of the sync process. 1322 """Perform only the local IO portion of the sync process.
@@ -1327,8 +1405,8 @@ class Project(object):
1327 # to rewrite the published commits so we punt. 1405 # to rewrite the published commits so we punt.
1328 # 1406 #
1329 syncbuf.fail(self, 1407 syncbuf.fail(self,
1330 "branch %s is published (but not merged) and is now %d commits behind" 1408 "branch %s is published (but not merged) and is now "
1331 % (branch.name, len(upstream_gain))) 1409 "%d commits behind" % (branch.name, len(upstream_gain)))
1332 return 1410 return
1333 elif pub == head: 1411 elif pub == head:
1334 # All published commits are merged, and thus we are a 1412 # All published commits are merged, and thus we are a
@@ -1422,7 +1500,7 @@ class Project(object):
1422 remote = self.GetRemote(self.remote.name) 1500 remote = self.GetRemote(self.remote.name)
1423 1501
1424 cmd = ['fetch', remote.name] 1502 cmd = ['fetch', remote.name]
1425 cmd.append('refs/changes/%2.2d/%d/%d' \ 1503 cmd.append('refs/changes/%2.2d/%d/%d'
1426 % (change_id % 100, change_id, patch_id)) 1504 % (change_id % 100, change_id, patch_id))
1427 if GitCommand(self, cmd, bare=True).Wait() != 0: 1505 if GitCommand(self, cmd, bare=True).Wait() != 0:
1428 return None 1506 return None
@@ -1433,7 +1511,7 @@ class Project(object):
1433 self.bare_git.rev_parse('FETCH_HEAD')) 1511 self.bare_git.rev_parse('FETCH_HEAD'))
1434 1512
1435 1513
1436## Branch Management ## 1514# Branch Management ##
1437 1515
1438 def StartBranch(self, name, branch_merge=''): 1516 def StartBranch(self, name, branch_merge=''):
1439 """Create a new branch off the manifest's revision. 1517 """Create a new branch off the manifest's revision.
@@ -1620,10 +1698,11 @@ class Project(object):
1620 return kept 1698 return kept
1621 1699
1622 1700
1623## Submodule Management ## 1701# Submodule Management ##
1624 1702
1625 def GetRegisteredSubprojects(self): 1703 def GetRegisteredSubprojects(self):
1626 result = [] 1704 result = []
1705
1627 def rec(subprojects): 1706 def rec(subprojects):
1628 if not subprojects: 1707 if not subprojects:
1629 return 1708 return
@@ -1658,6 +1737,7 @@ class Project(object):
1658 1737
1659 re_path = re.compile(r'^submodule\.([^.]+)\.path=(.*)$') 1738 re_path = re.compile(r'^submodule\.([^.]+)\.path=(.*)$')
1660 re_url = re.compile(r'^submodule\.([^.]+)\.url=(.*)$') 1739 re_url = re.compile(r'^submodule\.([^.]+)\.url=(.*)$')
1740
1661 def parse_gitmodules(gitdir, rev): 1741 def parse_gitmodules(gitdir, rev):
1662 cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev] 1742 cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev]
1663 try: 1743 try:
@@ -1767,7 +1847,7 @@ class Project(object):
1767 return result 1847 return result
1768 1848
1769 1849
1770## Direct Git Commands ## 1850# Direct Git Commands ##
1771 def _CheckForSha1(self): 1851 def _CheckForSha1(self):
1772 try: 1852 try:
1773 # if revision (sha or tag) is not present then following function 1853 # if revision (sha or tag) is not present then following function
@@ -1791,7 +1871,6 @@ class Project(object):
1791 if command.Wait() != 0: 1871 if command.Wait() != 0:
1792 raise GitError('git archive %s: %s' % (self.name, command.stderr)) 1872 raise GitError('git archive %s: %s' % (self.name, command.stderr))
1793 1873
1794
1795 def _RemoteFetch(self, name=None, 1874 def _RemoteFetch(self, name=None,
1796 current_branch_only=False, 1875 current_branch_only=False,
1797 initial=False, 1876 initial=False,
@@ -1838,7 +1917,10 @@ class Project(object):
1838 # will fail. 1917 # will fail.
1839 # * otherwise, fetch all branches to make sure we end up with the 1918 # * otherwise, fetch all branches to make sure we end up with the
1840 # specific commit. 1919 # specific commit.
1841 current_branch_only = self.upstream and not ID_RE.match(self.upstream) 1920 if self.upstream:
1921 current_branch_only = not ID_RE.match(self.upstream)
1922 else:
1923 current_branch_only = False
1842 1924
1843 if not name: 1925 if not name:
1844 name = self.remote.name 1926 name = self.remote.name
@@ -1955,9 +2037,9 @@ class Project(object):
1955 break 2037 break
1956 continue 2038 continue
1957 elif current_branch_only and is_sha1 and ret == 128: 2039 elif current_branch_only and is_sha1 and ret == 128:
1958 # Exit code 128 means "couldn't find the ref you asked for"; if we're in sha1 2040 # Exit code 128 means "couldn't find the ref you asked for"; if we're
1959 # mode, we just tried sync'ing from the upstream field; it doesn't exist, thus 2041 # in sha1 mode, we just tried sync'ing from the upstream field; it
1960 # abort the optimization attempt and do a full sync. 2042 # doesn't exist, thus abort the optimization attempt and do a full sync.
1961 break 2043 break
1962 elif ret < 0: 2044 elif ret < 0:
1963 # Git died with a signal, exit immediately 2045 # Git died with a signal, exit immediately
@@ -1984,20 +2066,24 @@ class Project(object):
1984 initial=False, quiet=quiet, alt_dir=alt_dir) 2066 initial=False, quiet=quiet, alt_dir=alt_dir)
1985 if self.clone_depth: 2067 if self.clone_depth:
1986 self.clone_depth = None 2068 self.clone_depth = None
1987 return self._RemoteFetch(name=name, current_branch_only=current_branch_only, 2069 return self._RemoteFetch(name=name,
2070 current_branch_only=current_branch_only,
1988 initial=False, quiet=quiet, alt_dir=alt_dir) 2071 initial=False, quiet=quiet, alt_dir=alt_dir)
1989 2072
1990 return ok 2073 return ok
1991 2074
1992 def _ApplyCloneBundle(self, initial=False, quiet=False): 2075 def _ApplyCloneBundle(self, initial=False, quiet=False):
1993 if initial and (self.manifest.manifestProject.config.GetString('repo.depth') or self.clone_depth): 2076 if initial and \
2077 (self.manifest.manifestProject.config.GetString('repo.depth') or
2078 self.clone_depth):
1994 return False 2079 return False
1995 2080
1996 remote = self.GetRemote(self.remote.name) 2081 remote = self.GetRemote(self.remote.name)
1997 bundle_url = remote.url + '/clone.bundle' 2082 bundle_url = remote.url + '/clone.bundle'
1998 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url) 2083 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
1999 if GetSchemeFromUrl(bundle_url) not in ( 2084 if GetSchemeFromUrl(bundle_url) not in ('http', 'https',
2000 'http', 'https', 'persistent-http', 'persistent-https'): 2085 'persistent-http',
2086 'persistent-https'):
2001 return False 2087 return False
2002 2088
2003 bundle_dst = os.path.join(self.gitdir, 'clone.bundle') 2089 bundle_dst = os.path.join(self.gitdir, 'clone.bundle')
@@ -2046,7 +2132,7 @@ class Project(object):
2046 os.remove(tmpPath) 2132 os.remove(tmpPath)
2047 if 'http_proxy' in os.environ and 'darwin' == sys.platform: 2133 if 'http_proxy' in os.environ and 'darwin' == sys.platform:
2048 cmd += ['--proxy', os.environ['http_proxy']] 2134 cmd += ['--proxy', os.environ['http_proxy']]
2049 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, proxy): 2135 with GetUrlCookieFile(srcUrl, quiet) as (cookiefile, _proxy):
2050 if cookiefile: 2136 if cookiefile:
2051 cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile] 2137 cmd += ['--cookie', cookiefile, '--cookie-jar', cookiefile]
2052 if srcUrl.startswith('persistent-'): 2138 if srcUrl.startswith('persistent-'):
@@ -2165,11 +2251,12 @@ class Project(object):
2165 self._CheckDirReference(self.objdir, self.gitdir, share_refs=False) 2251 self._CheckDirReference(self.objdir, self.gitdir, share_refs=False)
2166 except GitError as e: 2252 except GitError as e:
2167 if force_sync: 2253 if force_sync:
2168 print("Retrying clone after deleting %s" % self.gitdir, file=sys.stderr) 2254 print("Retrying clone after deleting %s" %
2255 self.gitdir, file=sys.stderr)
2169 try: 2256 try:
2170 shutil.rmtree(os.path.realpath(self.gitdir)) 2257 shutil.rmtree(os.path.realpath(self.gitdir))
2171 if self.worktree and os.path.exists( 2258 if self.worktree and os.path.exists(os.path.realpath
2172 os.path.realpath(self.worktree)): 2259 (self.worktree)):
2173 shutil.rmtree(os.path.realpath(self.worktree)) 2260 shutil.rmtree(os.path.realpath(self.worktree))
2174 return self._InitGitDir(mirror_git=mirror_git, force_sync=False) 2261 return self._InitGitDir(mirror_git=mirror_git, force_sync=False)
2175 except: 2262 except:
@@ -2205,6 +2292,7 @@ class Project(object):
2205 for key in ['user.name', 'user.email']: 2292 for key in ['user.name', 'user.email']:
2206 if m.Has(key, include_defaults=False): 2293 if m.Has(key, include_defaults=False):
2207 self.config.SetString(key, m.GetString(key)) 2294 self.config.SetString(key, m.GetString(key))
2295 self.config.SetString('filter.lfs.smudge', 'git-lfs smudge --skip -- %f')
2208 if self.manifest.IsMirror: 2296 if self.manifest.IsMirror:
2209 self.config.SetString('core.bare', 'true') 2297 self.config.SetString('core.bare', 'true')
2210 else: 2298 else:
@@ -2228,7 +2316,7 @@ class Project(object):
2228 name = os.path.basename(stock_hook) 2316 name = os.path.basename(stock_hook)
2229 2317
2230 if name in ('commit-msg',) and not self.remote.review \ 2318 if name in ('commit-msg',) and not self.remote.review \
2231 and not self is self.manifest.manifestProject: 2319 and self is not self.manifest.manifestProject:
2232 # Don't install a Gerrit Code Review hook if this 2320 # Don't install a Gerrit Code Review hook if this
2233 # project does not appear to use it for reviews. 2321 # project does not appear to use it for reviews.
2234 # 2322 #
@@ -2243,7 +2331,8 @@ class Project(object):
2243 if filecmp.cmp(stock_hook, dst, shallow=False): 2331 if filecmp.cmp(stock_hook, dst, shallow=False):
2244 os.remove(dst) 2332 os.remove(dst)
2245 else: 2333 else:
2246 _warn("%s: Not replacing locally modified %s hook", self.relpath, name) 2334 _warn("%s: Not replacing locally modified %s hook",
2335 self.relpath, name)
2247 continue 2336 continue
2248 try: 2337 try:
2249 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst) 2338 os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst)
@@ -2289,8 +2378,8 @@ class Project(object):
2289 self.bare_git.symbolic_ref('-m', msg, ref, dst) 2378 self.bare_git.symbolic_ref('-m', msg, ref, dst)
2290 2379
2291 def _CheckDirReference(self, srcdir, destdir, share_refs): 2380 def _CheckDirReference(self, srcdir, destdir, share_refs):
2292 symlink_files = self.shareable_files 2381 symlink_files = self.shareable_files[:]
2293 symlink_dirs = self.shareable_dirs 2382 symlink_dirs = self.shareable_dirs[:]
2294 if share_refs: 2383 if share_refs:
2295 symlink_files += self.working_tree_files 2384 symlink_files += self.working_tree_files
2296 symlink_dirs += self.working_tree_dirs 2385 symlink_dirs += self.working_tree_dirs
@@ -2318,8 +2407,8 @@ class Project(object):
2318 copy_all: If true, copy all remaining files from |gitdir| -> |dotgit|. 2407 copy_all: If true, copy all remaining files from |gitdir| -> |dotgit|.
2319 This saves you the effort of initializing |dotgit| yourself. 2408 This saves you the effort of initializing |dotgit| yourself.
2320 """ 2409 """
2321 symlink_files = self.shareable_files 2410 symlink_files = self.shareable_files[:]
2322 symlink_dirs = self.shareable_dirs 2411 symlink_dirs = self.shareable_dirs[:]
2323 if share_refs: 2412 if share_refs:
2324 symlink_files += self.working_tree_files 2413 symlink_files += self.working_tree_files
2325 symlink_dirs += self.working_tree_dirs 2414 symlink_dirs += self.working_tree_dirs
@@ -2411,7 +2500,7 @@ class Project(object):
2411 def _allrefs(self): 2500 def _allrefs(self):
2412 return self.bare_ref.all 2501 return self.bare_ref.all
2413 2502
2414 def _getLogs(self, rev1, rev2, oneline=False, color=True): 2503 def _getLogs(self, rev1, rev2, oneline=False, color=True, pretty_format=None):
2415 """Get logs between two revisions of this project.""" 2504 """Get logs between two revisions of this project."""
2416 comp = '..' 2505 comp = '..'
2417 if rev1: 2506 if rev1:
@@ -2422,6 +2511,8 @@ class Project(object):
2422 out = DiffColoring(self.config) 2511 out = DiffColoring(self.config)
2423 if out.is_on and color: 2512 if out.is_on and color:
2424 cmd.append('--color') 2513 cmd.append('--color')
2514 if pretty_format is not None:
2515 cmd.append('--pretty=format:%s' % pretty_format)
2425 if oneline: 2516 if oneline:
2426 cmd.append('--oneline') 2517 cmd.append('--oneline')
2427 2518
@@ -2438,17 +2529,21 @@ class Project(object):
2438 raise 2529 raise
2439 return None 2530 return None
2440 2531
2441 def getAddedAndRemovedLogs(self, toProject, oneline=False, color=True): 2532 def getAddedAndRemovedLogs(self, toProject, oneline=False, color=True,
2533 pretty_format=None):
2442 """Get the list of logs from this revision to given revisionId""" 2534 """Get the list of logs from this revision to given revisionId"""
2443 logs = {} 2535 logs = {}
2444 selfId = self.GetRevisionId(self._allrefs) 2536 selfId = self.GetRevisionId(self._allrefs)
2445 toId = toProject.GetRevisionId(toProject._allrefs) 2537 toId = toProject.GetRevisionId(toProject._allrefs)
2446 2538
2447 logs['added'] = self._getLogs(selfId, toId, oneline=oneline, color=color) 2539 logs['added'] = self._getLogs(selfId, toId, oneline=oneline, color=color,
2448 logs['removed'] = self._getLogs(toId, selfId, oneline=oneline, color=color) 2540 pretty_format=pretty_format)
2541 logs['removed'] = self._getLogs(toId, selfId, oneline=oneline, color=color,
2542 pretty_format=pretty_format)
2449 return logs 2543 return logs
2450 2544
2451 class _GitGetByExec(object): 2545 class _GitGetByExec(object):
2546
2452 def __init__(self, project, bare, gitdir): 2547 def __init__(self, project, bare, gitdir):
2453 self._project = project 2548 self._project = project
2454 self._bare = bare 2549 self._bare = bare
@@ -2467,8 +2562,8 @@ class Project(object):
2467 if p.Wait() == 0: 2562 if p.Wait() == 0:
2468 out = p.stdout 2563 out = p.stdout
2469 if out: 2564 if out:
2565 # Backslash is not anomalous
2470 return out[:-1].split('\0') # pylint: disable=W1401 2566 return out[:-1].split('\0') # pylint: disable=W1401
2471 # Backslash is not anomalous
2472 return [] 2567 return []
2473 2568
2474 def DiffZ(self, name, *args): 2569 def DiffZ(self, name, *args):
@@ -2494,6 +2589,7 @@ class Project(object):
2494 break 2589 break
2495 2590
2496 class _Info(object): 2591 class _Info(object):
2592
2497 def __init__(self, path, omode, nmode, oid, nid, state): 2593 def __init__(self, path, omode, nmode, oid, nid, state):
2498 self.path = path 2594 self.path = path
2499 self.src_path = None 2595 self.src_path = None
@@ -2596,10 +2692,8 @@ class Project(object):
2596 line = line[:-1] 2692 line = line[:-1]
2597 r.append(line) 2693 r.append(line)
2598 if p.Wait() != 0: 2694 if p.Wait() != 0:
2599 raise GitError('%s rev-list %s: %s' % ( 2695 raise GitError('%s rev-list %s: %s' %
2600 self._project.name, 2696 (self._project.name, str(args), p.stderr))
2601 str(args),
2602 p.stderr))
2603 return r 2697 return r
2604 2698
2605 def __getattr__(self, name): 2699 def __getattr__(self, name):
@@ -2622,6 +2716,7 @@ class Project(object):
2622 A callable object that will try to call git with the named command. 2716 A callable object that will try to call git with the named command.
2623 """ 2717 """
2624 name = name.replace('_', '-') 2718 name = name.replace('_', '-')
2719
2625 def runner(*args, **kwargs): 2720 def runner(*args, **kwargs):
2626 cmdv = [] 2721 cmdv = []
2627 config = kwargs.pop('config', None) 2722 config = kwargs.pop('config', None)
@@ -2644,10 +2739,8 @@ class Project(object):
2644 capture_stdout=True, 2739 capture_stdout=True,
2645 capture_stderr=True) 2740 capture_stderr=True)
2646 if p.Wait() != 0: 2741 if p.Wait() != 0:
2647 raise GitError('%s %s: %s' % ( 2742 raise GitError('%s %s: %s' %
2648 self._project.name, 2743 (self._project.name, name, p.stderr))
2649 name,
2650 p.stderr))
2651 r = p.stdout 2744 r = p.stdout
2652 try: 2745 try:
2653 r = r.decode('utf-8') 2746 r = r.decode('utf-8')
@@ -2660,14 +2753,19 @@ class Project(object):
2660 2753
2661 2754
2662class _PriorSyncFailedError(Exception): 2755class _PriorSyncFailedError(Exception):
2756
2663 def __str__(self): 2757 def __str__(self):
2664 return 'prior sync failed; rebase still in progress' 2758 return 'prior sync failed; rebase still in progress'
2665 2759
2760
2666class _DirtyError(Exception): 2761class _DirtyError(Exception):
2762
2667 def __str__(self): 2763 def __str__(self):
2668 return 'contains uncommitted changes' 2764 return 'contains uncommitted changes'
2669 2765
2766
2670class _InfoMessage(object): 2767class _InfoMessage(object):
2768
2671 def __init__(self, project, text): 2769 def __init__(self, project, text):
2672 self.project = project 2770 self.project = project
2673 self.text = text 2771 self.text = text
@@ -2676,7 +2774,9 @@ class _InfoMessage(object):
2676 syncbuf.out.info('%s/: %s', self.project.relpath, self.text) 2774 syncbuf.out.info('%s/: %s', self.project.relpath, self.text)
2677 syncbuf.out.nl() 2775 syncbuf.out.nl()
2678 2776
2777
2679class _Failure(object): 2778class _Failure(object):
2779
2680 def __init__(self, project, why): 2780 def __init__(self, project, why):
2681 self.project = project 2781 self.project = project
2682 self.why = why 2782 self.why = why
@@ -2687,7 +2787,9 @@ class _Failure(object):
2687 str(self.why)) 2787 str(self.why))
2688 syncbuf.out.nl() 2788 syncbuf.out.nl()
2689 2789
2790
2690class _Later(object): 2791class _Later(object):
2792
2691 def __init__(self, project, action): 2793 def __init__(self, project, action):
2692 self.project = project 2794 self.project = project
2693 self.action = action 2795 self.action = action
@@ -2704,14 +2806,18 @@ class _Later(object):
2704 out.nl() 2806 out.nl()
2705 return False 2807 return False
2706 2808
2809
2707class _SyncColoring(Coloring): 2810class _SyncColoring(Coloring):
2811
2708 def __init__(self, config): 2812 def __init__(self, config):
2709 Coloring.__init__(self, config, 'reposync') 2813 Coloring.__init__(self, config, 'reposync')
2710 self.project = self.printer('header', attr='bold') 2814 self.project = self.printer('header', attr='bold')
2711 self.info = self.printer('info') 2815 self.info = self.printer('info')
2712 self.fail = self.printer('fail', fg='red') 2816 self.fail = self.printer('fail', fg='red')
2713 2817
2818
2714class SyncBuffer(object): 2819class SyncBuffer(object):
2820
2715 def __init__(self, config, detach_head=False): 2821 def __init__(self, config, detach_head=False):
2716 self._messages = [] 2822 self._messages = []
2717 self._failures = [] 2823 self._failures = []
@@ -2767,8 +2873,10 @@ class SyncBuffer(object):
2767 2873
2768 2874
2769class MetaProject(Project): 2875class MetaProject(Project):
2876
2770 """A special project housed under .repo. 2877 """A special project housed under .repo.
2771 """ 2878 """
2879
2772 def __init__(self, manifest, name, gitdir, worktree): 2880 def __init__(self, manifest, name, gitdir, worktree):
2773 Project.__init__(self, 2881 Project.__init__(self,
2774 manifest=manifest, 2882 manifest=manifest,
@@ -2802,10 +2910,9 @@ class MetaProject(Project):
2802 syncbuf.Finish() 2910 syncbuf.Finish()
2803 2911
2804 return GitCommand(self, 2912 return GitCommand(self,
2805 ['update-ref', '-d', 'refs/heads/default'], 2913 ['update-ref', '-d', 'refs/heads/default'],
2806 capture_stdout=True, 2914 capture_stdout=True,
2807 capture_stderr=True).Wait() == 0 2915 capture_stderr=True).Wait() == 0
2808
2809 2916
2810 @property 2917 @property
2811 def LastFetch(self): 2918 def LastFetch(self):
diff --git a/repo b/repo
index 47211742..f9eb9e8a 100755
--- a/repo
+++ b/repo
@@ -23,7 +23,7 @@ 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)
@@ -196,6 +196,9 @@ group.add_option('-p', '--platform',
196 help='restrict manifest projects to ones with a specified ' 196 help='restrict manifest projects to ones with a specified '
197 'platform group [auto|all|none|linux|darwin|...]', 197 'platform group [auto|all|none|linux|darwin|...]',
198 metavar='PLATFORM') 198 metavar='PLATFORM')
199group.add_option('--no-clone-bundle',
200 dest='no_clone_bundle', action='store_true',
201 help='disable use of /clone.bundle on HTTP/HTTPS')
199 202
200 203
201# Tool 204# Tool
@@ -339,7 +342,7 @@ def _Init(args, gitc_init=False):
339 can_verify = True 342 can_verify = True
340 343
341 dst = os.path.abspath(os.path.join(repodir, S_repo)) 344 dst = os.path.abspath(os.path.join(repodir, S_repo))
342 _Clone(url, dst, opt.quiet) 345 _Clone(url, dst, opt.quiet, not opt.no_clone_bundle)
343 346
344 if can_verify and not opt.no_repo_verify: 347 if can_verify and not opt.no_repo_verify:
345 rev = _Verify(dst, branch, opt.quiet) 348 rev = _Verify(dst, branch, opt.quiet)
@@ -432,7 +435,10 @@ def SetupGnuPG(quiet):
432 sys.exit(1) 435 sys.exit(1)
433 436
434 env = os.environ.copy() 437 env = os.environ.copy()
435 env['GNUPGHOME'] = gpg_dir.encode() 438 try:
439 env['GNUPGHOME'] = gpg_dir
440 except UnicodeEncodeError:
441 env['GNUPGHOME'] = gpg_dir.encode()
436 442
437 cmd = ['gpg', '--import'] 443 cmd = ['gpg', '--import']
438 try: 444 try:
@@ -543,7 +549,7 @@ def _DownloadBundle(url, local, quiet):
543 try: 549 try:
544 r = urllib.request.urlopen(url) 550 r = urllib.request.urlopen(url)
545 except urllib.error.HTTPError as e: 551 except urllib.error.HTTPError as e:
546 if e.code in [401, 403, 404]: 552 if e.code in [401, 403, 404, 501]:
547 return False 553 return False
548 _print('fatal: Cannot get %s' % url, file=sys.stderr) 554 _print('fatal: Cannot get %s' % url, file=sys.stderr)
549 _print('fatal: HTTP error %s' % e.code, file=sys.stderr) 555 _print('fatal: HTTP error %s' % e.code, file=sys.stderr)
@@ -574,7 +580,7 @@ def _ImportBundle(local):
574 os.remove(path) 580 os.remove(path)
575 581
576 582
577def _Clone(url, local, quiet): 583def _Clone(url, local, quiet, clone_bundle):
578 """Clones a git repository to a new subdirectory of repodir 584 """Clones a git repository to a new subdirectory of repodir
579 """ 585 """
580 try: 586 try:
@@ -604,7 +610,7 @@ def _Clone(url, local, quiet):
604 _SetConfig(local, 610 _SetConfig(local,
605 'remote.origin.fetch', 611 'remote.origin.fetch',
606 '+refs/heads/*:refs/remotes/origin/*') 612 '+refs/heads/*:refs/remotes/origin/*')
607 if _DownloadBundle(url, local, quiet): 613 if clone_bundle and _DownloadBundle(url, local, quiet):
608 _ImportBundle(local) 614 _ImportBundle(local)
609 _Fetch(url, local, 'origin', quiet) 615 _Fetch(url, local, 'origin', quiet)
610 616
@@ -638,7 +644,10 @@ def _Verify(cwd, branch, quiet):
638 _print(file=sys.stderr) 644 _print(file=sys.stderr)
639 645
640 env = os.environ.copy() 646 env = os.environ.copy()
641 env['GNUPGHOME'] = gpg_dir.encode() 647 try:
648 env['GNUPGHOME'] = gpg_dir
649 except UnicodeEncodeError:
650 env['GNUPGHOME'] = gpg_dir.encode()
642 651
643 cmd = [GIT, 'tag', '-v', cur] 652 cmd = [GIT, 'tag', '-v', cur]
644 proc = subprocess.Popen(cmd, 653 proc = subprocess.Popen(cmd,
diff --git a/subcmds/diffmanifests.py b/subcmds/diffmanifests.py
index 05998681..751a2026 100644
--- a/subcmds/diffmanifests.py
+++ b/subcmds/diffmanifests.py
@@ -71,6 +71,10 @@ synced and their revisions won't be found.
71 p.add_option('--no-color', 71 p.add_option('--no-color',
72 dest='color', action='store_false', default=True, 72 dest='color', action='store_false', default=True,
73 help='does not display the diff in color.') 73 help='does not display the diff in color.')
74 p.add_option('--pretty-format',
75 dest='pretty_format', action='store',
76 metavar='<FORMAT>',
77 help='print the log using a custom git pretty format string')
74 78
75 def _printRawDiff(self, diff): 79 def _printRawDiff(self, diff):
76 for project in diff['added']: 80 for project in diff['added']:
@@ -92,7 +96,7 @@ synced and their revisions won't be found.
92 otherProject.revisionExpr)) 96 otherProject.revisionExpr))
93 self.out.nl() 97 self.out.nl()
94 98
95 def _printDiff(self, diff, color=True): 99 def _printDiff(self, diff, color=True, pretty_format=None):
96 if diff['added']: 100 if diff['added']:
97 self.out.nl() 101 self.out.nl()
98 self.printText('added projects : \n') 102 self.printText('added projects : \n')
@@ -124,7 +128,8 @@ synced and their revisions won't be found.
124 self.printText(' to ') 128 self.printText(' to ')
125 self.printRevision(otherProject.revisionExpr) 129 self.printRevision(otherProject.revisionExpr)
126 self.out.nl() 130 self.out.nl()
127 self._printLogs(project, otherProject, raw=False, color=color) 131 self._printLogs(project, otherProject, raw=False, color=color,
132 pretty_format=pretty_format)
128 self.out.nl() 133 self.out.nl()
129 134
130 if diff['unreachable']: 135 if diff['unreachable']:
@@ -139,9 +144,13 @@ synced and their revisions won't be found.
139 self.printText(' not found') 144 self.printText(' not found')
140 self.out.nl() 145 self.out.nl()
141 146
142 def _printLogs(self, project, otherProject, raw=False, color=True): 147 def _printLogs(self, project, otherProject, raw=False, color=True,
143 logs = project.getAddedAndRemovedLogs(otherProject, oneline=True, 148 pretty_format=None):
144 color=color) 149
150 logs = project.getAddedAndRemovedLogs(otherProject,
151 oneline=(pretty_format is None),
152 color=color,
153 pretty_format=pretty_format)
145 if logs['removed']: 154 if logs['removed']:
146 removedLogs = logs['removed'].split('\n') 155 removedLogs = logs['removed'].split('\n')
147 for log in removedLogs: 156 for log in removedLogs:
@@ -192,4 +201,4 @@ synced and their revisions won't be found.
192 if opt.raw: 201 if opt.raw:
193 self._printRawDiff(diff) 202 self._printRawDiff(diff)
194 else: 203 else:
195 self._printDiff(diff, color=opt.color) 204 self._printDiff(diff, color=opt.color, pretty_format=opt.pretty_format)
diff --git a/subcmds/forall.py b/subcmds/forall.py
index b10f34b3..07ee8d58 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -120,6 +120,9 @@ without iterating through the remaining projects.
120 p.add_option('-r', '--regex', 120 p.add_option('-r', '--regex',
121 dest='regex', action='store_true', 121 dest='regex', action='store_true',
122 help="Execute the command only on projects matching regex or wildcard expression") 122 help="Execute the command only on projects matching regex or wildcard expression")
123 p.add_option('-i', '--inverse-regex',
124 dest='inverse_regex', action='store_true',
125 help="Execute the command only on projects not matching regex or wildcard expression")
123 p.add_option('-g', '--groups', 126 p.add_option('-g', '--groups',
124 dest='groups', 127 dest='groups',
125 help="Execute the command only on projects matching the specified groups") 128 help="Execute the command only on projects matching the specified groups")
@@ -215,10 +218,12 @@ without iterating through the remaining projects.
215 if os.path.isfile(smart_sync_manifest_path): 218 if os.path.isfile(smart_sync_manifest_path):
216 self.manifest.Override(smart_sync_manifest_path) 219 self.manifest.Override(smart_sync_manifest_path)
217 220
218 if not opt.regex: 221 if opt.regex:
219 projects = self.GetProjects(args, groups=opt.groups)
220 else:
221 projects = self.FindProjects(args) 222 projects = self.FindProjects(args)
223 elif opt.inverse_regex:
224 projects = self.FindProjects(args, inverse=True)
225 else:
226 projects = self.GetProjects(args, groups=opt.groups)
222 227
223 os.environ['REPO_COUNT'] = str(len(projects)) 228 os.environ['REPO_COUNT'] = str(len(projects))
224 229
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/sync.py b/subcmds/sync.py
index 4af411c9..9124a653 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -242,7 +242,7 @@ later is required to fix a server side protocol bug.
242 if show_smart: 242 if show_smart:
243 p.add_option('-s', '--smart-sync', 243 p.add_option('-s', '--smart-sync',
244 dest='smart_sync', action='store_true', 244 dest='smart_sync', action='store_true',
245 help='smart sync using manifest from a known good build') 245 help='smart sync using manifest from the latest known good build')
246 p.add_option('-t', '--smart-tag', 246 p.add_option('-t', '--smart-tag',
247 dest='smart_tag', action='store', 247 dest='smart_tag', action='store',
248 help='smart sync using manifest from a known tag') 248 help='smart sync using manifest from a known tag')
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)