Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(167)

Side by Side Diff: git_common.py

Issue 184253003: Add git-reup and friends (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@freeze_thaw
Patch Set: fix pylint Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « git-upstream-diff ('k') | git_freezer.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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)
OLDNEW
« no previous file with comments | « git-upstream-diff ('k') | git_freezer.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698