OLD | NEW |
---|---|
1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 # Monkeypatch IMapIterator so that Ctrl-C can kill everything properly. | 5 # Monkeypatch IMapIterator so that Ctrl-C can kill everything properly. |
6 # Derived from https://gist.github.com/aljungberg/626518 | 6 # Derived from https://gist.github.com/aljungberg/626518 |
7 import multiprocessing.pool | 7 import multiprocessing.pool |
8 from multiprocessing.pool import IMapIterator | 8 from multiprocessing.pool import IMapIterator |
9 def wrapper(func): | 9 def wrapper(func): |
10 def wrap(self, timeout=None): | 10 def wrap(self, timeout=None): |
11 return func(self, timeout=timeout or 1e100) | 11 return func(self, timeout=timeout or 1e100) |
12 return wrap | 12 return wrap |
13 IMapIterator.next = wrapper(IMapIterator.next) | 13 IMapIterator.next = wrapper(IMapIterator.next) |
14 IMapIterator.__next__ = IMapIterator.next | 14 IMapIterator.__next__ = IMapIterator.next |
15 # TODO(iannucci): Monkeypatch all other 'wait' methods too. | 15 # TODO(iannucci): Monkeypatch all other 'wait' methods too. |
16 | 16 |
17 | 17 |
18 import binascii | 18 import binascii |
19 import collections | |
19 import contextlib | 20 import contextlib |
20 import functools | 21 import functools |
21 import logging | 22 import logging |
22 import os | 23 import os |
23 import signal | 24 import signal |
24 import sys | 25 import sys |
25 import tempfile | 26 import tempfile |
26 import threading | 27 import threading |
27 | 28 |
28 import subprocess2 | 29 import subprocess2 |
29 | 30 |
30 | 31 |
32 MERGE_BASE_FMT = 'branch.%s.base' | |
31 GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git' | 33 GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git' |
32 | 34 |
33 | 35 |
34 class BadCommitRefException(Exception): | 36 class BadCommitRefException(Exception): |
35 def __init__(self, refs): | 37 def __init__(self, refs): |
36 msg = ('one of %s does not seem to be a valid commitref.' % | 38 msg = ('one of %s does not seem to be a valid commitref.' % |
37 str(refs)) | 39 str(refs)) |
38 super(BadCommitRefException, self).__init__(msg) | 40 super(BadCommitRefException, self).__init__(msg) |
39 | 41 |
40 | 42 |
(...skipping 152 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
193 return self.inc | 195 return self.inc |
194 | 196 |
195 def __exit__(self, _exc_type, _exc_value, _traceback): | 197 def __exit__(self, _exc_type, _exc_value, _traceback): |
196 self._dead = True | 198 self._dead = True |
197 with self._dead_cond: | 199 with self._dead_cond: |
198 self._dead_cond.notifyAll() | 200 self._dead_cond.notifyAll() |
199 self._thread.join() | 201 self._thread.join() |
200 del self._thread | 202 del self._thread |
201 | 203 |
202 | 204 |
205 def once(function): | |
206 """@Decorates |function| so that it only performs its action once, no matter | |
207 how many times the decorated |function| is called.""" | |
208 def _inner_gen(): | |
209 yield function() | |
210 while True: | |
211 yield | |
212 return _inner_gen().next | |
213 | |
214 | |
215 ## Git functions | |
216 | |
203 def branches(*args): | 217 def branches(*args): |
204 NO_BRANCH = ('* (no branch)', '* (detached from ') | 218 NO_BRANCH = ('* (no branch', '* (detached from ') |
205 for line in run('branch', *args).splitlines(): | 219 for line in run('branch', *args).splitlines(): |
206 if line.startswith(NO_BRANCH): | 220 if line.startswith(NO_BRANCH): |
207 continue | 221 continue |
208 yield line.split()[-1] | 222 yield line.split()[-1] |
209 | 223 |
210 | 224 |
225 def config(option, default=None): | |
226 try: | |
227 return run('config', '--get', option) or default | |
228 except subprocess2.CalledProcessError: | |
229 return default | |
230 | |
231 | |
211 def config_list(option): | 232 def config_list(option): |
212 try: | 233 try: |
213 return run('config', '--get-all', option).split() | 234 return run('config', '--get-all', option).split() |
214 except subprocess2.CalledProcessError: | 235 except subprocess2.CalledProcessError: |
215 return [] | 236 return [] |
216 | 237 |
217 | 238 |
218 def current_branch(): | 239 def current_branch(): |
219 return run('rev-parse', '--abbrev-ref', 'HEAD') | 240 return run('rev-parse', '--abbrev-ref', 'HEAD') |
220 | 241 |
221 | 242 |
243 def del_config(option, scope='local'): | |
244 run('config', '--' + scope, '--unset', option) | |
245 | |
246 | |
247 def get_branch_tree(): | |
248 """Get the dictionary of {branch: parent}, compatible with topo_iter. | |
249 | |
250 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a list of | |
251 branches without upstream branches defined. | |
252 """ | |
253 skipped = [] | |
agable
2014/03/21 01:14:21
set?
iannucci
2014/03/22 04:17:35
Done.
| |
254 branch_tree = {} | |
255 | |
256 for branch in branches(): | |
257 parent = upstream(branch) | |
258 if not parent: | |
259 skipped.append(branch) | |
260 continue | |
261 branch_tree[branch] = parent | |
262 | |
263 return skipped, branch_tree | |
264 | |
265 | |
222 def parse_commitrefs(*commitrefs): | 266 def parse_commitrefs(*commitrefs): |
223 """Returns binary encoded commit hashes for one or more commitrefs. | 267 """Returns binary encoded commit hashes for one or more commitrefs. |
224 | 268 |
225 A commitref is anything which can resolve to a commit. Popular examples: | 269 A commitref is anything which can resolve to a commit. Popular examples: |
226 * 'HEAD' | 270 * 'HEAD' |
227 * 'origin/master' | 271 * 'origin/master' |
228 * 'cool_branch~2' | 272 * 'cool_branch~2' |
229 """ | 273 """ |
230 try: | 274 try: |
231 return map(binascii.unhexlify, hash_multi(*commitrefs)) | 275 return map(binascii.unhexlify, hash_multi(*commitrefs)) |
232 except subprocess2.CalledProcessError: | 276 except subprocess2.CalledProcessError: |
233 raise BadCommitRefException(commitrefs) | 277 raise BadCommitRefException(commitrefs) |
234 | 278 |
235 | 279 |
280 def root(): | |
281 return config('depot-tools.upstream', 'origin/master') | |
282 | |
283 | |
236 def run(*cmd, **kwargs): | 284 def run(*cmd, **kwargs): |
237 """Runs a git command. Returns stdout as a string. | 285 """The same as run_both, except it only returns stdout.""" |
286 return run_both(*cmd, **kwargs)[0] | |
238 | 287 |
239 If logging is DEBUG, we'll print the command before we run it. | 288 |
289 def run_both(*cmd, **kwargs): | |
agable
2014/03/21 01:14:21
run_both? really? least descriptive name evar.
iannucci
2014/03/22 04:17:35
Done.
| |
290 """Runs a git command. | |
291 | |
292 Returns (stdout, stderr) as a pair of strings. | |
240 | 293 |
241 kwargs | 294 kwargs |
242 autostrip (bool) - Strip the output. Defaults to True. | 295 autostrip (bool) - Strip the output. Defaults to True. |
296 indata (str) - Specifies stdin data for the process. | |
243 """ | 297 """ |
298 kwargs.setdefault('stdin', subprocess2.PIPE) | |
299 kwargs.setdefault('stdout', subprocess2.PIPE) | |
300 kwargs.setdefault('stderr', subprocess2.PIPE) | |
244 autostrip = kwargs.pop('autostrip', True) | 301 autostrip = kwargs.pop('autostrip', True) |
302 indata = kwargs.pop('indata', None) | |
245 | 303 |
246 retstream, proc = stream_proc(*cmd, **kwargs) | 304 cmd = (GIT_EXE,) + cmd |
247 ret = retstream.read() | 305 proc = subprocess2.Popen(cmd, **kwargs) |
306 ret, err = proc.communicate(indata) | |
248 retcode = proc.wait() | 307 retcode = proc.wait() |
249 if retcode != 0: | 308 if retcode != 0: |
250 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, None) | 309 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err) |
251 | 310 |
252 if autostrip: | 311 if autostrip: |
253 ret = (ret or '').strip() | 312 ret = (ret or '').strip() |
254 return ret | 313 err = (err or '').strip() |
255 | 314 |
256 | 315 return ret, err |
257 def 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 | 316 |
268 | 317 |
269 def stream(*cmd, **kwargs): | 318 def stream(*cmd, **kwargs): |
agable
2014/03/21 01:14:21
Also poorly named. Since it's executing a command
iannucci
2014/03/22 04:17:35
Done.
| |
270 return stream_proc(*cmd, **kwargs)[0] | 319 """Runs a git command. Returns stdout as a file.""" |
320 kwargs.setdefault('stderr', subprocess2.VOID) | |
321 kwargs.setdefault('stdout', subprocess2.PIPE) | |
322 cmd = (GIT_EXE,) + cmd | |
323 proc = subprocess2.Popen(cmd, **kwargs) | |
324 return proc.stdout | |
325 | |
326 | |
327 def set_config(option, value, scope='local'): | |
328 run('config', '--' + scope, option, value) | |
329 | |
330 | |
331 def get_or_create_merge_base(branch, parent=None): | |
332 """Finds the configured merge base for branch. | |
333 | |
334 If parent is supplied, it's used instead of calling upstream(branch). | |
335 """ | |
336 option = MERGE_BASE_FMT % branch | |
337 base = config(option) | |
338 if base: | |
339 try: | |
340 run('merge-base', '--is-ancestor', base, branch) | |
341 logging.debug('Found pre-set merge-base for %s: %s', branch, base) | |
342 except subprocess2.CalledProcessError: | |
343 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base) | |
344 base = None | |
345 | |
346 if not base: | |
347 base = run('merge-base', parent or upstream(branch), branch) | |
348 set_config(option, base) | |
349 | |
350 return base | |
271 | 351 |
272 | 352 |
273 def hash_one(reflike): | 353 def hash_one(reflike): |
274 return run('rev-parse', reflike) | 354 return run('rev-parse', reflike) |
275 | 355 |
276 | 356 |
277 def hash_multi(*reflike): | 357 def hash_multi(*reflike): |
278 return run('rev-parse', *reflike).splitlines() | 358 return run('rev-parse', *reflike).splitlines() |
279 | 359 |
280 | 360 |
281 def intern_f(f, kind='blob'): | 361 def intern_f(f, kind='blob'): |
282 """Interns a file object into the git object store. | 362 """Interns a file object into the git object store. |
283 | 363 |
284 Args: | 364 Args: |
285 f (file-like object) - The file-like object to intern | 365 f (file-like object) - The file-like object to intern |
286 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'. | 366 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'. |
287 | 367 |
288 Returns the git hash of the interned object (hex encoded). | 368 Returns the git hash of the interned object (hex encoded). |
289 """ | 369 """ |
290 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f) | 370 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f) |
291 f.close() | 371 f.close() |
292 return ret | 372 return ret |
293 | 373 |
294 | 374 |
375 def in_rebase(): | |
376 git_dir = run('rev-parse', '--git-dir') | |
377 return ( | |
378 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or | |
379 os.path.exists(os.path.join(git_dir, 'rebase-apply'))) | |
380 | |
381 | |
382 def manual_merge_base(branch, base): | |
383 set_config(MERGE_BASE_FMT % branch, base) | |
384 | |
385 | |
386 def mktree(treedict): | |
387 """Makes a git tree object and returns its hash. | |
388 | |
389 See |tree()| for the values of mode, type, and ref. | |
390 | |
391 Args: | |
392 treedict - { name: (mode, type, ref) } | |
393 """ | |
394 with tempfile.TemporaryFile() as f: | |
395 for name, (mode, typ, ref) in treedict.iteritems(): | |
396 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name)) | |
397 f.seek(0) | |
398 return run('mktree', '-z', stdin=f) | |
399 | |
400 | |
401 def remove_merge_base(branch): | |
402 del_config(MERGE_BASE_FMT % branch) | |
403 | |
404 | |
405 RebaseRet = collections.namedtuple('RebaseRet', 'success message') | |
406 | |
407 | |
408 def rebase(parent, start, branch, abort=False, ignore_date=False): | |
409 """Rebases |start|..|branch| onto the branch |parent|. | |
agable
2014/03/21 01:14:21
start..branch doesn't actually include start, in g
iannucci
2014/03/22 04:17:35
Er... no it doesn't include start, which is why I
| |
410 | |
411 Args: | |
412 parent - The new parent ref for the rebased commits. | |
413 start - The commit to start from | |
414 branch - The branch to rebase | |
415 abort - If True, will call git-rebase --abort in the event that the rebase | |
416 doesn't complete successfully. | |
417 ignore_date - If True, will cause commit timestamps to match the timestamps | |
418 of the original commits. Mostly used for getting deterministic | |
419 timestamps in tests. | |
420 | |
421 Returns a namedtuple with fields: | |
422 success - a boolean indicating that the rebase command completed | |
423 successfully. | |
424 message - if the rebase failed, this contains the stdout of the failed | |
425 rebase. | |
426 """ | |
427 try: | |
428 args = ['--committer-date-is-author-date'] if ignore_date else [] | |
429 args.extend(('--onto', parent, start, branch)) | |
430 run('rebase', *args) | |
431 return RebaseRet(True, '') | |
432 except subprocess2.CalledProcessError as cpe: | |
433 if abort: | |
434 run('rebase', '--abort') | |
435 return RebaseRet(False, cpe.output) | |
436 | |
437 | |
438 def squash_current_branch(header=None, merge_base=None): | |
439 header = header or 'git squash commit.' | |
440 merge_base = merge_base or get_or_create_merge_base(current_branch()) | |
441 log_msg = header | |
442 if log_msg: | |
443 log_msg += '\n' | |
444 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base) | |
445 run('reset', '--soft', merge_base) | |
446 run('commit', '-a', '-F', '-', indata=log_msg) | |
447 | |
448 | |
295 def tags(*args): | 449 def tags(*args): |
296 return run('tag', *args).splitlines() | 450 return run('tag', *args).splitlines() |
297 | 451 |
298 | 452 |
453 def topo_iter(branch_tree, top_down=True): | |
454 """Generates (branch, parent) in topographical order for a branch tree. | |
455 | |
456 Given a tree: | |
457 | |
458 A1 | |
459 B1 B2 | |
460 C1 C2 C3 | |
461 D1 | |
462 | |
463 branch_tree would look like: { | |
464 'D1': 'C3', | |
465 'C3': 'B2', | |
466 'B2': 'A1', | |
467 'C1': 'B1', | |
468 'C2': 'B1', | |
469 'B1': 'A1', | |
470 } | |
471 | |
472 It is OK to have multiple 'root' nodes in your graph. | |
473 | |
474 if top_down is True, items are yielded from A->D. Otherwise they're yielded | |
475 from D->A. There is no specified ordering within a layer. | |
476 """ | |
477 branch_tree = branch_tree.copy() | |
478 | |
479 # TODO(iannucci): There is probably a more efficient way to do these. | |
480 if top_down: | |
481 while branch_tree: | |
482 this_pass = [(b, p) for b, p in branch_tree.iteritems() | |
483 if p not in branch_tree] | |
484 assert this_pass, "Branch tree has cycles: %r" % branch_tree | |
485 for branch, parent in this_pass: | |
486 yield branch, parent | |
487 del branch_tree[branch] | |
488 else: | |
489 parent_to_branches = collections.defaultdict(set) | |
490 for branch, parent in branch_tree.iteritems(): | |
491 parent_to_branches[parent].add(branch) | |
492 | |
493 while branch_tree: | |
494 this_pass = [(b, p) for b, p in branch_tree.iteritems() | |
495 if not parent_to_branches[b]] | |
496 assert this_pass, "Branch tree has cycles: %r" % branch_tree | |
497 for branch, parent in this_pass: | |
498 yield branch, parent | |
499 parent_to_branches[parent].discard(branch) | |
500 del branch_tree[branch] | |
501 | |
502 | |
299 def tree(treeref, recurse=False): | 503 def tree(treeref, recurse=False): |
300 """Returns a dict representation of a git tree object. | 504 """Returns a dict representation of a git tree object. |
301 | 505 |
302 Args: | 506 Args: |
303 treeref (str) - a git ref which resolves to a tree (commits count as trees). | 507 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 | 508 recurse (bool) - include all of the tree's decendants too. File names will |
305 take the form of 'some/path/to/file'. | 509 take the form of 'some/path/to/file'. |
306 | 510 |
307 Return format: | 511 Return format: |
308 { 'file_name': (mode, type, ref) } | 512 { 'file_name': (mode, type, ref) } |
(...skipping 23 matching lines...) Expand all Loading... | |
332 return None | 536 return None |
333 return ret | 537 return ret |
334 | 538 |
335 | 539 |
336 def upstream(branch): | 540 def upstream(branch): |
337 try: | 541 try: |
338 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name', | 542 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name', |
339 branch+'@{upstream}') | 543 branch+'@{upstream}') |
340 except subprocess2.CalledProcessError: | 544 except subprocess2.CalledProcessError: |
341 return None | 545 return None |
342 | |
343 | |
344 def 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) | |
OLD | NEW |