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

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: minor fixes 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
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
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
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
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)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698