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