summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/manifest-format.txt20
-rw-r--r--git_command.py4
-rw-r--r--git_config.py62
-rwxr-xr-xhooks/commit-msg10
-rw-r--r--manifest_xml.py32
-rw-r--r--project.py46
-rwxr-xr-xrepo8
-rw-r--r--subcmds/forall.py290
-rw-r--r--subcmds/status.py6
-rw-r--r--subcmds/sync.py47
-rw-r--r--subcmds/upload.py6
11 files changed, 313 insertions, 218 deletions
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt
index e48b75fe..28a21bb7 100644
--- a/docs/manifest-format.txt
+++ b/docs/manifest-format.txt
@@ -35,6 +35,7 @@ following DTD:
35 <!ATTLIST remote alias CDATA #IMPLIED> 35 <!ATTLIST remote alias CDATA #IMPLIED>
36 <!ATTLIST remote fetch CDATA #REQUIRED> 36 <!ATTLIST remote fetch CDATA #REQUIRED>
37 <!ATTLIST remote review CDATA #IMPLIED> 37 <!ATTLIST remote review CDATA #IMPLIED>
38 <!ATTLIST remote revision CDATA #IMPLIED>
38 39
39 <!ELEMENT default (EMPTY)> 40 <!ELEMENT default (EMPTY)>
40 <!ATTLIST default remote IDREF #IMPLIED> 41 <!ATTLIST default remote IDREF #IMPLIED>
@@ -112,6 +113,10 @@ Attribute `review`: Hostname of the Gerrit server where reviews
112are uploaded to by `repo upload`. This attribute is optional; 113are uploaded to by `repo upload`. This attribute is optional;
113if not specified then `repo upload` will not function. 114if not specified then `repo upload` will not function.
114 115
116Attribute `revision`: Name of a Git branch (e.g. `master` or
117`refs/heads/master`). Remotes with their own revision will override
118the default revision.
119
115Element default 120Element default
116--------------- 121---------------
117 122
@@ -132,14 +137,14 @@ Project elements not setting their own `dest-branch` will inherit
132this value. If this value is not set, projects will use `revision` 137this value. If this value is not set, projects will use `revision`
133by default instead. 138by default instead.
134 139
135Attribute `sync_j`: Number of parallel jobs to use when synching. 140Attribute `sync-j`: Number of parallel jobs to use when synching.
136 141
137Attribute `sync_c`: Set to true to only sync the given Git 142Attribute `sync-c`: Set to true to only sync the given Git
138branch (specified in the `revision` attribute) rather than the 143branch (specified in the `revision` attribute) rather than the
139whole ref space. Project elements lacking a sync_c element of 144whole ref space. Project elements lacking a sync-c element of
140their own will use this value. 145their own will use this value.
141 146
142Attribute `sync_s`: Set to true to also sync sub-projects. 147Attribute `sync-s`: Set to true to also sync sub-projects.
143 148
144 149
145Element manifest-server 150Element manifest-server
@@ -208,7 +213,8 @@ to track for this project. Names can be relative to refs/heads
208(e.g. just "master") or absolute (e.g. "refs/heads/master"). 213(e.g. just "master") or absolute (e.g. "refs/heads/master").
209Tags and/or explicit SHA-1s should work in theory, but have not 214Tags and/or explicit SHA-1s should work in theory, but have not
210been extensively tested. If not supplied the revision given by 215been extensively tested. If not supplied the revision given by
211the default element is used. 216the remote element is used if applicable, else the default
217element is used.
212 218
213Attribute `dest-branch`: Name of a Git branch (e.g. `master`). 219Attribute `dest-branch`: Name of a Git branch (e.g. `master`).
214When using `repo upload`, changes will be submitted for code 220When using `repo upload`, changes will be submitted for code
@@ -226,11 +232,11 @@ group "notdefault", it will not be automatically downloaded by repo.
226If the project has a parent element, the `name` and `path` here 232If the project has a parent element, the `name` and `path` here
227are the prefixed ones. 233are the prefixed ones.
228 234
229Attribute `sync_c`: Set to true to only sync the given Git 235Attribute `sync-c`: Set to true to only sync the given Git
230branch (specified in the `revision` attribute) rather than the 236branch (specified in the `revision` attribute) rather than the
231whole ref space. 237whole ref space.
232 238
233Attribute `sync_s`: Set to true to also sync sub-projects. 239Attribute `sync-s`: Set to true to also sync sub-projects.
234 240
235Attribute `upstream`: Name of the Git branch in which a sha1 241Attribute `upstream`: Name of the Git branch in which a sha1
236can be found. Used when syncing a revision locked manifest in 242can be found. Used when syncing a revision locked manifest in
diff --git a/git_command.py b/git_command.py
index 354fc715..53b3e75c 100644
--- a/git_command.py
+++ b/git_command.py
@@ -80,13 +80,13 @@ class _GitCall(object):
80 def version(self): 80 def version(self):
81 p = GitCommand(None, ['--version'], capture_stdout=True) 81 p = GitCommand(None, ['--version'], capture_stdout=True)
82 if p.Wait() == 0: 82 if p.Wait() == 0:
83 return p.stdout 83 return p.stdout.decode('utf-8')
84 return None 84 return None
85 85
86 def version_tuple(self): 86 def version_tuple(self):
87 global _git_version 87 global _git_version
88 if _git_version is None: 88 if _git_version is None:
89 ver_str = git.version().decode('utf-8') 89 ver_str = git.version()
90 _git_version = Wrapper().ParseGitVersion(ver_str) 90 _git_version = Wrapper().ParseGitVersion(ver_str)
91 if _git_version is None: 91 if _git_version is None:
92 print('fatal: "%s" unsupported' % ver_str, file=sys.stderr) 92 print('fatal: "%s" unsupported' % ver_str, file=sys.stderr)
diff --git a/git_config.py b/git_config.py
index 32879ec7..aa07d1b7 100644
--- a/git_config.py
+++ b/git_config.py
@@ -15,8 +15,8 @@
15 15
16from __future__ import print_function 16from __future__ import print_function
17 17
18import json
18import os 19import os
19import pickle
20import re 20import re
21import subprocess 21import subprocess
22import sys 22import sys
@@ -80,7 +80,7 @@ class GitConfig(object):
80 return cls(configfile = os.path.join(gitdir, 'config'), 80 return cls(configfile = os.path.join(gitdir, 'config'),
81 defaults = defaults) 81 defaults = defaults)
82 82
83 def __init__(self, configfile, defaults=None, pickleFile=None): 83 def __init__(self, configfile, defaults=None, jsonFile=None):
84 self.file = configfile 84 self.file = configfile
85 self.defaults = defaults 85 self.defaults = defaults
86 self._cache_dict = None 86 self._cache_dict = None
@@ -88,12 +88,11 @@ class GitConfig(object):
88 self._remotes = {} 88 self._remotes = {}
89 self._branches = {} 89 self._branches = {}
90 90
91 if pickleFile is None: 91 self._json = jsonFile
92 self._pickle = os.path.join( 92 if self._json is None:
93 self._json = os.path.join(
93 os.path.dirname(self.file), 94 os.path.dirname(self.file),
94 '.repopickle_' + os.path.basename(self.file)) 95 '.repo_' + os.path.basename(self.file) + '.json')
95 else:
96 self._pickle = pickleFile
97 96
98 def Has(self, name, include_defaults = True): 97 def Has(self, name, include_defaults = True):
99 """Return true if this configuration file has the key. 98 """Return true if this configuration file has the key.
@@ -217,9 +216,9 @@ class GitConfig(object):
217 """Resolve any url.*.insteadof references. 216 """Resolve any url.*.insteadof references.
218 """ 217 """
219 for new_url in self.GetSubSections('url'): 218 for new_url in self.GetSubSections('url'):
220 old_url = self.GetString('url.%s.insteadof' % new_url) 219 for old_url in self.GetString('url.%s.insteadof' % new_url, True):
221 if old_url is not None and url.startswith(old_url): 220 if old_url is not None and url.startswith(old_url):
222 return new_url + url[len(old_url):] 221 return new_url + url[len(old_url):]
223 return url 222 return url
224 223
225 @property 224 @property
@@ -248,50 +247,41 @@ class GitConfig(object):
248 return self._cache_dict 247 return self._cache_dict
249 248
250 def _Read(self): 249 def _Read(self):
251 d = self._ReadPickle() 250 d = self._ReadJson()
252 if d is None: 251 if d is None:
253 d = self._ReadGit() 252 d = self._ReadGit()
254 self._SavePickle(d) 253 self._SaveJson(d)
255 return d 254 return d
256 255
257 def _ReadPickle(self): 256 def _ReadJson(self):
258 try: 257 try:
259 if os.path.getmtime(self._pickle) \ 258 if os.path.getmtime(self._json) \
260 <= os.path.getmtime(self.file): 259 <= os.path.getmtime(self.file):
261 os.remove(self._pickle) 260 os.remove(self._json)
262 return None 261 return None
263 except OSError: 262 except OSError:
264 return None 263 return None
265 try: 264 try:
266 Trace(': unpickle %s', self.file) 265 Trace(': parsing %s', self.file)
267 fd = open(self._pickle, 'rb') 266 fd = open(self._json)
268 try: 267 try:
269 return pickle.load(fd) 268 return json.load(fd)
270 finally: 269 finally:
271 fd.close() 270 fd.close()
272 except EOFError: 271 except (IOError, ValueError):
273 os.remove(self._pickle) 272 os.remove(self._json)
274 return None
275 except IOError:
276 os.remove(self._pickle)
277 return None
278 except pickle.PickleError:
279 os.remove(self._pickle)
280 return None 273 return None
281 274
282 def _SavePickle(self, cache): 275 def _SaveJson(self, cache):
283 try: 276 try:
284 fd = open(self._pickle, 'wb') 277 fd = open(self._json, 'w')
285 try: 278 try:
286 pickle.dump(cache, fd, pickle.HIGHEST_PROTOCOL) 279 json.dump(cache, fd, indent=2)
287 finally: 280 finally:
288 fd.close() 281 fd.close()
289 except IOError: 282 except (IOError, TypeError):
290 if os.path.exists(self._pickle): 283 if os.path.exists(self.json):
291 os.remove(self._pickle) 284 os.remove(self._json)
292 except pickle.PickleError:
293 if os.path.exists(self._pickle):
294 os.remove(self._pickle)
295 285
296 def _ReadGit(self): 286 def _ReadGit(self):
297 """ 287 """
@@ -707,7 +697,7 @@ class Branch(object):
707 self._Set('merge', self.merge) 697 self._Set('merge', self.merge)
708 698
709 else: 699 else:
710 fd = open(self._config.file, 'ab') 700 fd = open(self._config.file, 'a')
711 try: 701 try:
712 fd.write('[branch "%s"]\n' % self.name) 702 fd.write('[branch "%s"]\n' % self.name)
713 if self.remote: 703 if self.remote:
diff --git a/hooks/commit-msg b/hooks/commit-msg
index 5ca2b112..d8f009b6 100755
--- a/hooks/commit-msg
+++ b/hooks/commit-msg
@@ -1,5 +1,4 @@
1#!/bin/sh 1#!/bin/sh
2# From Gerrit Code Review 2.6
3# 2#
4# Part of Gerrit Code Review (http://code.google.com/p/gerrit/) 3# Part of Gerrit Code Review (http://code.google.com/p/gerrit/)
5# 4#
@@ -27,7 +26,7 @@ MSG="$1"
27# 26#
28add_ChangeId() { 27add_ChangeId() {
29 clean_message=`sed -e ' 28 clean_message=`sed -e '
30 /^diff --git a\/.*/{ 29 /^diff --git .*/{
31 s/// 30 s///
32 q 31 q
33 } 32 }
@@ -39,6 +38,11 @@ add_ChangeId() {
39 return 38 return
40 fi 39 fi
41 40
41 if test "false" = "`git config --bool --get gerrit.createChangeId`"
42 then
43 return
44 fi
45
42 # Does Change-Id: already exist? if so, exit (no change). 46 # Does Change-Id: already exist? if so, exit (no change).
43 if grep -i '^Change-Id:' "$MSG" >/dev/null 47 if grep -i '^Change-Id:' "$MSG" >/dev/null
44 then 48 then
@@ -77,7 +81,7 @@ add_ChangeId() {
77 # Skip the line starting with the diff command and everything after it, 81 # Skip the line starting with the diff command and everything after it,
78 # up to the end of the file, assuming it is only patch data. 82 # up to the end of the file, assuming it is only patch data.
79 # If more than one line before the diff was empty, strip all but one. 83 # If more than one line before the diff was empty, strip all but one.
80 /^diff --git a/ { 84 /^diff --git / {
81 blankLines = 0 85 blankLines = 0
82 while (getline) { } 86 while (getline) { }
83 next 87 next
diff --git a/manifest_xml.py b/manifest_xml.py
index e2f58e62..3517c151 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -63,12 +63,14 @@ class _XmlRemote(object):
63 alias=None, 63 alias=None,
64 fetch=None, 64 fetch=None,
65 manifestUrl=None, 65 manifestUrl=None,
66 review=None): 66 review=None,
67 revision=None):
67 self.name = name 68 self.name = name
68 self.fetchUrl = fetch 69 self.fetchUrl = fetch
69 self.manifestUrl = manifestUrl 70 self.manifestUrl = manifestUrl
70 self.remoteAlias = alias 71 self.remoteAlias = alias
71 self.reviewUrl = review 72 self.reviewUrl = review
73 self.revision = revision
72 self.resolvedFetchUrl = self._resolveFetchUrl() 74 self.resolvedFetchUrl = self._resolveFetchUrl()
73 75
74 def __eq__(self, other): 76 def __eq__(self, other):
@@ -159,6 +161,8 @@ class XmlManifest(object):
159 e.setAttribute('alias', r.remoteAlias) 161 e.setAttribute('alias', r.remoteAlias)
160 if r.reviewUrl is not None: 162 if r.reviewUrl is not None:
161 e.setAttribute('review', r.reviewUrl) 163 e.setAttribute('review', r.reviewUrl)
164 if r.revision is not None:
165 e.setAttribute('revision', r.revision)
162 166
163 def Save(self, fd, peg_rev=False, peg_rev_upstream=True): 167 def Save(self, fd, peg_rev=False, peg_rev_upstream=True):
164 """Write the current manifest out to the given file descriptor. 168 """Write the current manifest out to the given file descriptor.
@@ -240,7 +244,8 @@ class XmlManifest(object):
240 if d.remote: 244 if d.remote:
241 remoteName = d.remote.remoteAlias or d.remote.name 245 remoteName = d.remote.remoteAlias or d.remote.name
242 if not d.remote or p.remote.name != remoteName: 246 if not d.remote or p.remote.name != remoteName:
243 e.setAttribute('remote', p.remote.name) 247 remoteName = p.remote.name
248 e.setAttribute('remote', remoteName)
244 if peg_rev: 249 if peg_rev:
245 if self.IsMirror: 250 if self.IsMirror:
246 value = p.bare_git.rev_parse(p.revisionExpr + '^0') 251 value = p.bare_git.rev_parse(p.revisionExpr + '^0')
@@ -252,8 +257,12 @@ class XmlManifest(object):
252 # isn't our value, and the if the default doesn't already have that 257 # isn't our value, and the if the default doesn't already have that
253 # covered. 258 # covered.
254 e.setAttribute('upstream', p.revisionExpr) 259 e.setAttribute('upstream', p.revisionExpr)
255 elif not d.revisionExpr or p.revisionExpr != d.revisionExpr: 260 else:
256 e.setAttribute('revision', p.revisionExpr) 261 revision = self.remotes[remoteName].revision or d.revisionExpr
262 if not revision or revision != p.revisionExpr:
263 e.setAttribute('revision', p.revisionExpr)
264 if p.upstream and p.upstream != p.revisionExpr:
265 e.setAttribute('upstream', p.upstream)
257 266
258 for c in p.copyfiles: 267 for c in p.copyfiles:
259 ce = doc.createElement('copyfile') 268 ce = doc.createElement('copyfile')
@@ -310,7 +319,7 @@ class XmlManifest(object):
310 @property 319 @property
311 def projects(self): 320 def projects(self):
312 self._Load() 321 self._Load()
313 return self._paths.values() 322 return list(self._paths.values())
314 323
315 @property 324 @property
316 def remotes(self): 325 def remotes(self):
@@ -592,8 +601,11 @@ class XmlManifest(object):
592 review = node.getAttribute('review') 601 review = node.getAttribute('review')
593 if review == '': 602 if review == '':
594 review = None 603 review = None
604 revision = node.getAttribute('revision')
605 if revision == '':
606 revision = None
595 manifestUrl = self.manifestProject.config.GetString('remote.origin.url') 607 manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
596 return _XmlRemote(name, alias, fetch, manifestUrl, review) 608 return _XmlRemote(name, alias, fetch, manifestUrl, review, revision)
597 609
598 def _ParseDefault(self, node): 610 def _ParseDefault(self, node):
599 """ 611 """
@@ -686,7 +698,7 @@ class XmlManifest(object):
686 raise ManifestParseError("no remote for project %s within %s" % 698 raise ManifestParseError("no remote for project %s within %s" %
687 (name, self.manifestFile)) 699 (name, self.manifestFile))
688 700
689 revisionExpr = node.getAttribute('revision') 701 revisionExpr = node.getAttribute('revision') or remote.revision
690 if not revisionExpr: 702 if not revisionExpr:
691 revisionExpr = self._default.revisionExpr 703 revisionExpr = self._default.revisionExpr
692 if not revisionExpr: 704 if not revisionExpr:
@@ -872,10 +884,8 @@ class XmlManifest(object):
872 fromProjects = self.paths 884 fromProjects = self.paths
873 toProjects = manifest.paths 885 toProjects = manifest.paths
874 886
875 fromKeys = fromProjects.keys() 887 fromKeys = sorted(fromProjects.keys())
876 fromKeys.sort() 888 toKeys = sorted(toProjects.keys())
877 toKeys = toProjects.keys()
878 toKeys.sort()
879 889
880 diff = {'added': [], 'removed': [], 'changed': [], 'unreachable': []} 890 diff = {'added': [], 'removed': [], 'changed': [], 'unreachable': []}
881 891
diff --git a/project.py b/project.py
index 127176e5..95403ccb 100644
--- a/project.py
+++ b/project.py
@@ -46,7 +46,7 @@ if not is_python3():
46def _lwrite(path, content): 46def _lwrite(path, content):
47 lock = '%s.lock' % path 47 lock = '%s.lock' % path
48 48
49 fd = open(lock, 'wb') 49 fd = open(lock, 'w')
50 try: 50 try:
51 fd.write(content) 51 fd.write(content)
52 finally: 52 finally:
@@ -259,10 +259,12 @@ class RemoteSpec(object):
259 def __init__(self, 259 def __init__(self,
260 name, 260 name,
261 url = None, 261 url = None,
262 review = None): 262 review = None,
263 revision = None):
263 self.name = name 264 self.name = name
264 self.url = url 265 self.url = url
265 self.review = review 266 self.review = review
267 self.revision = revision
266 268
267class RepoHook(object): 269class RepoHook(object):
268 """A RepoHook contains information about a script to run as a hook. 270 """A RepoHook contains information about a script to run as a hook.
@@ -438,7 +440,8 @@ class RepoHook(object):
438 # and convert to a HookError w/ just the failing traceback. 440 # and convert to a HookError w/ just the failing traceback.
439 context = {} 441 context = {}
440 try: 442 try:
441 execfile(self._script_fullpath, context) 443 exec(compile(open(self._script_fullpath).read(),
444 self._script_fullpath, 'exec'), context)
442 except Exception: 445 except Exception:
443 raise HookError('%s\nFailed to import %s hook; see traceback above.' % ( 446 raise HookError('%s\nFailed to import %s hook; see traceback above.' % (
444 traceback.format_exc(), self._hook_type)) 447 traceback.format_exc(), self._hook_type))
@@ -1657,7 +1660,8 @@ class Project(object):
1657 1660
1658 remote = RemoteSpec(self.remote.name, 1661 remote = RemoteSpec(self.remote.name,
1659 url = url, 1662 url = url,
1660 review = self.remote.review) 1663 review = self.remote.review,
1664 revision = self.remote.revision)
1661 subproject = Project(manifest = self.manifest, 1665 subproject = Project(manifest = self.manifest,
1662 name = name, 1666 name = name,
1663 remote = remote, 1667 remote = remote,
@@ -1702,6 +1706,7 @@ class Project(object):
1702 if command.Wait() != 0: 1706 if command.Wait() != 0:
1703 raise GitError('git archive %s: %s' % (self.name, command.stderr)) 1707 raise GitError('git archive %s: %s' % (self.name, command.stderr))
1704 1708
1709
1705 def _RemoteFetch(self, name=None, 1710 def _RemoteFetch(self, name=None,
1706 current_branch_only=False, 1711 current_branch_only=False,
1707 initial=False, 1712 initial=False,
@@ -1804,19 +1809,30 @@ class Project(object):
1804 else: 1809 else:
1805 cmd.append('--tags') 1810 cmd.append('--tags')
1806 1811
1812 spec = []
1807 if not current_branch_only: 1813 if not current_branch_only:
1808 # Fetch whole repo 1814 # Fetch whole repo
1809 cmd.append(str((u'+refs/heads/*:') + remote.ToLocal('refs/heads/*'))) 1815 spec.append(str((u'+refs/heads/*:') + remote.ToLocal('refs/heads/*')))
1810 elif tag_name is not None: 1816 elif tag_name is not None:
1811 cmd.append('tag') 1817 spec.append('tag')
1812 cmd.append(tag_name) 1818 spec.append(tag_name)
1813 else: 1819 else:
1814 branch = self.revisionExpr 1820 branch = self.revisionExpr
1815 if is_sha1: 1821 if is_sha1:
1816 branch = self.upstream 1822 branch = self.upstream
1817 if branch.startswith(R_HEADS): 1823 if branch.startswith(R_HEADS):
1818 branch = branch[len(R_HEADS):] 1824 branch = branch[len(R_HEADS):]
1819 cmd.append(str((u'+refs/heads/%s:' % branch) + remote.ToLocal('refs/heads/%s' % branch))) 1825 spec.append(str((u'+refs/heads/%s:' % branch) + remote.ToLocal('refs/heads/%s' % branch)))
1826 cmd.extend(spec)
1827
1828 shallowfetch = self.config.GetString('repo.shallowfetch')
1829 if shallowfetch and shallowfetch != ' '.join(spec):
1830 GitCommand(self, ['fetch', '--unshallow', name] + shallowfetch.split(),
1831 bare=True, ssh_proxy=ssh_proxy).Wait()
1832 if depth:
1833 self.config.SetString('repo.shallowfetch', ' '.join(spec))
1834 else:
1835 self.config.SetString('repo.shallowfetch', None)
1820 1836
1821 ok = False 1837 ok = False
1822 for _i in range(2): 1838 for _i in range(2):
@@ -2201,6 +2217,14 @@ class Project(object):
2201 if name in symlink_dirs and not os.path.lexists(src): 2217 if name in symlink_dirs and not os.path.lexists(src):
2202 os.makedirs(src) 2218 os.makedirs(src)
2203 2219
2220 # If the source file doesn't exist, ensure the destination
2221 # file doesn't either.
2222 if name in symlink_files and not os.path.lexists(src):
2223 try:
2224 os.remove(dst)
2225 except OSError:
2226 pass
2227
2204 if name in to_symlink: 2228 if name in to_symlink:
2205 os.symlink(os.path.relpath(src, os.path.dirname(dst)), dst) 2229 os.symlink(os.path.relpath(src, os.path.dirname(dst)), dst)
2206 elif copy_all and not os.path.islink(dst): 2230 elif copy_all and not os.path.islink(dst):
@@ -2321,8 +2345,8 @@ class Project(object):
2321 out = iter(out[:-1].split('\0')) # pylint: disable=W1401 2345 out = iter(out[:-1].split('\0')) # pylint: disable=W1401
2322 while out: 2346 while out:
2323 try: 2347 try:
2324 info = out.next() 2348 info = next(out)
2325 path = out.next() 2349 path = next(out)
2326 except StopIteration: 2350 except StopIteration:
2327 break 2351 break
2328 2352
@@ -2348,7 +2372,7 @@ class Project(object):
2348 info = _Info(path, *info) 2372 info = _Info(path, *info)
2349 if info.status in ('R', 'C'): 2373 if info.status in ('R', 'C'):
2350 info.src_path = info.path 2374 info.src_path = info.path
2351 info.path = out.next() 2375 info.path = next(out)
2352 r[info.path] = info 2376 r[info.path] = info
2353 return r 2377 return r
2354 finally: 2378 finally:
diff --git a/repo b/repo
index b8c414be..3fd0166e 100755
--- a/repo
+++ b/repo
@@ -139,10 +139,6 @@ def _print(*objects, **kwargs):
139 139
140# Python version check 140# Python version check
141ver = sys.version_info 141ver = sys.version_info
142if ver[0] == 3:
143 _print('warning: Python 3 support is currently experimental. YMMV.\n'
144 'Please use Python 2.6 - 2.7 instead.',
145 file=sys.stderr)
146if (ver[0], ver[1]) < MIN_PYTHON_VERSION: 142if (ver[0], ver[1]) < MIN_PYTHON_VERSION:
147 _print('error: Python version %s unsupported.\n' 143 _print('error: Python version %s unsupported.\n'
148 'Please use Python 2.6 - 2.7 instead.' 144 'Please use Python 2.6 - 2.7 instead.'
@@ -768,4 +764,8 @@ def main(orig_args):
768 764
769 765
770if __name__ == '__main__': 766if __name__ == '__main__':
767 if ver[0] == 3:
768 _print('warning: Python 3 support is currently experimental. YMMV.\n'
769 'Please use Python 2.6 - 2.7 instead.',
770 file=sys.stderr)
771 main(sys.argv[1:]) 771 main(sys.argv[1:])
diff --git a/subcmds/forall.py b/subcmds/forall.py
index 03ebcb21..7771ec16 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -14,7 +14,9 @@
14# limitations under the License. 14# limitations under the License.
15 15
16from __future__ import print_function 16from __future__ import print_function
17import errno
17import fcntl 18import fcntl
19import multiprocessing
18import re 20import re
19import os 21import os
20import select 22import select
@@ -31,6 +33,7 @@ _CAN_COLOR = [
31 'log', 33 'log',
32] 34]
33 35
36
34class ForallColoring(Coloring): 37class ForallColoring(Coloring):
35 def __init__(self, config): 38 def __init__(self, config):
36 Coloring.__init__(self, config, 'forall') 39 Coloring.__init__(self, config, 'forall')
@@ -132,9 +135,31 @@ without iterating through the remaining projects.
132 g.add_option('-v', '--verbose', 135 g.add_option('-v', '--verbose',
133 dest='verbose', action='store_true', 136 dest='verbose', action='store_true',
134 help='Show command error messages') 137 help='Show command error messages')
138 g.add_option('-j', '--jobs',
139 dest='jobs', action='store', type='int', default=1,
140 help='number of commands to execute simultaneously')
135 141
136 def WantPager(self, opt): 142 def WantPager(self, opt):
137 return opt.project_header 143 return opt.project_header and opt.jobs == 1
144
145 def _SerializeProject(self, project):
146 """ Serialize a project._GitGetByExec instance.
147
148 project._GitGetByExec is not pickle-able. Instead of trying to pass it
149 around between processes, make a dict ourselves containing only the
150 attributes that we need.
151
152 """
153 return {
154 'name': project.name,
155 'relpath': project.relpath,
156 'remote_name': project.remote.name,
157 'lrev': project.GetRevisionId(),
158 'rrev': project.revisionExpr,
159 'annotations': dict((a.name, a.value) for a in project.annotations),
160 'gitdir': project.gitdir,
161 'worktree': project.worktree,
162 }
138 163
139 def Execute(self, opt, args): 164 def Execute(self, opt, args):
140 if not opt.command: 165 if not opt.command:
@@ -173,11 +198,7 @@ without iterating through the remaining projects.
173 # pylint: enable=W0631 198 # pylint: enable=W0631
174 199
175 mirror = self.manifest.IsMirror 200 mirror = self.manifest.IsMirror
176 out = ForallColoring(self.manifest.manifestProject.config)
177 out.redirect(sys.stdout)
178
179 rc = 0 201 rc = 0
180 first = True
181 202
182 if not opt.regex: 203 if not opt.regex:
183 projects = self.GetProjects(args) 204 projects = self.GetProjects(args)
@@ -186,113 +207,156 @@ without iterating through the remaining projects.
186 207
187 os.environ['REPO_COUNT'] = str(len(projects)) 208 os.environ['REPO_COUNT'] = str(len(projects))
188 209
189 for (cnt, project) in enumerate(projects): 210 pool = multiprocessing.Pool(opt.jobs)
190 env = os.environ.copy() 211 try:
191 def setenv(name, val): 212 config = self.manifest.manifestProject.config
192 if val is None: 213 results_it = pool.imap(
193 val = '' 214 DoWorkWrapper,
194 env[name] = val.encode() 215 [[mirror, opt, cmd, shell, cnt, config, self._SerializeProject(p)]
195 216 for cnt, p in enumerate(projects)]
196 setenv('REPO_PROJECT', project.name) 217 )
197 setenv('REPO_PATH', project.relpath) 218 pool.close()
198 setenv('REPO_REMOTE', project.remote.name) 219 for r in results_it:
199 setenv('REPO_LREV', project.GetRevisionId()) 220 rc = rc or r
200 setenv('REPO_RREV', project.revisionExpr) 221 if r != 0 and opt.abort_on_errors:
201 setenv('REPO_I', str(cnt + 1)) 222 raise Exception('Aborting due to previous error')
202 for a in project.annotations: 223 except (KeyboardInterrupt, WorkerKeyboardInterrupt):
203 setenv("REPO__%s" % (a.name), a.value) 224 # Catch KeyboardInterrupt raised inside and outside of workers
204 225 print('Interrupted - terminating the pool')
205 if mirror: 226 pool.terminate()
206 setenv('GIT_DIR', project.gitdir) 227 rc = rc or errno.EINTR
207 cwd = project.gitdir 228 except Exception as e:
208 else: 229 # Catch any other exceptions raised
209 cwd = project.worktree 230 print('Got an error, terminating the pool: %r' % e,
210 231 file=sys.stderr)
211 if not os.path.exists(cwd): 232 pool.terminate()
212 if (opt.project_header and opt.verbose) \ 233 rc = rc or getattr(e, 'errno', 1)
213 or not opt.project_header: 234 finally:
214 print('skipping %s/' % project.relpath, file=sys.stderr) 235 pool.join()
215 continue
216
217 if opt.project_header:
218 stdin = subprocess.PIPE
219 stdout = subprocess.PIPE
220 stderr = subprocess.PIPE
221 else:
222 stdin = None
223 stdout = None
224 stderr = None
225
226 p = subprocess.Popen(cmd,
227 cwd = cwd,
228 shell = shell,
229 env = env,
230 stdin = stdin,
231 stdout = stdout,
232 stderr = stderr)
233
234 if opt.project_header:
235 class sfd(object):
236 def __init__(self, fd, dest):
237 self.fd = fd
238 self.dest = dest
239 def fileno(self):
240 return self.fd.fileno()
241
242 empty = True
243 errbuf = ''
244
245 p.stdin.close()
246 s_in = [sfd(p.stdout, sys.stdout),
247 sfd(p.stderr, sys.stderr)]
248
249 for s in s_in:
250 flags = fcntl.fcntl(s.fd, fcntl.F_GETFL)
251 fcntl.fcntl(s.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
252
253 while s_in:
254 in_ready, _out_ready, _err_ready = select.select(s_in, [], [])
255 for s in in_ready:
256 buf = s.fd.read(4096)
257 if not buf:
258 s.fd.close()
259 s_in.remove(s)
260 continue
261
262 if not opt.verbose:
263 if s.fd != p.stdout:
264 errbuf += buf
265 continue
266
267 if empty:
268 if first:
269 first = False
270 else:
271 out.nl()
272
273 if mirror:
274 project_header_path = project.name
275 else:
276 project_header_path = project.relpath
277 out.project('project %s/', project_header_path)
278 out.nl()
279 out.flush()
280 if errbuf:
281 sys.stderr.write(errbuf)
282 sys.stderr.flush()
283 errbuf = ''
284 empty = False
285
286 s.dest.write(buf)
287 s.dest.flush()
288
289 r = p.wait()
290 if r != 0:
291 if r != rc:
292 rc = r
293 if opt.abort_on_errors:
294 print("error: %s: Aborting due to previous error" % project.relpath,
295 file=sys.stderr)
296 sys.exit(r)
297 if rc != 0: 236 if rc != 0:
298 sys.exit(rc) 237 sys.exit(rc)
238
239
240class WorkerKeyboardInterrupt(Exception):
241 """ Keyboard interrupt exception for worker processes. """
242 pass
243
244
245def DoWorkWrapper(args):
246 """ A wrapper around the DoWork() method.
247
248 Catch the KeyboardInterrupt exceptions here and re-raise them as a different,
249 ``Exception``-based exception to stop it flooding the console with stacktraces
250 and making the parent hang indefinitely.
251
252 """
253 project = args.pop()
254 try:
255 return DoWork(project, *args)
256 except KeyboardInterrupt:
257 print('%s: Worker interrupted' % project['name'])
258 raise WorkerKeyboardInterrupt()
259
260
261def DoWork(project, mirror, opt, cmd, shell, cnt, config):
262 env = os.environ.copy()
263 def setenv(name, val):
264 if val is None:
265 val = ''
266 env[name] = val.encode()
267
268 setenv('REPO_PROJECT', project['name'])
269 setenv('REPO_PATH', project['relpath'])
270 setenv('REPO_REMOTE', project['remote_name'])
271 setenv('REPO_LREV', project['lrev'])
272 setenv('REPO_RREV', project['rrev'])
273 setenv('REPO_I', str(cnt + 1))
274 for name in project['annotations']:
275 setenv("REPO__%s" % (name), project['annotations'][name])
276
277 if mirror:
278 setenv('GIT_DIR', project['gitdir'])
279 cwd = project['gitdir']
280 else:
281 cwd = project['worktree']
282
283 if not os.path.exists(cwd):
284 if (opt.project_header and opt.verbose) \
285 or not opt.project_header:
286 print('skipping %s/' % project['relpath'], file=sys.stderr)
287 return
288
289 if opt.project_header:
290 stdin = subprocess.PIPE
291 stdout = subprocess.PIPE
292 stderr = subprocess.PIPE
293 else:
294 stdin = None
295 stdout = None
296 stderr = None
297
298 p = subprocess.Popen(cmd,
299 cwd=cwd,
300 shell=shell,
301 env=env,
302 stdin=stdin,
303 stdout=stdout,
304 stderr=stderr)
305
306 if opt.project_header:
307 out = ForallColoring(config)
308 out.redirect(sys.stdout)
309 class sfd(object):
310 def __init__(self, fd, dest):
311 self.fd = fd
312 self.dest = dest
313 def fileno(self):
314 return self.fd.fileno()
315
316 empty = True
317 errbuf = ''
318
319 p.stdin.close()
320 s_in = [sfd(p.stdout, sys.stdout),
321 sfd(p.stderr, sys.stderr)]
322
323 for s in s_in:
324 flags = fcntl.fcntl(s.fd, fcntl.F_GETFL)
325 fcntl.fcntl(s.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
326
327 while s_in:
328 in_ready, _out_ready, _err_ready = select.select(s_in, [], [])
329 for s in in_ready:
330 buf = s.fd.read(4096)
331 if not buf:
332 s.fd.close()
333 s_in.remove(s)
334 continue
335
336 if not opt.verbose:
337 if s.fd != p.stdout:
338 errbuf += buf
339 continue
340
341 if empty and out:
342 if not cnt == 0:
343 out.nl()
344
345 if mirror:
346 project_header_path = project['name']
347 else:
348 project_header_path = project['relpath']
349 out.project('project %s/', project_header_path)
350 out.nl()
351 out.flush()
352 if errbuf:
353 sys.stderr.write(errbuf)
354 sys.stderr.flush()
355 errbuf = ''
356 empty = False
357
358 s.dest.write(buf)
359 s.dest.flush()
360
361 r = p.wait()
362 return r
diff --git a/subcmds/status.py b/subcmds/status.py
index 41c4429a..b42675e0 100644
--- a/subcmds/status.py
+++ b/subcmds/status.py
@@ -113,7 +113,7 @@ the following meanings:
113 try: 113 try:
114 state = project.PrintWorkTreeStatus(output) 114 state = project.PrintWorkTreeStatus(output)
115 if state == 'CLEAN': 115 if state == 'CLEAN':
116 clean_counter.next() 116 next(clean_counter)
117 finally: 117 finally:
118 sem.release() 118 sem.release()
119 119
@@ -141,7 +141,7 @@ the following meanings:
141 for project in all_projects: 141 for project in all_projects:
142 state = project.PrintWorkTreeStatus() 142 state = project.PrintWorkTreeStatus()
143 if state == 'CLEAN': 143 if state == 'CLEAN':
144 counter.next() 144 next(counter)
145 else: 145 else:
146 sem = _threading.Semaphore(opt.jobs) 146 sem = _threading.Semaphore(opt.jobs)
147 threads_and_output = [] 147 threads_and_output = []
@@ -164,7 +164,7 @@ the following meanings:
164 t.join() 164 t.join()
165 output.dump(sys.stdout) 165 output.dump(sys.stdout)
166 output.close() 166 output.close()
167 if len(all_projects) == counter.next(): 167 if len(all_projects) == next(counter):
168 print('nothing to commit (working directory clean)') 168 print('nothing to commit (working directory clean)')
169 169
170 if opt.orphans: 170 if opt.orphans:
diff --git a/subcmds/sync.py b/subcmds/sync.py
index a0a68960..6f77310f 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -14,10 +14,10 @@
14# limitations under the License. 14# limitations under the License.
15 15
16from __future__ import print_function 16from __future__ import print_function
17import json
17import netrc 18import netrc
18from optparse import SUPPRESS_HELP 19from optparse import SUPPRESS_HELP
19import os 20import os
20import pickle
21import re 21import re
22import shutil 22import shutil
23import socket 23import socket
@@ -760,7 +760,7 @@ class _FetchTimes(object):
760 _ALPHA = 0.5 760 _ALPHA = 0.5
761 761
762 def __init__(self, manifest): 762 def __init__(self, manifest):
763 self._path = os.path.join(manifest.repodir, '.repopickle_fetchtimes') 763 self._path = os.path.join(manifest.repodir, '.repo_fetchtimes.json')
764 self._times = None 764 self._times = None
765 self._seen = set() 765 self._seen = set()
766 766
@@ -779,22 +779,17 @@ class _FetchTimes(object):
779 def _Load(self): 779 def _Load(self):
780 if self._times is None: 780 if self._times is None:
781 try: 781 try:
782 f = open(self._path, 'rb') 782 f = open(self._path)
783 except IOError:
784 self._times = {}
785 return self._times
786 try:
787 try: 783 try:
788 self._times = pickle.load(f) 784 self._times = json.load(f)
789 except IOError: 785 finally:
790 try: 786 f.close()
791 os.remove(self._path) 787 except (IOError, ValueError):
792 except OSError: 788 try:
793 pass 789 os.remove(self._path)
794 self._times = {} 790 except OSError:
795 finally: 791 pass
796 f.close() 792 self._times = {}
797 return self._times
798 793
799 def Save(self): 794 def Save(self):
800 if self._times is None: 795 if self._times is None:
@@ -808,13 +803,13 @@ class _FetchTimes(object):
808 del self._times[name] 803 del self._times[name]
809 804
810 try: 805 try:
811 f = open(self._path, 'wb') 806 f = open(self._path, 'w')
812 try: 807 try:
813 pickle.dump(self._times, f) 808 json.dump(self._times, f, indent=2)
814 except (IOError, OSError, pickle.PickleError): 809 finally:
815 try: 810 f.close()
816 os.remove(self._path) 811 except (IOError, TypeError):
817 except OSError: 812 try:
818 pass 813 os.remove(self._path)
819 finally: 814 except OSError:
820 f.close() 815 pass
diff --git a/subcmds/upload.py b/subcmds/upload.py
index e2fa261e..0ee36df1 100644
--- a/subcmds/upload.py
+++ b/subcmds/upload.py
@@ -25,10 +25,12 @@ from git_command import GitCommand
25from project import RepoHook 25from project import RepoHook
26 26
27from pyversion import is_python3 27from pyversion import is_python3
28# pylint:disable=W0622
28if not is_python3(): 29if not is_python3():
29 # pylint:disable=W0622
30 input = raw_input 30 input = raw_input
31 # pylint:enable=W0622 31else:
32 unicode = str
33# pylint:enable=W0622
32 34
33UNUSUAL_COMMIT_THRESHOLD = 5 35UNUSUAL_COMMIT_THRESHOLD = 5
34 36