diff options
-rw-r--r-- | docs/manifest-format.txt | 20 | ||||
-rw-r--r-- | git_command.py | 4 | ||||
-rw-r--r-- | git_config.py | 62 | ||||
-rwxr-xr-x | hooks/commit-msg | 10 | ||||
-rw-r--r-- | manifest_xml.py | 32 | ||||
-rw-r--r-- | project.py | 46 | ||||
-rwxr-xr-x | repo | 8 | ||||
-rw-r--r-- | subcmds/forall.py | 290 | ||||
-rw-r--r-- | subcmds/status.py | 6 | ||||
-rw-r--r-- | subcmds/sync.py | 47 | ||||
-rw-r--r-- | subcmds/upload.py | 6 |
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 | |||
112 | are uploaded to by `repo upload`. This attribute is optional; | 113 | are uploaded to by `repo upload`. This attribute is optional; |
113 | if not specified then `repo upload` will not function. | 114 | if not specified then `repo upload` will not function. |
114 | 115 | ||
116 | Attribute `revision`: Name of a Git branch (e.g. `master` or | ||
117 | `refs/heads/master`). Remotes with their own revision will override | ||
118 | the default revision. | ||
119 | |||
115 | Element default | 120 | Element default |
116 | --------------- | 121 | --------------- |
117 | 122 | ||
@@ -132,14 +137,14 @@ Project elements not setting their own `dest-branch` will inherit | |||
132 | this value. If this value is not set, projects will use `revision` | 137 | this value. If this value is not set, projects will use `revision` |
133 | by default instead. | 138 | by default instead. |
134 | 139 | ||
135 | Attribute `sync_j`: Number of parallel jobs to use when synching. | 140 | Attribute `sync-j`: Number of parallel jobs to use when synching. |
136 | 141 | ||
137 | Attribute `sync_c`: Set to true to only sync the given Git | 142 | Attribute `sync-c`: Set to true to only sync the given Git |
138 | branch (specified in the `revision` attribute) rather than the | 143 | branch (specified in the `revision` attribute) rather than the |
139 | whole ref space. Project elements lacking a sync_c element of | 144 | whole ref space. Project elements lacking a sync-c element of |
140 | their own will use this value. | 145 | their own will use this value. |
141 | 146 | ||
142 | Attribute `sync_s`: Set to true to also sync sub-projects. | 147 | Attribute `sync-s`: Set to true to also sync sub-projects. |
143 | 148 | ||
144 | 149 | ||
145 | Element manifest-server | 150 | Element 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"). |
209 | Tags and/or explicit SHA-1s should work in theory, but have not | 214 | Tags and/or explicit SHA-1s should work in theory, but have not |
210 | been extensively tested. If not supplied the revision given by | 215 | been extensively tested. If not supplied the revision given by |
211 | the default element is used. | 216 | the remote element is used if applicable, else the default |
217 | element is used. | ||
212 | 218 | ||
213 | Attribute `dest-branch`: Name of a Git branch (e.g. `master`). | 219 | Attribute `dest-branch`: Name of a Git branch (e.g. `master`). |
214 | When using `repo upload`, changes will be submitted for code | 220 | When using `repo upload`, changes will be submitted for code |
@@ -226,11 +232,11 @@ group "notdefault", it will not be automatically downloaded by repo. | |||
226 | If the project has a parent element, the `name` and `path` here | 232 | If the project has a parent element, the `name` and `path` here |
227 | are the prefixed ones. | 233 | are the prefixed ones. |
228 | 234 | ||
229 | Attribute `sync_c`: Set to true to only sync the given Git | 235 | Attribute `sync-c`: Set to true to only sync the given Git |
230 | branch (specified in the `revision` attribute) rather than the | 236 | branch (specified in the `revision` attribute) rather than the |
231 | whole ref space. | 237 | whole ref space. |
232 | 238 | ||
233 | Attribute `sync_s`: Set to true to also sync sub-projects. | 239 | Attribute `sync-s`: Set to true to also sync sub-projects. |
234 | 240 | ||
235 | Attribute `upstream`: Name of the Git branch in which a sha1 | 241 | Attribute `upstream`: Name of the Git branch in which a sha1 |
236 | can be found. Used when syncing a revision locked manifest in | 242 | can 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 | ||
16 | from __future__ import print_function | 16 | from __future__ import print_function |
17 | 17 | ||
18 | import json | ||
18 | import os | 19 | import os |
19 | import pickle | ||
20 | import re | 20 | import re |
21 | import subprocess | 21 | import subprocess |
22 | import sys | 22 | import 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 | # |
28 | add_ChangeId() { | 27 | add_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 | ||
@@ -46,7 +46,7 @@ if not is_python3(): | |||
46 | def _lwrite(path, content): | 46 | def _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 | ||
267 | class RepoHook(object): | 269 | class 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: |
@@ -139,10 +139,6 @@ def _print(*objects, **kwargs): | |||
139 | 139 | ||
140 | # Python version check | 140 | # Python version check |
141 | ver = sys.version_info | 141 | ver = sys.version_info |
142 | if 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) | ||
146 | if (ver[0], ver[1]) < MIN_PYTHON_VERSION: | 142 | if (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 | ||
770 | if __name__ == '__main__': | 766 | if __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 | ||
16 | from __future__ import print_function | 16 | from __future__ import print_function |
17 | import errno | ||
17 | import fcntl | 18 | import fcntl |
19 | import multiprocessing | ||
18 | import re | 20 | import re |
19 | import os | 21 | import os |
20 | import select | 22 | import select |
@@ -31,6 +33,7 @@ _CAN_COLOR = [ | |||
31 | 'log', | 33 | 'log', |
32 | ] | 34 | ] |
33 | 35 | ||
36 | |||
34 | class ForallColoring(Coloring): | 37 | class 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 | |||
240 | class WorkerKeyboardInterrupt(Exception): | ||
241 | """ Keyboard interrupt exception for worker processes. """ | ||
242 | pass | ||
243 | |||
244 | |||
245 | def 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 | |||
261 | def 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 | ||
16 | from __future__ import print_function | 16 | from __future__ import print_function |
17 | import json | ||
17 | import netrc | 18 | import netrc |
18 | from optparse import SUPPRESS_HELP | 19 | from optparse import SUPPRESS_HELP |
19 | import os | 20 | import os |
20 | import pickle | ||
21 | import re | 21 | import re |
22 | import shutil | 22 | import shutil |
23 | import socket | 23 | import 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 | |||
25 | from project import RepoHook | 25 | from project import RepoHook |
26 | 26 | ||
27 | from pyversion import is_python3 | 27 | from pyversion import is_python3 |
28 | # pylint:disable=W0622 | ||
28 | if not is_python3(): | 29 | if not is_python3(): |
29 | # pylint:disable=W0622 | ||
30 | input = raw_input | 30 | input = raw_input |
31 | # pylint:enable=W0622 | 31 | else: |
32 | unicode = str | ||
33 | # pylint:enable=W0622 | ||
32 | 34 | ||
33 | UNUSUAL_COMMIT_THRESHOLD = 5 | 35 | UNUSUAL_COMMIT_THRESHOLD = 5 |
34 | 36 | ||