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