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

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: one more argparse 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
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
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
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)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698