git_common.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  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
  3. # found in the LICENSE file.
  4. # Monkeypatch IMapIterator so that Ctrl-C can kill everything properly.
  5. # Derived from https://gist.github.com/aljungberg/626518
  6. import multiprocessing.pool
  7. from multiprocessing.pool import IMapIterator
  8. def wrapper(func):
  9. def wrap(self, timeout=None):
  10. return func(self, timeout=timeout or 1e100)
  11. return wrap
  12. IMapIterator.next = wrapper(IMapIterator.next)
  13. IMapIterator.__next__ = IMapIterator.next
  14. # TODO(iannucci): Monkeypatch all other 'wait' methods too.
  15. import binascii
  16. import collections
  17. import contextlib
  18. import functools
  19. import logging
  20. import os
  21. import re
  22. import signal
  23. import sys
  24. import tempfile
  25. import textwrap
  26. import threading
  27. import subprocess2
  28. GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git'
  29. TEST_MODE = False
  30. FREEZE = 'FREEZE'
  31. FREEZE_SECTIONS = {
  32. 'indexed': 'soft',
  33. 'unindexed': 'mixed'
  34. }
  35. FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
  36. class BadCommitRefException(Exception):
  37. def __init__(self, refs):
  38. msg = ('one of %s does not seem to be a valid commitref.' %
  39. str(refs))
  40. super(BadCommitRefException, self).__init__(msg)
  41. def memoize_one(**kwargs):
  42. """Memoizes a single-argument pure function.
  43. Values of None are not cached.
  44. Kwargs:
  45. threadsafe (bool) - REQUIRED. Specifies whether to use locking around
  46. cache manipulation functions. This is a kwarg so that users of memoize_one
  47. are forced to explicitly and verbosely pick True or False.
  48. Adds three methods to the decorated function:
  49. * get(key, default=None) - Gets the value for this key from the cache.
  50. * set(key, value) - Sets the value for this key from the cache.
  51. * clear() - Drops the entire contents of the cache. Useful for unittests.
  52. * update(other) - Updates the contents of the cache from another dict.
  53. """
  54. assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
  55. threadsafe = kwargs['threadsafe']
  56. if threadsafe:
  57. def withlock(lock, f):
  58. def inner(*args, **kwargs):
  59. with lock:
  60. return f(*args, **kwargs)
  61. return inner
  62. else:
  63. def withlock(_lock, f):
  64. return f
  65. def decorator(f):
  66. # Instantiate the lock in decorator, in case users of memoize_one do:
  67. #
  68. # memoizer = memoize_one(threadsafe=True)
  69. #
  70. # @memoizer
  71. # def fn1(val): ...
  72. #
  73. # @memoizer
  74. # def fn2(val): ...
  75. lock = threading.Lock() if threadsafe else None
  76. cache = {}
  77. _get = withlock(lock, cache.get)
  78. _set = withlock(lock, cache.__setitem__)
  79. @functools.wraps(f)
  80. def inner(arg):
  81. ret = _get(arg)
  82. if ret is None:
  83. ret = f(arg)
  84. if ret is not None:
  85. _set(arg, ret)
  86. return ret
  87. inner.get = _get
  88. inner.set = _set
  89. inner.clear = withlock(lock, cache.clear)
  90. inner.update = withlock(lock, cache.update)
  91. return inner
  92. return decorator
  93. def _ScopedPool_initer(orig, orig_args): # pragma: no cover
  94. """Initializer method for ScopedPool's subprocesses.
  95. This helps ScopedPool handle Ctrl-C's correctly.
  96. """
  97. signal.signal(signal.SIGINT, signal.SIG_IGN)
  98. if orig:
  99. orig(*orig_args)
  100. @contextlib.contextmanager
  101. def ScopedPool(*args, **kwargs):
  102. """Context Manager which returns a multiprocessing.pool instance which
  103. correctly deals with thrown exceptions.
  104. *args - Arguments to multiprocessing.pool
  105. Kwargs:
  106. kind ('threads', 'procs') - The type of underlying coprocess to use.
  107. **etc - Arguments to multiprocessing.pool
  108. """
  109. if kwargs.pop('kind', None) == 'threads':
  110. pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
  111. else:
  112. orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
  113. kwargs['initializer'] = _ScopedPool_initer
  114. kwargs['initargs'] = orig, orig_args
  115. pool = multiprocessing.pool.Pool(*args, **kwargs)
  116. try:
  117. yield pool
  118. pool.close()
  119. except:
  120. pool.terminate()
  121. raise
  122. finally:
  123. pool.join()
  124. class ProgressPrinter(object):
  125. """Threaded single-stat status message printer."""
  126. def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
  127. """Create a ProgressPrinter.
  128. Use it as a context manager which produces a simple 'increment' method:
  129. with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
  130. for i in xrange(1000):
  131. # do stuff
  132. if i % 10 == 0:
  133. inc(10)
  134. Args:
  135. fmt - String format with a single '%(count)d' where the counter value
  136. should go.
  137. enabled (bool) - If this is None, will default to True if
  138. logging.getLogger() is set to INFO or more verbose.
  139. fout (file-like) - The stream to print status messages to.
  140. period (float) - The time in seconds for the printer thread to wait
  141. between printing.
  142. """
  143. self.fmt = fmt
  144. if enabled is None: # pragma: no cover
  145. self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
  146. else:
  147. self.enabled = enabled
  148. self._count = 0
  149. self._dead = False
  150. self._dead_cond = threading.Condition()
  151. self._stream = fout
  152. self._thread = threading.Thread(target=self._run)
  153. self._period = period
  154. def _emit(self, s):
  155. if self.enabled:
  156. self._stream.write('\r' + s)
  157. self._stream.flush()
  158. def _run(self):
  159. with self._dead_cond:
  160. while not self._dead:
  161. self._emit(self.fmt % {'count': self._count})
  162. self._dead_cond.wait(self._period)
  163. self._emit((self.fmt + '\n') % {'count': self._count})
  164. def inc(self, amount=1):
  165. self._count += amount
  166. def __enter__(self):
  167. self._thread.start()
  168. return self.inc
  169. def __exit__(self, _exc_type, _exc_value, _traceback):
  170. self._dead = True
  171. with self._dead_cond:
  172. self._dead_cond.notifyAll()
  173. self._thread.join()
  174. del self._thread
  175. def once(function):
  176. """@Decorates |function| so that it only performs its action once, no matter
  177. how many times the decorated |function| is called."""
  178. def _inner_gen():
  179. yield function()
  180. while True:
  181. yield
  182. return _inner_gen().next
  183. ## Git functions
  184. def branch_config(branch, option, default=None):
  185. return config('branch.%s.%s' % (branch, option), default=default)
  186. def branch_config_map(option):
  187. """Return {branch: <|option| value>} for all branches."""
  188. try:
  189. reg = re.compile(r'^branch\.(.*)\.%s$' % option)
  190. lines = run('config', '--get-regexp', reg.pattern).splitlines()
  191. return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
  192. except subprocess2.CalledProcessError:
  193. return {}
  194. def branches(*args):
  195. NO_BRANCH = ('* (no branch', '* (detached from ')
  196. key = 'depot-tools.branch-limit'
  197. limit = 20
  198. try:
  199. limit = int(config(key, limit))
  200. except ValueError:
  201. pass
  202. raw_branches = run('branch', *args).splitlines()
  203. num = len(raw_branches)
  204. if num > limit:
  205. print >> sys.stderr, textwrap.dedent("""\
  206. Your git repo has too many branches (%d/%d) for this tool to work well.
  207. You may adjust this limit by running:
  208. git config %s <new_limit>
  209. """ % (num, limit, key))
  210. sys.exit(1)
  211. for line in raw_branches:
  212. if line.startswith(NO_BRANCH):
  213. continue
  214. yield line.split()[-1]
  215. def run_with_retcode(*cmd, **kwargs):
  216. """Run a command but only return the status code."""
  217. try:
  218. run(*cmd, **kwargs)
  219. return 0
  220. except subprocess2.CalledProcessError as cpe:
  221. return cpe.returncode
  222. def config(option, default=None):
  223. try:
  224. return run('config', '--get', option) or default
  225. except subprocess2.CalledProcessError:
  226. return default
  227. def config_list(option):
  228. try:
  229. return run('config', '--get-all', option).split()
  230. except subprocess2.CalledProcessError:
  231. return []
  232. def current_branch():
  233. try:
  234. return run('rev-parse', '--abbrev-ref', 'HEAD')
  235. except subprocess2.CalledProcessError:
  236. return None
  237. def del_branch_config(branch, option, scope='local'):
  238. del_config('branch.%s.%s' % (branch, option), scope=scope)
  239. def del_config(option, scope='local'):
  240. try:
  241. run('config', '--' + scope, '--unset', option)
  242. except subprocess2.CalledProcessError:
  243. pass
  244. def freeze():
  245. took_action = False
  246. try:
  247. run('commit', '-m', FREEZE + '.indexed')
  248. took_action = True
  249. except subprocess2.CalledProcessError:
  250. pass
  251. try:
  252. run('add', '-A')
  253. run('commit', '-m', FREEZE + '.unindexed')
  254. took_action = True
  255. except subprocess2.CalledProcessError:
  256. pass
  257. if not took_action:
  258. return 'Nothing to freeze.'
  259. def get_branch_tree():
  260. """Get the dictionary of {branch: parent}, compatible with topo_iter.
  261. Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
  262. branches without upstream branches defined.
  263. """
  264. skipped = set()
  265. branch_tree = {}
  266. for branch in branches():
  267. parent = upstream(branch)
  268. if not parent:
  269. skipped.add(branch)
  270. continue
  271. branch_tree[branch] = parent
  272. return skipped, branch_tree
  273. def get_or_create_merge_base(branch, parent=None):
  274. """Finds the configured merge base for branch.
  275. If parent is supplied, it's used instead of calling upstream(branch).
  276. """
  277. base = branch_config(branch, 'base')
  278. base_upstream = branch_config(branch, 'base-upstream')
  279. parent = parent or upstream(branch)
  280. if not parent:
  281. return None
  282. actual_merge_base = run('merge-base', parent, branch)
  283. if base_upstream != parent:
  284. base = None
  285. base_upstream = None
  286. def is_ancestor(a, b):
  287. return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
  288. if base:
  289. if not is_ancestor(base, branch):
  290. logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
  291. base = None
  292. elif is_ancestor(base, actual_merge_base):
  293. logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
  294. base = None
  295. else:
  296. logging.debug('Found pre-set merge-base for %s: %s', branch, base)
  297. if not base:
  298. base = actual_merge_base
  299. manual_merge_base(branch, base, parent)
  300. return base
  301. def hash_multi(*reflike):
  302. return run('rev-parse', *reflike).splitlines()
  303. def hash_one(reflike):
  304. return run('rev-parse', reflike)
  305. def in_rebase():
  306. git_dir = run('rev-parse', '--git-dir')
  307. return (
  308. os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
  309. os.path.exists(os.path.join(git_dir, 'rebase-apply')))
  310. def intern_f(f, kind='blob'):
  311. """Interns a file object into the git object store.
  312. Args:
  313. f (file-like object) - The file-like object to intern
  314. kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
  315. Returns the git hash of the interned object (hex encoded).
  316. """
  317. ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
  318. f.close()
  319. return ret
  320. def is_dormant(branch):
  321. # TODO(iannucci): Do an oldness check?
  322. return branch_config(branch, 'dormant', 'false') != 'false'
  323. def manual_merge_base(branch, base, parent):
  324. set_branch_config(branch, 'base', base)
  325. set_branch_config(branch, 'base-upstream', parent)
  326. def mktree(treedict):
  327. """Makes a git tree object and returns its hash.
  328. See |tree()| for the values of mode, type, and ref.
  329. Args:
  330. treedict - { name: (mode, type, ref) }
  331. """
  332. with tempfile.TemporaryFile() as f:
  333. for name, (mode, typ, ref) in treedict.iteritems():
  334. f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
  335. f.seek(0)
  336. return run('mktree', '-z', stdin=f)
  337. def parse_commitrefs(*commitrefs):
  338. """Returns binary encoded commit hashes for one or more commitrefs.
  339. A commitref is anything which can resolve to a commit. Popular examples:
  340. * 'HEAD'
  341. * 'origin/master'
  342. * 'cool_branch~2'
  343. """
  344. try:
  345. return map(binascii.unhexlify, hash_multi(*commitrefs))
  346. except subprocess2.CalledProcessError:
  347. raise BadCommitRefException(commitrefs)
  348. RebaseRet = collections.namedtuple('RebaseRet', 'success message')
  349. def rebase(parent, start, branch, abort=False):
  350. """Rebases |start|..|branch| onto the branch |parent|.
  351. Args:
  352. parent - The new parent ref for the rebased commits.
  353. start - The commit to start from
  354. branch - The branch to rebase
  355. abort - If True, will call git-rebase --abort in the event that the rebase
  356. doesn't complete successfully.
  357. Returns a namedtuple with fields:
  358. success - a boolean indicating that the rebase command completed
  359. successfully.
  360. message - if the rebase failed, this contains the stdout of the failed
  361. rebase.
  362. """
  363. try:
  364. args = ['--onto', parent, start, branch]
  365. if TEST_MODE:
  366. args.insert(0, '--committer-date-is-author-date')
  367. run('rebase', *args)
  368. return RebaseRet(True, '')
  369. except subprocess2.CalledProcessError as cpe:
  370. if abort:
  371. run('rebase', '--abort')
  372. return RebaseRet(False, cpe.stdout)
  373. def remove_merge_base(branch):
  374. del_branch_config(branch, 'base')
  375. del_branch_config(branch, 'base-upstream')
  376. def root():
  377. return config('depot-tools.upstream', 'origin/master')
  378. def run(*cmd, **kwargs):
  379. """The same as run_with_stderr, except it only returns stdout."""
  380. return run_with_stderr(*cmd, **kwargs)[0]
  381. def run_stream(*cmd, **kwargs):
  382. """Runs a git command. Returns stdout as a PIPE (file-like object).
  383. stderr is dropped to avoid races if the process outputs to both stdout and
  384. stderr.
  385. """
  386. kwargs.setdefault('stderr', subprocess2.VOID)
  387. kwargs.setdefault('stdout', subprocess2.PIPE)
  388. cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
  389. proc = subprocess2.Popen(cmd, **kwargs)
  390. return proc.stdout
  391. def run_with_stderr(*cmd, **kwargs):
  392. """Runs a git command.
  393. Returns (stdout, stderr) as a pair of strings.
  394. kwargs
  395. autostrip (bool) - Strip the output. Defaults to True.
  396. indata (str) - Specifies stdin data for the process.
  397. """
  398. kwargs.setdefault('stdin', subprocess2.PIPE)
  399. kwargs.setdefault('stdout', subprocess2.PIPE)
  400. kwargs.setdefault('stderr', subprocess2.PIPE)
  401. autostrip = kwargs.pop('autostrip', True)
  402. indata = kwargs.pop('indata', None)
  403. cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
  404. proc = subprocess2.Popen(cmd, **kwargs)
  405. ret, err = proc.communicate(indata)
  406. retcode = proc.wait()
  407. if retcode != 0:
  408. raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
  409. if autostrip:
  410. ret = (ret or '').strip()
  411. err = (err or '').strip()
  412. return ret, err
  413. def set_branch_config(branch, option, value, scope='local'):
  414. set_config('branch.%s.%s' % (branch, option), value, scope=scope)
  415. def set_config(option, value, scope='local'):
  416. run('config', '--' + scope, option, value)
  417. def squash_current_branch(header=None, merge_base=None):
  418. header = header or 'git squash commit.'
  419. merge_base = merge_base or get_or_create_merge_base(current_branch())
  420. log_msg = header + '\n'
  421. if log_msg:
  422. log_msg += '\n'
  423. log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
  424. run('reset', '--soft', merge_base)
  425. run('commit', '-a', '-F', '-', indata=log_msg)
  426. def tags(*args):
  427. return run('tag', *args).splitlines()
  428. def thaw():
  429. took_action = False
  430. for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
  431. msg = run('show', '--format=%f%b', '-s', 'HEAD')
  432. match = FREEZE_MATCHER.match(msg)
  433. if not match:
  434. if not took_action:
  435. return 'Nothing to thaw.'
  436. break
  437. run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
  438. took_action = True
  439. def topo_iter(branch_tree, top_down=True):
  440. """Generates (branch, parent) in topographical order for a branch tree.
  441. Given a tree:
  442. A1
  443. B1 B2
  444. C1 C2 C3
  445. D1
  446. branch_tree would look like: {
  447. 'D1': 'C3',
  448. 'C3': 'B2',
  449. 'B2': 'A1',
  450. 'C1': 'B1',
  451. 'C2': 'B1',
  452. 'B1': 'A1',
  453. }
  454. It is OK to have multiple 'root' nodes in your graph.
  455. if top_down is True, items are yielded from A->D. Otherwise they're yielded
  456. from D->A. Within a layer the branches will be yielded in sorted order.
  457. """
  458. branch_tree = branch_tree.copy()
  459. # TODO(iannucci): There is probably a more efficient way to do these.
  460. if top_down:
  461. while branch_tree:
  462. this_pass = [(b, p) for b, p in branch_tree.iteritems()
  463. if p not in branch_tree]
  464. assert this_pass, "Branch tree has cycles: %r" % branch_tree
  465. for branch, parent in sorted(this_pass):
  466. yield branch, parent
  467. del branch_tree[branch]
  468. else:
  469. parent_to_branches = collections.defaultdict(set)
  470. for branch, parent in branch_tree.iteritems():
  471. parent_to_branches[parent].add(branch)
  472. while branch_tree:
  473. this_pass = [(b, p) for b, p in branch_tree.iteritems()
  474. if not parent_to_branches[b]]
  475. assert this_pass, "Branch tree has cycles: %r" % branch_tree
  476. for branch, parent in sorted(this_pass):
  477. yield branch, parent
  478. parent_to_branches[parent].discard(branch)
  479. del branch_tree[branch]
  480. def tree(treeref, recurse=False):
  481. """Returns a dict representation of a git tree object.
  482. Args:
  483. treeref (str) - a git ref which resolves to a tree (commits count as trees).
  484. recurse (bool) - include all of the tree's decendants too. File names will
  485. take the form of 'some/path/to/file'.
  486. Return format:
  487. { 'file_name': (mode, type, ref) }
  488. mode is an integer where:
  489. * 0040000 - Directory
  490. * 0100644 - Regular non-executable file
  491. * 0100664 - Regular non-executable group-writeable file
  492. * 0100755 - Regular executable file
  493. * 0120000 - Symbolic link
  494. * 0160000 - Gitlink
  495. type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
  496. ref is the hex encoded hash of the entry.
  497. """
  498. ret = {}
  499. opts = ['ls-tree', '--full-tree']
  500. if recurse:
  501. opts.append('-r')
  502. opts.append(treeref)
  503. try:
  504. for line in run(*opts).splitlines():
  505. mode, typ, ref, name = line.split(None, 3)
  506. ret[name] = (mode, typ, ref)
  507. except subprocess2.CalledProcessError:
  508. return None
  509. return ret
  510. def upstream(branch):
  511. try:
  512. return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
  513. branch+'@{upstream}')
  514. except subprocess2.CalledProcessError:
  515. return None