blob: f5edfb16717691c384fe8d2b7b04ae2f1a9bfcab [file] [log] [blame]
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +00001# Copyright 2014 The Chromium Authors. All rights reserved.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# Monkeypatch IMapIterator so that Ctrl-C can kill everything properly.
6# Derived from https://gist.github.com/aljungberg/626518
Raul Tambrec2f74c12019-03-19 05:55:53 +00007
8from __future__ import print_function
9
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000010import multiprocessing.pool
11from multiprocessing.pool import IMapIterator
12def wrapper(func):
13 def wrap(self, timeout=None):
14 return func(self, timeout=timeout or 1e100)
15 return wrap
16IMapIterator.next = wrapper(IMapIterator.next)
17IMapIterator.__next__ = IMapIterator.next
18# TODO(iannucci): Monkeypatch all other 'wait' methods too.
19
20
21import binascii
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000022import collections
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000023import contextlib
24import functools
25import logging
iannucci@chromium.org97345eb2014-03-13 07:55:15 +000026import os
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000027import re
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000028import setup_color
sammc@chromium.org900a33f2015-09-29 06:57:09 +000029import shutil
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000030import signal
31import sys
32import tempfile
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +000033import textwrap
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000034import threading
35
36import subprocess2
37
Raul Tambrec2f74c12019-03-19 05:55:53 +000038from io import BytesIO
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000039
agable02b3c982016-06-22 07:51:22 -070040
41ROOT = os.path.abspath(os.path.dirname(__file__))
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +000042IS_WIN = sys.platform == 'win32'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000043TEST_MODE = False
44
Dan Jacques209a6812017-07-12 11:40:20 -070045
46def win_find_git():
47 for elem in os.environ.get('PATH', '').split(os.pathsep):
48 for candidate in ('git.exe', 'git.bat'):
49 path = os.path.join(elem, candidate)
50 if os.path.isfile(path):
51 return path
52 raise ValueError('Could not find Git on PATH.')
53
54
55GIT_EXE = 'git' if not IS_WIN else win_find_git()
56
57
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000058FREEZE = 'FREEZE'
59FREEZE_SECTIONS = {
60 'indexed': 'soft',
61 'unindexed': 'mixed'
62}
63FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000064
65
Dan Jacques2f8b0c12017-04-05 12:57:21 -070066# NOTE: This list is DEPRECATED in favor of the Infra Git wrapper:
67# https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/tools/git
68#
69# New entries should be added to the Git wrapper, NOT to this list. "git_retry"
70# is, similarly, being deprecated in favor of the Git wrapper.
71#
72# ---
73#
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000074# Retry a git operation if git returns a error response with any of these
75# messages. It's all observed 'bad' GoB responses so far.
76#
77# This list is inspired/derived from the one in ChromiumOS's Chromite:
78# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
79#
80# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
81GIT_TRANSIENT_ERRORS = (
82 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000083 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000084
85 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000086 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000087
88 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000089 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000090
91 # crbug.com/285832
92 r'remote error: Internal Server Error',
93
94 # crbug.com/294449
95 r'fatal: Couldn\'t find remote ref ',
96
97 # crbug.com/220543
98 r'git fetch_pack: expected ACK/NAK, got',
99
100 # crbug.com/189455
101 r'protocol error: bad pack header',
102
103 # crbug.com/202807
104 r'The remote end hung up unexpectedly',
105
106 # crbug.com/298189
107 r'TLS packet with unexpected length was received',
108
109 # crbug.com/187444
110 r'RPC failed; result=\d+, HTTP code = \d+',
111
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000112 # crbug.com/388876
113 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +0000114
115 # crbug.com/430343
116 # TODO(dnj): Resync with Chromite.
117 r'The requested URL returned error: 5\d+',
Arikonb3a21482016-07-22 10:12:24 -0700118
119 r'Connection reset by peer',
120
121 r'Unable to look up',
122
123 r'Couldn\'t resolve host',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000124)
125
126GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
127 re.IGNORECASE)
128
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +0000129# git's for-each-ref command first supported the upstream:track token in its
130# format string in version 1.9.0, but some usages were broken until 2.3.0.
131# See git commit b6160d95 for more information.
132MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000133
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000134class BadCommitRefException(Exception):
135 def __init__(self, refs):
136 msg = ('one of %s does not seem to be a valid commitref.' %
137 str(refs))
138 super(BadCommitRefException, self).__init__(msg)
139
140
141def memoize_one(**kwargs):
142 """Memoizes a single-argument pure function.
143
144 Values of None are not cached.
145
146 Kwargs:
147 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
148 cache manipulation functions. This is a kwarg so that users of memoize_one
149 are forced to explicitly and verbosely pick True or False.
150
151 Adds three methods to the decorated function:
152 * get(key, default=None) - Gets the value for this key from the cache.
153 * set(key, value) - Sets the value for this key from the cache.
154 * clear() - Drops the entire contents of the cache. Useful for unittests.
155 * update(other) - Updates the contents of the cache from another dict.
156 """
157 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
158 threadsafe = kwargs['threadsafe']
159
160 if threadsafe:
161 def withlock(lock, f):
162 def inner(*args, **kwargs):
163 with lock:
164 return f(*args, **kwargs)
165 return inner
166 else:
167 def withlock(_lock, f):
168 return f
169
170 def decorator(f):
171 # Instantiate the lock in decorator, in case users of memoize_one do:
172 #
173 # memoizer = memoize_one(threadsafe=True)
174 #
175 # @memoizer
176 # def fn1(val): ...
177 #
178 # @memoizer
179 # def fn2(val): ...
180
181 lock = threading.Lock() if threadsafe else None
182 cache = {}
183 _get = withlock(lock, cache.get)
184 _set = withlock(lock, cache.__setitem__)
185
186 @functools.wraps(f)
187 def inner(arg):
188 ret = _get(arg)
189 if ret is None:
190 ret = f(arg)
191 if ret is not None:
192 _set(arg, ret)
193 return ret
194 inner.get = _get
195 inner.set = _set
196 inner.clear = withlock(lock, cache.clear)
197 inner.update = withlock(lock, cache.update)
198 return inner
199 return decorator
200
201
202def _ScopedPool_initer(orig, orig_args): # pragma: no cover
203 """Initializer method for ScopedPool's subprocesses.
204
205 This helps ScopedPool handle Ctrl-C's correctly.
206 """
207 signal.signal(signal.SIGINT, signal.SIG_IGN)
208 if orig:
209 orig(*orig_args)
210
211
212@contextlib.contextmanager
213def ScopedPool(*args, **kwargs):
214 """Context Manager which returns a multiprocessing.pool instance which
215 correctly deals with thrown exceptions.
216
217 *args - Arguments to multiprocessing.pool
218
219 Kwargs:
220 kind ('threads', 'procs') - The type of underlying coprocess to use.
221 **etc - Arguments to multiprocessing.pool
222 """
223 if kwargs.pop('kind', None) == 'threads':
224 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
225 else:
226 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
227 kwargs['initializer'] = _ScopedPool_initer
228 kwargs['initargs'] = orig, orig_args
229 pool = multiprocessing.pool.Pool(*args, **kwargs)
230
231 try:
232 yield pool
233 pool.close()
234 except:
235 pool.terminate()
236 raise
237 finally:
238 pool.join()
239
240
241class ProgressPrinter(object):
242 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000243 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000244 """Create a ProgressPrinter.
245
246 Use it as a context manager which produces a simple 'increment' method:
247
248 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
249 for i in xrange(1000):
250 # do stuff
251 if i % 10 == 0:
252 inc(10)
253
254 Args:
255 fmt - String format with a single '%(count)d' where the counter value
256 should go.
257 enabled (bool) - If this is None, will default to True if
258 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000259 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000260 period (float) - The time in seconds for the printer thread to wait
261 between printing.
262 """
263 self.fmt = fmt
264 if enabled is None: # pragma: no cover
265 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
266 else:
267 self.enabled = enabled
268
269 self._count = 0
270 self._dead = False
271 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000272 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000273 self._thread = threading.Thread(target=self._run)
274 self._period = period
275
276 def _emit(self, s):
277 if self.enabled:
278 self._stream.write('\r' + s)
279 self._stream.flush()
280
281 def _run(self):
282 with self._dead_cond:
283 while not self._dead:
284 self._emit(self.fmt % {'count': self._count})
285 self._dead_cond.wait(self._period)
286 self._emit((self.fmt + '\n') % {'count': self._count})
287
288 def inc(self, amount=1):
289 self._count += amount
290
291 def __enter__(self):
292 self._thread.start()
293 return self.inc
294
295 def __exit__(self, _exc_type, _exc_value, _traceback):
296 self._dead = True
297 with self._dead_cond:
298 self._dead_cond.notifyAll()
299 self._thread.join()
300 del self._thread
301
302
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000303def once(function):
304 """@Decorates |function| so that it only performs its action once, no matter
305 how many times the decorated |function| is called."""
306 def _inner_gen():
307 yield function()
308 while True:
309 yield
310 return _inner_gen().next
311
312
313## Git functions
314
agable7aa2ddd2016-06-21 07:47:00 -0700315def die(message, *args):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000316 print(textwrap.dedent(message % args), file=sys.stderr)
agable7aa2ddd2016-06-21 07:47:00 -0700317 sys.exit(1)
318
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000319
Mark Mentovaif548d082017-03-08 13:32:00 -0500320def blame(filename, revision=None, porcelain=False, abbrev=None, *_args):
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000321 command = ['blame']
322 if porcelain:
323 command.append('-p')
324 if revision is not None:
325 command.append(revision)
Mark Mentovaif548d082017-03-08 13:32:00 -0500326 if abbrev is not None:
327 command.append('--abbrev=%d' % abbrev)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000328 command.extend(['--', filename])
329 return run(*command)
330
331
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000332def branch_config(branch, option, default=None):
agable7aa2ddd2016-06-21 07:47:00 -0700333 return get_config('branch.%s.%s' % (branch, option), default=default)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000334
335
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000336def branch_config_map(option):
337 """Return {branch: <|option| value>} for all branches."""
338 try:
339 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
agable7aa2ddd2016-06-21 07:47:00 -0700340 lines = get_config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000341 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
342 except subprocess2.CalledProcessError:
343 return {}
344
345
Francois Dorayd42c6812017-05-30 15:10:20 -0400346def branches(use_limit=True, *args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000347 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000348
349 key = 'depot-tools.branch-limit'
agable7aa2ddd2016-06-21 07:47:00 -0700350 limit = get_config_int(key, 20)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000351
352 raw_branches = run('branch', *args).splitlines()
353
354 num = len(raw_branches)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000355
Francois Dorayd42c6812017-05-30 15:10:20 -0400356 if use_limit and num > limit:
agable7aa2ddd2016-06-21 07:47:00 -0700357 die("""\
358 Your git repo has too many branches (%d/%d) for this tool to work well.
359
360 You may adjust this limit by running:
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000361 git config %s <new_limit>
agable7aa2ddd2016-06-21 07:47:00 -0700362
363 You may also try cleaning up your old branches by running:
364 git cl archive
365 """, num, limit, key)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000366
367 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000368 if line.startswith(NO_BRANCH):
369 continue
370 yield line.split()[-1]
371
372
agable7aa2ddd2016-06-21 07:47:00 -0700373def get_config(option, default=None):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000374 try:
375 return run('config', '--get', option) or default
376 except subprocess2.CalledProcessError:
377 return default
378
379
agable7aa2ddd2016-06-21 07:47:00 -0700380def get_config_int(option, default=0):
381 assert isinstance(default, int)
382 try:
383 return int(get_config(option, default))
384 except ValueError:
385 return default
386
387
388def get_config_list(option):
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000389 try:
390 return run('config', '--get-all', option).split()
391 except subprocess2.CalledProcessError:
392 return []
393
394
agable7aa2ddd2016-06-21 07:47:00 -0700395def get_config_regexp(pattern):
396 if IS_WIN: # pragma: no cover
397 # this madness is because we call git.bat which calls git.exe which calls
398 # bash.exe (or something to that effect). Each layer divides the number of
399 # ^'s by 2.
400 pattern = pattern.replace('^', '^' * 8)
401 return run('config', '--get-regexp', pattern).splitlines()
402
403
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000404def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000405 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000406 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000407 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000408 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000409
410
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000411def del_branch_config(branch, option, scope='local'):
412 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000413
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000414
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000415def del_config(option, scope='local'):
416 try:
417 run('config', '--' + scope, '--unset', option)
418 except subprocess2.CalledProcessError:
419 pass
420
421
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000422def diff(oldrev, newrev, *args):
423 return run('diff', oldrev, newrev, *args)
424
425
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000426def freeze():
427 took_action = False
agable02b3c982016-06-22 07:51:22 -0700428 key = 'depot-tools.freeze-size-limit'
429 MB = 2**20
430 limit_mb = get_config_int(key, 100)
431 untracked_bytes = 0
432
iannuccieaca0332016-08-03 16:46:50 -0700433 root_path = repo_root()
434
agable02b3c982016-06-22 07:51:22 -0700435 for f, s in status():
436 if is_unmerged(s):
437 die("Cannot freeze unmerged changes!")
438 if limit_mb > 0:
439 if s.lstat == '?':
iannuccieaca0332016-08-03 16:46:50 -0700440 untracked_bytes += os.stat(os.path.join(root_path, f)).st_size
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800441 if limit_mb > 0 and untracked_bytes > limit_mb * MB:
442 die("""\
443 You appear to have too much untracked+unignored data in your git
444 checkout: %.1f / %d MB.
agable02b3c982016-06-22 07:51:22 -0700445
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800446 Run `git status` to see what it is.
agable02b3c982016-06-22 07:51:22 -0700447
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800448 In addition to making many git commands slower, this will prevent
449 depot_tools from freezing your in-progress changes.
agable02b3c982016-06-22 07:51:22 -0700450
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800451 You should add untracked data that you want to ignore to your repo's
452 .git/info/exclude
453 file. See `git help ignore` for the format of this file.
agable02b3c982016-06-22 07:51:22 -0700454
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800455 If this data is indended as part of your commit, you may adjust the
456 freeze limit by running:
457 git config %s <new_limit>
458 Where <new_limit> is an integer threshold in megabytes.""",
459 untracked_bytes / (MB * 1.0), limit_mb, key)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000460
461 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000462 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000463 took_action = True
464 except subprocess2.CalledProcessError:
465 pass
466
agable96e179b2016-06-24 10:32:51 -0700467 add_errors = False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000468 try:
agable96e179b2016-06-24 10:32:51 -0700469 run('add', '-A', '--ignore-errors')
470 except subprocess2.CalledProcessError:
471 add_errors = True
472
473 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000474 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000475 took_action = True
476 except subprocess2.CalledProcessError:
477 pass
478
agable96e179b2016-06-24 10:32:51 -0700479 ret = []
480 if add_errors:
481 ret.append('Failed to index some unindexed files.')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000482 if not took_action:
agable96e179b2016-06-24 10:32:51 -0700483 ret.append('Nothing to freeze.')
484 return ' '.join(ret) or None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000485
486
487def get_branch_tree():
488 """Get the dictionary of {branch: parent}, compatible with topo_iter.
489
490 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
491 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000492 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000493 skipped = set()
494 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000495
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000496 for branch in branches():
497 parent = upstream(branch)
498 if not parent:
499 skipped.add(branch)
500 continue
501 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000502
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000503 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000504
505
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000506def get_or_create_merge_base(branch, parent=None):
507 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000508
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000509 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000510 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000511 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000512 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000513 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000514 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000515 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000516 actual_merge_base = run('merge-base', parent, branch)
517
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000518 if base_upstream != parent:
519 base = None
520 base_upstream = None
521
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000522 def is_ancestor(a, b):
523 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
524
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000525 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000526 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000527 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
528 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000529 elif is_ancestor(base, actual_merge_base):
530 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
531 base = None
532 else:
533 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000534
535 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000536 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000537 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000538
539 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000540
541
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000542def hash_multi(*reflike):
543 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000544
545
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000546def hash_one(reflike, short=False):
547 args = ['rev-parse', reflike]
548 if short:
549 args.insert(1, '--short')
550 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000551
552
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000553def in_rebase():
554 git_dir = run('rev-parse', '--git-dir')
555 return (
556 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
557 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000558
559
560def intern_f(f, kind='blob'):
561 """Interns a file object into the git object store.
562
563 Args:
564 f (file-like object) - The file-like object to intern
565 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
566
567 Returns the git hash of the interned object (hex encoded).
568 """
569 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
570 f.close()
571 return ret
572
573
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000574def is_dormant(branch):
575 # TODO(iannucci): Do an oldness check?
576 return branch_config(branch, 'dormant', 'false') != 'false'
577
578
agable02b3c982016-06-22 07:51:22 -0700579def is_unmerged(stat_value):
580 return (
581 'U' in (stat_value.lstat, stat_value.rstat) or
582 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD')
583 )
584
585
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000586def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000587 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000588 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000589
590
591def mktree(treedict):
592 """Makes a git tree object and returns its hash.
593
594 See |tree()| for the values of mode, type, and ref.
595
596 Args:
597 treedict - { name: (mode, type, ref) }
598 """
599 with tempfile.TemporaryFile() as f:
600 for name, (mode, typ, ref) in treedict.iteritems():
601 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
602 f.seek(0)
603 return run('mktree', '-z', stdin=f)
604
605
606def parse_commitrefs(*commitrefs):
607 """Returns binary encoded commit hashes for one or more commitrefs.
608
609 A commitref is anything which can resolve to a commit. Popular examples:
610 * 'HEAD'
611 * 'origin/master'
612 * 'cool_branch~2'
613 """
614 try:
615 return map(binascii.unhexlify, hash_multi(*commitrefs))
616 except subprocess2.CalledProcessError:
617 raise BadCommitRefException(commitrefs)
618
619
sbc@chromium.org384039b2014-10-13 21:01:00 +0000620RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000621
622
623def rebase(parent, start, branch, abort=False):
624 """Rebases |start|..|branch| onto the branch |parent|.
625
626 Args:
627 parent - The new parent ref for the rebased commits.
628 start - The commit to start from
629 branch - The branch to rebase
630 abort - If True, will call git-rebase --abort in the event that the rebase
631 doesn't complete successfully.
632
633 Returns a namedtuple with fields:
634 success - a boolean indicating that the rebase command completed
635 successfully.
636 message - if the rebase failed, this contains the stdout of the failed
637 rebase.
638 """
639 try:
640 args = ['--onto', parent, start, branch]
641 if TEST_MODE:
642 args.insert(0, '--committer-date-is-author-date')
643 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000644 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000645 except subprocess2.CalledProcessError as cpe:
646 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000647 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000648 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000649
650
651def remove_merge_base(branch):
652 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000653 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000654
655
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000656def repo_root():
657 """Returns the absolute path to the repository root."""
658 return run('rev-parse', '--show-toplevel')
659
660
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000661def root():
agable7aa2ddd2016-06-21 07:47:00 -0700662 return get_config('depot-tools.upstream', 'origin/master')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000663
664
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000665@contextlib.contextmanager
666def less(): # pragma: no cover
667 """Runs 'less' as context manager yielding its stdin as a PIPE.
668
669 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
670 running less and just yields sys.stdout.
671 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000672 if not setup_color.IS_TTY:
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000673 yield sys.stdout
674 return
675
676 # Run with the same options that git uses (see setup_pager in git repo).
677 # -F: Automatically quit if the output is less than one screen.
678 # -R: Don't escape ANSI color codes.
679 # -X: Don't clear the screen before starting.
680 cmd = ('less', '-FRX')
681 try:
682 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
683 yield proc.stdin
684 finally:
685 proc.stdin.close()
686 proc.wait()
687
688
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000689def run(*cmd, **kwargs):
690 """The same as run_with_stderr, except it only returns stdout."""
691 return run_with_stderr(*cmd, **kwargs)[0]
692
693
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000694def run_with_retcode(*cmd, **kwargs):
695 """Run a command but only return the status code."""
696 try:
697 run(*cmd, **kwargs)
698 return 0
699 except subprocess2.CalledProcessError as cpe:
700 return cpe.returncode
701
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000702def run_stream(*cmd, **kwargs):
703 """Runs a git command. Returns stdout as a PIPE (file-like object).
704
705 stderr is dropped to avoid races if the process outputs to both stdout and
706 stderr.
707 """
708 kwargs.setdefault('stderr', subprocess2.VOID)
709 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000710 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000711 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000712 proc = subprocess2.Popen(cmd, **kwargs)
713 return proc.stdout
714
715
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000716@contextlib.contextmanager
717def run_stream_with_retcode(*cmd, **kwargs):
718 """Runs a git command as context manager yielding stdout as a PIPE.
719
720 stderr is dropped to avoid races if the process outputs to both stdout and
721 stderr.
722
723 Raises subprocess2.CalledProcessError on nonzero return code.
724 """
725 kwargs.setdefault('stderr', subprocess2.VOID)
726 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000727 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000728 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
729 try:
730 proc = subprocess2.Popen(cmd, **kwargs)
731 yield proc.stdout
732 finally:
733 retcode = proc.wait()
734 if retcode != 0:
735 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
736 None, None)
737
738
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000739def run_with_stderr(*cmd, **kwargs):
740 """Runs a git command.
741
742 Returns (stdout, stderr) as a pair of strings.
743
744 kwargs
745 autostrip (bool) - Strip the output. Defaults to True.
746 indata (str) - Specifies stdin data for the process.
747 """
748 kwargs.setdefault('stdin', subprocess2.PIPE)
749 kwargs.setdefault('stdout', subprocess2.PIPE)
750 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000751 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000752 autostrip = kwargs.pop('autostrip', True)
753 indata = kwargs.pop('indata', None)
754
iannucci@chromium.org21980022014-04-11 04:51:49 +0000755 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000756 proc = subprocess2.Popen(cmd, **kwargs)
757 ret, err = proc.communicate(indata)
758 retcode = proc.wait()
759 if retcode != 0:
760 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
761
762 if autostrip:
763 ret = (ret or '').strip()
764 err = (err or '').strip()
765
766 return ret, err
767
768
769def set_branch_config(branch, option, value, scope='local'):
770 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
771
772
773def set_config(option, value, scope='local'):
774 run('config', '--' + scope, option, value)
775
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000776
sbc@chromium.org71437c02015-04-09 19:29:40 +0000777def get_dirty_files():
778 # Make sure index is up-to-date before running diff-index.
779 run_with_retcode('update-index', '--refresh', '-q')
780 return run('diff-index', '--name-status', 'HEAD')
781
782
783def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700784 w = lambda s: sys.stderr.write(s+"\n")
785
sbc@chromium.org71437c02015-04-09 19:29:40 +0000786 dirty = get_dirty_files()
787 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700788 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
789 % cmd)
790 w('Uncommitted files: (git diff-index --name-status HEAD)')
791 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000792 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700793 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000794 return True
795 return False
796
797
agable02b3c982016-06-22 07:51:22 -0700798def status():
799 """Returns a parsed version of git-status.
800
801 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
802 * current_name is the name of the file
803 * lstat is the left status code letter from git-status
804 * rstat is the left status code letter from git-status
805 * src is the current name of the file, or the original name of the file
806 if lstat == 'R'
807 """
808 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
809
810 def tokenizer(stream):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000811 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700812 c = None
813 while c != '':
814 c = stream.read(1)
815 if c in (None, '', '\0'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000816 if len(acc.getvalue()):
agable02b3c982016-06-22 07:51:22 -0700817 yield acc.getvalue()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000818 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700819 else:
820 acc.write(c)
821
822 def parser(tokens):
823 while True:
824 # Raises StopIteration if it runs out of tokens.
825 status_dest = next(tokens)
826 stat, dest = status_dest[:2], status_dest[3:]
827 lstat, rstat = stat
828 if lstat == 'R':
829 src = next(tokens)
830 else:
831 src = dest
832 yield (dest, stat_entry(lstat, rstat, src))
833
834 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
835
836
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000837def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100838 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000839 merge_base = merge_base or get_or_create_merge_base(current_branch())
840 log_msg = header + '\n'
841 if log_msg:
842 log_msg += '\n'
843 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
844 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000845
846 if not get_dirty_files():
847 # Sometimes the squash can result in the same tree, meaning that there is
848 # nothing to commit at this point.
Raul Tambrec2f74c12019-03-19 05:55:53 +0000849 print('Nothing to commit; squashed branch is empty')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000850 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000851 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000852 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000853
854
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000855def tags(*args):
856 return run('tag', *args).splitlines()
857
858
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000859def thaw():
860 took_action = False
861 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
862 msg = run('show', '--format=%f%b', '-s', 'HEAD')
863 match = FREEZE_MATCHER.match(msg)
864 if not match:
865 if not took_action:
866 return 'Nothing to thaw.'
867 break
868
869 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
870 took_action = True
871
872
873def topo_iter(branch_tree, top_down=True):
874 """Generates (branch, parent) in topographical order for a branch tree.
875
876 Given a tree:
877
878 A1
879 B1 B2
880 C1 C2 C3
881 D1
882
883 branch_tree would look like: {
884 'D1': 'C3',
885 'C3': 'B2',
886 'B2': 'A1',
887 'C1': 'B1',
888 'C2': 'B1',
889 'B1': 'A1',
890 }
891
892 It is OK to have multiple 'root' nodes in your graph.
893
894 if top_down is True, items are yielded from A->D. Otherwise they're yielded
895 from D->A. Within a layer the branches will be yielded in sorted order.
896 """
897 branch_tree = branch_tree.copy()
898
899 # TODO(iannucci): There is probably a more efficient way to do these.
900 if top_down:
901 while branch_tree:
902 this_pass = [(b, p) for b, p in branch_tree.iteritems()
903 if p not in branch_tree]
904 assert this_pass, "Branch tree has cycles: %r" % branch_tree
905 for branch, parent in sorted(this_pass):
906 yield branch, parent
907 del branch_tree[branch]
908 else:
909 parent_to_branches = collections.defaultdict(set)
910 for branch, parent in branch_tree.iteritems():
911 parent_to_branches[parent].add(branch)
912
913 while branch_tree:
914 this_pass = [(b, p) for b, p in branch_tree.iteritems()
915 if not parent_to_branches[b]]
916 assert this_pass, "Branch tree has cycles: %r" % branch_tree
917 for branch, parent in sorted(this_pass):
918 yield branch, parent
919 parent_to_branches[parent].discard(branch)
920 del branch_tree[branch]
921
922
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000923def tree(treeref, recurse=False):
924 """Returns a dict representation of a git tree object.
925
926 Args:
927 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700928 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000929 take the form of 'some/path/to/file'.
930
931 Return format:
932 { 'file_name': (mode, type, ref) }
933
934 mode is an integer where:
935 * 0040000 - Directory
936 * 0100644 - Regular non-executable file
937 * 0100664 - Regular non-executable group-writeable file
938 * 0100755 - Regular executable file
939 * 0120000 - Symbolic link
940 * 0160000 - Gitlink
941
942 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
943
944 ref is the hex encoded hash of the entry.
945 """
946 ret = {}
947 opts = ['ls-tree', '--full-tree']
948 if recurse:
949 opts.append('-r')
950 opts.append(treeref)
951 try:
952 for line in run(*opts).splitlines():
953 mode, typ, ref, name = line.split(None, 3)
954 ret[name] = (mode, typ, ref)
955 except subprocess2.CalledProcessError:
956 return None
957 return ret
958
959
Mun Yong Jang781e71e2017-10-25 15:46:20 -0700960def get_remote_url(remote='origin'):
961 try:
962 return run('config', 'remote.%s.url' % remote)
963 except subprocess2.CalledProcessError:
964 return None
965
966
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000967def upstream(branch):
968 try:
969 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
970 branch+'@{upstream}')
971 except subprocess2.CalledProcessError:
972 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000973
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000974
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000975def get_git_version():
976 """Returns a tuple that contains the numeric components of the current git
977 version."""
978 version_string = run('--version')
979 version_match = re.search(r'(\d+.)+(\d+)', version_string)
980 version = version_match.group() if version_match else ''
981
982 return tuple(int(x) for x in version.split('.'))
983
984
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000985def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000986 format_string = (
987 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
988
989 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000990 if (include_tracking_status and
991 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000992 format_string += '%(upstream:track)'
993
994 info_map = {}
995 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000996 BranchesInfo = collections.namedtuple(
997 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000998 for line in data.splitlines():
999 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
1000
1001 ahead_match = re.search(r'ahead (\d+)', tracking_status)
1002 ahead = int(ahead_match.group(1)) if ahead_match else None
1003
1004 behind_match = re.search(r'behind (\d+)', tracking_status)
1005 behind = int(behind_match.group(1)) if behind_match else None
1006
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001007 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001008 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
1009
1010 # Set None for upstreams which are not branches (e.g empty upstream, remotes
1011 # and deleted upstream branches).
1012 missing_upstreams = {}
1013 for info in info_map.values():
1014 if info.upstream not in info_map and info.upstream not in missing_upstreams:
1015 missing_upstreams[info.upstream] = None
1016
1017 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001018
1019
1020def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001021 files_to_copy, symlink=None):
1022 if not symlink:
1023 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001024 os.makedirs(new_workdir)
1025 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001026 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001027 for entry in files_to_copy:
1028 clone_file(repository, new_workdir, entry, shutil.copy)
1029
1030
1031def make_workdir(repository, new_workdir):
1032 GIT_DIRECTORY_WHITELIST = [
1033 'config',
1034 'info',
1035 'hooks',
1036 'logs/refs',
1037 'objects',
1038 'packed-refs',
1039 'refs',
1040 'remotes',
1041 'rr-cache',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001042 ]
1043 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1044 ['HEAD'])
1045
1046
1047def clone_file(repository, new_workdir, link, operation):
1048 if not os.path.exists(os.path.join(repository, link)):
1049 return
1050 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1051 if not os.path.exists(link_dir):
1052 os.makedirs(link_dir)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001053 src = os.path.join(repository, link)
1054 if os.path.islink(src):
Henrique Ferreiroaea45d22018-02-19 09:48:36 +01001055 src = os.path.realpath(src)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001056 operation(src, os.path.join(new_workdir, link))