blob: 1296286b42e076c2c5aa265d88eb8b16422a6284 [file] [log] [blame]
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +00001# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# Monkeypatch IMapIterator so that Ctrl-C can kill everything properly.
6# Derived from https://gist.github.com/aljungberg/626518
7import multiprocessing.pool
8from multiprocessing.pool import IMapIterator
9def wrapper(func):
10 def wrap(self, timeout=None):
11 return func(self, timeout=timeout or 1e100)
12 return wrap
13IMapIterator.next = wrapper(IMapIterator.next)
14IMapIterator.__next__ = IMapIterator.next
15# TODO(iannucci): Monkeypatch all other 'wait' methods too.
16
17
18import binascii
19import contextlib
20import functools
21import logging
iannucci@chromium.org97345eb2014-03-13 07:55:15 +000022import os
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000023import signal
24import sys
25import tempfile
26import threading
27
28import subprocess2
29
30
31GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git'
32
33
34class BadCommitRefException(Exception):
35 def __init__(self, refs):
36 msg = ('one of %s does not seem to be a valid commitref.' %
37 str(refs))
38 super(BadCommitRefException, self).__init__(msg)
39
40
41def memoize_one(**kwargs):
42 """Memoizes a single-argument pure function.
43
44 Values of None are not cached.
45
46 Kwargs:
47 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
48 cache manipulation functions. This is a kwarg so that users of memoize_one
49 are forced to explicitly and verbosely pick True or False.
50
51 Adds three methods to the decorated function:
52 * get(key, default=None) - Gets the value for this key from the cache.
53 * set(key, value) - Sets the value for this key from the cache.
54 * clear() - Drops the entire contents of the cache. Useful for unittests.
55 * update(other) - Updates the contents of the cache from another dict.
56 """
57 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
58 threadsafe = kwargs['threadsafe']
59
60 if threadsafe:
61 def withlock(lock, f):
62 def inner(*args, **kwargs):
63 with lock:
64 return f(*args, **kwargs)
65 return inner
66 else:
67 def withlock(_lock, f):
68 return f
69
70 def decorator(f):
71 # Instantiate the lock in decorator, in case users of memoize_one do:
72 #
73 # memoizer = memoize_one(threadsafe=True)
74 #
75 # @memoizer
76 # def fn1(val): ...
77 #
78 # @memoizer
79 # def fn2(val): ...
80
81 lock = threading.Lock() if threadsafe else None
82 cache = {}
83 _get = withlock(lock, cache.get)
84 _set = withlock(lock, cache.__setitem__)
85
86 @functools.wraps(f)
87 def inner(arg):
88 ret = _get(arg)
89 if ret is None:
90 ret = f(arg)
91 if ret is not None:
92 _set(arg, ret)
93 return ret
94 inner.get = _get
95 inner.set = _set
96 inner.clear = withlock(lock, cache.clear)
97 inner.update = withlock(lock, cache.update)
98 return inner
99 return decorator
100
101
102def _ScopedPool_initer(orig, orig_args): # pragma: no cover
103 """Initializer method for ScopedPool's subprocesses.
104
105 This helps ScopedPool handle Ctrl-C's correctly.
106 """
107 signal.signal(signal.SIGINT, signal.SIG_IGN)
108 if orig:
109 orig(*orig_args)
110
111
112@contextlib.contextmanager
113def ScopedPool(*args, **kwargs):
114 """Context Manager which returns a multiprocessing.pool instance which
115 correctly deals with thrown exceptions.
116
117 *args - Arguments to multiprocessing.pool
118
119 Kwargs:
120 kind ('threads', 'procs') - The type of underlying coprocess to use.
121 **etc - Arguments to multiprocessing.pool
122 """
123 if kwargs.pop('kind', None) == 'threads':
124 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
125 else:
126 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
127 kwargs['initializer'] = _ScopedPool_initer
128 kwargs['initargs'] = orig, orig_args
129 pool = multiprocessing.pool.Pool(*args, **kwargs)
130
131 try:
132 yield pool
133 pool.close()
134 except:
135 pool.terminate()
136 raise
137 finally:
138 pool.join()
139
140
141class ProgressPrinter(object):
142 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000143 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000144 """Create a ProgressPrinter.
145
146 Use it as a context manager which produces a simple 'increment' method:
147
148 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
149 for i in xrange(1000):
150 # do stuff
151 if i % 10 == 0:
152 inc(10)
153
154 Args:
155 fmt - String format with a single '%(count)d' where the counter value
156 should go.
157 enabled (bool) - If this is None, will default to True if
158 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000159 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000160 period (float) - The time in seconds for the printer thread to wait
161 between printing.
162 """
163 self.fmt = fmt
164 if enabled is None: # pragma: no cover
165 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
166 else:
167 self.enabled = enabled
168
169 self._count = 0
170 self._dead = False
171 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000172 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000173 self._thread = threading.Thread(target=self._run)
174 self._period = period
175
176 def _emit(self, s):
177 if self.enabled:
178 self._stream.write('\r' + s)
179 self._stream.flush()
180
181 def _run(self):
182 with self._dead_cond:
183 while not self._dead:
184 self._emit(self.fmt % {'count': self._count})
185 self._dead_cond.wait(self._period)
186 self._emit((self.fmt + '\n') % {'count': self._count})
187
188 def inc(self, amount=1):
189 self._count += amount
190
191 def __enter__(self):
192 self._thread.start()
193 return self.inc
194
195 def __exit__(self, _exc_type, _exc_value, _traceback):
196 self._dead = True
197 with self._dead_cond:
198 self._dead_cond.notifyAll()
199 self._thread.join()
200 del self._thread
201
202
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000203def branches(*args):
204 NO_BRANCH = ('* (no branch)', '* (detached from ')
205 for line in run('branch', *args).splitlines():
206 if line.startswith(NO_BRANCH):
207 continue
208 yield line.split()[-1]
209
210
211def config_list(option):
212 try:
213 return run('config', '--get-all', option).split()
214 except subprocess2.CalledProcessError:
215 return []
216
217
218def current_branch():
219 return run('rev-parse', '--abbrev-ref', 'HEAD')
220
221
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000222def parse_commitrefs(*commitrefs):
223 """Returns binary encoded commit hashes for one or more commitrefs.
224
225 A commitref is anything which can resolve to a commit. Popular examples:
226 * 'HEAD'
227 * 'origin/master'
228 * 'cool_branch~2'
229 """
230 try:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000231 return map(binascii.unhexlify, hash_multi(*commitrefs))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000232 except subprocess2.CalledProcessError:
233 raise BadCommitRefException(commitrefs)
234
235
236def run(*cmd, **kwargs):
237 """Runs a git command. Returns stdout as a string.
238
239 If logging is DEBUG, we'll print the command before we run it.
240
241 kwargs
242 autostrip (bool) - Strip the output. Defaults to True.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000243 """
244 autostrip = kwargs.pop('autostrip', True)
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000245
246 retstream, proc = stream_proc(*cmd, **kwargs)
247 ret = retstream.read()
248 retcode = proc.wait()
249 if retcode != 0:
250 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, None)
251
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000252 if autostrip:
253 ret = (ret or '').strip()
254 return ret
255
256
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000257def stream_proc(*cmd, **kwargs):
258 """Runs a git command. Returns stdout as a file.
259
260 If logging is DEBUG, we'll print the command before we run it.
261 """
262 cmd = (GIT_EXE,) + cmd
263 logging.debug('Running %s', ' '.join(repr(tok) for tok in cmd))
264 proc = subprocess2.Popen(cmd, stderr=subprocess2.VOID,
265 stdout=subprocess2.PIPE, **kwargs)
266 return proc.stdout, proc
267
268
269def stream(*cmd, **kwargs):
270 return stream_proc(*cmd, **kwargs)[0]
271
272
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000273def hash_one(reflike):
274 return run('rev-parse', reflike)
275
276
277def hash_multi(*reflike):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000278 return run('rev-parse', *reflike).splitlines()
279
280
281def intern_f(f, kind='blob'):
282 """Interns a file object into the git object store.
283
284 Args:
285 f (file-like object) - The file-like object to intern
286 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
287
288 Returns the git hash of the interned object (hex encoded).
289 """
290 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
291 f.close()
292 return ret
293
294
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000295def tags(*args):
296 return run('tag', *args).splitlines()
297
298
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000299def tree(treeref, recurse=False):
300 """Returns a dict representation of a git tree object.
301
302 Args:
303 treeref (str) - a git ref which resolves to a tree (commits count as trees).
304 recurse (bool) - include all of the tree's decendants too. File names will
305 take the form of 'some/path/to/file'.
306
307 Return format:
308 { 'file_name': (mode, type, ref) }
309
310 mode is an integer where:
311 * 0040000 - Directory
312 * 0100644 - Regular non-executable file
313 * 0100664 - Regular non-executable group-writeable file
314 * 0100755 - Regular executable file
315 * 0120000 - Symbolic link
316 * 0160000 - Gitlink
317
318 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
319
320 ref is the hex encoded hash of the entry.
321 """
322 ret = {}
323 opts = ['ls-tree', '--full-tree']
324 if recurse:
325 opts.append('-r')
326 opts.append(treeref)
327 try:
328 for line in run(*opts).splitlines():
329 mode, typ, ref, name = line.split(None, 3)
330 ret[name] = (mode, typ, ref)
331 except subprocess2.CalledProcessError:
332 return None
333 return ret
334
335
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000336def upstream(branch):
337 try:
338 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
339 branch+'@{upstream}')
340 except subprocess2.CalledProcessError:
341 return None
342
343
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000344def mktree(treedict):
345 """Makes a git tree object and returns its hash.
346
347 See |tree()| for the values of mode, type, and ref.
348
349 Args:
350 treedict - { name: (mode, type, ref) }
351 """
352 with tempfile.TemporaryFile() as f:
353 for name, (mode, typ, ref) in treedict.iteritems():
354 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
355 f.seek(0)
356 return run('mktree', '-z', stdin=f)