blob: f3d5626c958b738e43d6e2ca9392c364b881dc0f [file] [log] [blame]
agable@chromium.org5a306a22014-02-24 22:13:59 +00001#!/usr/bin/env python
2# Copyright 2014 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""A git command for managing a local cache of git repositories."""
7
szager@chromium.org848fd492014-04-09 19:06:44 +00008from __future__ import print_function
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -08009import contextlib
agable@chromium.org5a306a22014-02-24 22:13:59 +000010import errno
11import logging
12import optparse
13import os
szager@chromium.org174766f2014-05-13 21:27:46 +000014import re
agable@chromium.org5a306a22014-02-24 22:13:59 +000015import tempfile
szager@chromium.org1132f5f2014-08-23 01:57:59 +000016import threading
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000017import time
agable@chromium.org5a306a22014-02-24 22:13:59 +000018import subprocess
19import sys
20import urlparse
hinoka@google.com776a2c32014-04-25 07:54:25 +000021import zipfile
agable@chromium.org5a306a22014-02-24 22:13:59 +000022
hinoka@google.com563559c2014-04-02 00:36:24 +000023from download_from_google_storage import Gsutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000024import gclient_utils
25import subcommand
26
szager@chromium.org301a7c32014-06-16 17:13:50 +000027# Analogous to gc.autopacklimit git config.
28GC_AUTOPACKLIMIT = 50
Takuto Ikuta9fce2132017-12-14 10:44:28 +090029
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000030GIT_CACHE_CORRUPT_MESSAGE = 'WARNING: The Git cache is corrupt.'
31
szager@chromium.org848fd492014-04-09 19:06:44 +000032try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080033 # pylint: disable=undefined-variable
szager@chromium.org848fd492014-04-09 19:06:44 +000034 WinErr = WindowsError
35except NameError:
36 class WinErr(Exception):
37 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000038
Vadim Shtayura08049e22017-10-11 00:14:52 +000039class LockError(Exception):
40 pass
41
hinokadcd84042016-06-09 14:26:17 -070042class ClobberNeeded(Exception):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000043 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000044
dnj4625b5a2016-11-10 18:23:26 -080045
46def exponential_backoff_retry(fn, excs=(Exception,), name=None, count=10,
47 sleep_time=0.25, printerr=None):
48 """Executes |fn| up to |count| times, backing off exponentially.
49
50 Args:
51 fn (callable): The function to execute. If this raises a handled
52 exception, the function will retry with exponential backoff.
53 excs (tuple): A tuple of Exception types to handle. If one of these is
54 raised by |fn|, a retry will be attempted. If |fn| raises an Exception
55 that is not in this list, it will immediately pass through. If |excs|
56 is empty, the Exception base class will be used.
57 name (str): Optional operation name to print in the retry string.
58 count (int): The number of times to try before allowing the exception to
59 pass through.
60 sleep_time (float): The initial number of seconds to sleep in between
61 retries. This will be doubled each retry.
62 printerr (callable): Function that will be called with the error string upon
63 failures. If None, |logging.warning| will be used.
64
65 Returns: The return value of the successful fn.
66 """
67 printerr = printerr or logging.warning
68 for i in xrange(count):
69 try:
70 return fn()
71 except excs as e:
72 if (i+1) >= count:
73 raise
74
75 printerr('Retrying %s in %.2f second(s) (%d / %d attempts): %s' % (
76 (name or 'operation'), sleep_time, (i+1), count, e))
77 time.sleep(sleep_time)
78 sleep_time *= 2
79
80
Vadim Shtayura08049e22017-10-11 00:14:52 +000081class Lockfile(object):
82 """Class to represent a cross-platform process-specific lockfile."""
83
84 def __init__(self, path, timeout=0):
85 self.path = os.path.abspath(path)
86 self.timeout = timeout
87 self.lockfile = self.path + ".lock"
88 self.pid = os.getpid()
89
90 def _read_pid(self):
91 """Read the pid stored in the lockfile.
92
93 Note: This method is potentially racy. By the time it returns the lockfile
94 may have been unlocked, removed, or stolen by some other process.
95 """
96 try:
97 with open(self.lockfile, 'r') as f:
98 pid = int(f.readline().strip())
99 except (IOError, ValueError):
100 pid = None
101 return pid
102
103 def _make_lockfile(self):
104 """Safely creates a lockfile containing the current pid."""
105 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
106 fd = os.open(self.lockfile, open_flags, 0o644)
107 f = os.fdopen(fd, 'w')
108 print(self.pid, file=f)
109 f.close()
110
111 def _remove_lockfile(self):
112 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
113
114 See gclient_utils.py:rmtree docstring for more explanation on the
115 windows case.
116 """
117 if sys.platform == 'win32':
118 lockfile = os.path.normcase(self.lockfile)
119
120 def delete():
121 exitcode = subprocess.call(['cmd.exe', '/c',
122 'del', '/f', '/q', lockfile])
123 if exitcode != 0:
124 raise LockError('Failed to remove lock: %s' % (lockfile,))
125 exponential_backoff_retry(
126 delete,
127 excs=(LockError,),
128 name='del [%s]' % (lockfile,))
129 else:
130 os.remove(self.lockfile)
131
132 def lock(self):
133 """Acquire the lock.
134
135 This will block with a deadline of self.timeout seconds.
136 """
137 elapsed = 0
138 while True:
139 try:
140 self._make_lockfile()
141 return
142 except OSError as e:
143 if elapsed < self.timeout:
144 sleep_time = max(10, min(3, self.timeout - elapsed))
145 logging.info('Could not create git cache lockfile; '
146 'will retry after sleep(%d).', sleep_time);
147 elapsed += sleep_time
148 time.sleep(sleep_time)
149 continue
150 if e.errno == errno.EEXIST:
151 raise LockError("%s is already locked" % self.path)
152 else:
153 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
154
155 def unlock(self):
156 """Release the lock."""
157 try:
158 if not self.is_locked():
159 raise LockError("%s is not locked" % self.path)
160 if not self.i_am_locking():
161 raise LockError("%s is locked, but not by me" % self.path)
162 self._remove_lockfile()
163 except WinErr:
164 # Windows is unreliable when it comes to file locking. YMMV.
165 pass
166
167 def break_lock(self):
168 """Remove the lock, even if it was created by someone else."""
169 try:
170 self._remove_lockfile()
171 return True
172 except OSError as exc:
173 if exc.errno == errno.ENOENT:
174 return False
175 else:
176 raise
177
178 def is_locked(self):
179 """Test if the file is locked by anyone.
180
181 Note: This method is potentially racy. By the time it returns the lockfile
182 may have been unlocked, removed, or stolen by some other process.
183 """
184 return os.path.exists(self.lockfile)
185
186 def i_am_locking(self):
187 """Test if the file is locked by this process."""
188 return self.is_locked() and self.pid == self._read_pid()
189
190
szager@chromium.org848fd492014-04-09 19:06:44 +0000191class Mirror(object):
192
193 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
194 gsutil_exe = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +0000195 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000196 cachepath_lock = threading.Lock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000197
szager@chromium.org66c8b852015-09-22 23:19:07 +0000198 @staticmethod
199 def parse_fetch_spec(spec):
200 """Parses and canonicalizes a fetch spec.
201
202 Returns (fetchspec, value_regex), where value_regex can be used
203 with 'git config --replace-all'.
204 """
205 parts = spec.split(':', 1)
206 src = parts[0].lstrip('+').rstrip('/')
207 if not src.startswith('refs/'):
208 src = 'refs/heads/%s' % src
209 dest = parts[1].rstrip('/') if len(parts) > 1 else src
210 regex = r'\+%s:.*' % src.replace('*', r'\*')
211 return ('+%s:%s' % (src, dest), regex)
212
szager@chromium.org848fd492014-04-09 19:06:44 +0000213 def __init__(self, url, refs=None, print_func=None):
214 self.url = url
szager@chromium.org66c8b852015-09-22 23:19:07 +0000215 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
szager@chromium.org848fd492014-04-09 19:06:44 +0000216 self.basedir = self.UrlToCacheDir(url)
217 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000218 if print_func:
219 self.print = self.print_without_file
220 self.print_func = print_func
221 else:
222 self.print = print
223
dnj4625b5a2016-11-10 18:23:26 -0800224 def print_without_file(self, message, **_kwargs):
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000225 self.print_func(message)
szager@chromium.org848fd492014-04-09 19:06:44 +0000226
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800227 @contextlib.contextmanager
228 def print_duration_of(self, what):
229 start = time.time()
230 try:
231 yield
232 finally:
233 self.print('%s took %.1f minutes' % (what, (time.time() - start) / 60.0))
234
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000235 @property
236 def bootstrap_bucket(self):
Ryan Tseng3beabd02017-03-15 13:57:58 -0700237 u = urlparse.urlparse(self.url)
238 if u.netloc == 'chromium.googlesource.com':
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000239 return 'chromium-git-cache'
Ryan Tseng3beabd02017-03-15 13:57:58 -0700240 elif u.netloc == 'chrome-internal.googlesource.com':
241 return 'chrome-git-cache'
242 # Not recognized.
243 return None
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000244
szager@chromium.org174766f2014-05-13 21:27:46 +0000245 @classmethod
246 def FromPath(cls, path):
247 return cls(cls.CacheDirToUrl(path))
248
szager@chromium.org848fd492014-04-09 19:06:44 +0000249 @staticmethod
250 def UrlToCacheDir(url):
251 """Convert a git url to a normalized form for the cache dir path."""
252 parsed = urlparse.urlparse(url)
253 norm_url = parsed.netloc + parsed.path
254 if norm_url.endswith('.git'):
255 norm_url = norm_url[:-len('.git')]
256 return norm_url.replace('-', '--').replace('/', '-').lower()
257
258 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000259 def CacheDirToUrl(path):
260 """Convert a cache dir path to its corresponding url."""
261 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
262 return 'https://%s' % netpath
263
szager@chromium.org848fd492014-04-09 19:06:44 +0000264 @classmethod
265 def SetCachePath(cls, cachepath):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000266 with cls.cachepath_lock:
267 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000268
269 @classmethod
270 def GetCachePath(cls):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000271 with cls.cachepath_lock:
272 if not hasattr(cls, 'cachepath'):
273 try:
274 cachepath = subprocess.check_output(
275 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
276 except subprocess.CalledProcessError:
277 cachepath = None
278 if not cachepath:
279 raise RuntimeError(
280 'No global cache.cachepath git configuration found.')
281 setattr(cls, 'cachepath', cachepath)
282 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000283
dnj4625b5a2016-11-10 18:23:26 -0800284 def Rename(self, src, dst):
285 # This is somehow racy on Windows.
286 # Catching OSError because WindowsError isn't portable and
287 # pylint complains.
288 exponential_backoff_retry(
289 lambda: os.rename(src, dst),
290 excs=(OSError,),
291 name='rename [%s] => [%s]' % (src, dst),
292 printerr=self.print)
293
szager@chromium.org848fd492014-04-09 19:06:44 +0000294 def RunGit(self, cmd, **kwargs):
295 """Run git in a subprocess."""
296 cwd = kwargs.setdefault('cwd', self.mirror_path)
297 kwargs.setdefault('print_stdout', False)
298 kwargs.setdefault('filter_fn', self.print)
299 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
300 env.setdefault('GIT_ASKPASS', 'true')
301 env.setdefault('SSH_ASKPASS', 'true')
302 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
303 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
304
305 def config(self, cwd=None):
306 if cwd is None:
307 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000308
309 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinokadcd84042016-06-09 14:26:17 -0700310 try:
311 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
312 except subprocess.CalledProcessError:
313 # Hard error, need to clobber.
314 raise ClobberNeeded()
szager@chromium.org301a7c32014-06-16 17:13:50 +0000315
316 # Don't combine pack files into one big pack file. It's really slow for
317 # repositories, and there's no way to track progress and make sure it's
318 # not stuck.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700319 if self.supported_project():
320 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000321
322 # Allocate more RAM for cache-ing delta chains, for better performance
323 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000324 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000325 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000326
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000327 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000328 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000329 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000330 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000331 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000332 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000333 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000334
335 def bootstrap_repo(self, directory):
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800336 """Bootstrap the repo from Google Storage if possible.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000337
338 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
339 """
Ryan Tseng3beabd02017-03-15 13:57:58 -0700340 if not self.bootstrap_bucket:
341 return False
hinoka@google.com776a2c32014-04-25 07:54:25 +0000342 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000343 if (sys.platform.startswith('win') and
344 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000345 python_fallback = True
346 elif sys.platform.startswith('darwin'):
347 # The OSX version of unzip doesn't support zip64.
348 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000349 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000350 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000351
352 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000353 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000354 # Get the most recent version of the zipfile.
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800355 _, ls_out, ls_err = gsutil.check_call('ls', gs_folder)
szager@chromium.org848fd492014-04-09 19:06:44 +0000356 ls_out_sorted = sorted(ls_out.splitlines())
357 if not ls_out_sorted:
358 # This repo is not on Google Storage.
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800359 self.print('No bootstrap file for %s found in %s, stderr:\n %s' %
360 (self.mirror_path, self.bootstrap_bucket,
361 ' '.join((ls_err or '').splitlines(True))))
szager@chromium.org848fd492014-04-09 19:06:44 +0000362 return False
363 latest_checkout = ls_out_sorted[-1]
364
365 # Download zip file to a temporary directory.
366 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000367 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000368 self.print('Downloading %s' % latest_checkout)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800369 with self.print_duration_of('download'):
370 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000371 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000372 return False
373 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
374
hinoka@google.com776a2c32014-04-25 07:54:25 +0000375 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800376 with self.print_duration_of('unzip'):
377 if not python_fallback:
378 if sys.platform.startswith('win'):
379 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
380 else:
381 cmd = ['unzip', filename, '-d', directory]
382 retcode = subprocess.call(cmd)
hinoka@google.com776a2c32014-04-25 07:54:25 +0000383 else:
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800384 try:
385 with zipfile.ZipFile(filename, 'r') as f:
386 f.printdir()
387 f.extractall(directory)
388 except Exception as e:
389 self.print('Encountered error: %s' % str(e), file=sys.stderr)
390 retcode = 1
391 else:
392 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000393 finally:
394 # Clean up the downloaded zipfile.
dnj4625b5a2016-11-10 18:23:26 -0800395 #
396 # This is somehow racy on Windows.
397 # Catching OSError because WindowsError isn't portable and
398 # pylint complains.
399 exponential_backoff_retry(
400 lambda: gclient_utils.rm_file_or_tree(tempdir),
401 excs=(OSError,),
402 name='rmtree [%s]' % (tempdir,),
403 printerr=self.print)
szager@chromium.org848fd492014-04-09 19:06:44 +0000404
405 if retcode:
406 self.print(
407 'Extracting bootstrap zipfile %s failed.\n'
408 'Resuming normal operations.' % filename)
409 return False
410 return True
411
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800412 def contains_revision(self, revision):
413 if not self.exists():
414 return False
415
416 if sys.platform.startswith('win'):
417 # Windows .bat scripts use ^ as escape sequence, which means we have to
418 # escape it with itself for every .bat invocation.
419 needle = '%s^^^^{commit}' % revision
420 else:
421 needle = '%s^{commit}' % revision
422 try:
423 # cat-file exits with 0 on success, that is git object of given hash was
424 # found.
425 self.RunGit(['cat-file', '-e', needle])
426 return True
427 except subprocess.CalledProcessError:
428 return False
429
szager@chromium.org848fd492014-04-09 19:06:44 +0000430 def exists(self):
431 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
432
Ryan Tseng3beabd02017-03-15 13:57:58 -0700433 def supported_project(self):
434 """Returns true if this repo is known to have a bootstrap zip file."""
435 u = urlparse.urlparse(self.url)
436 return u.netloc in [
437 'chromium.googlesource.com',
438 'chrome-internal.googlesource.com']
439
szager@chromium.org66c8b852015-09-22 23:19:07 +0000440 def _preserve_fetchspec(self):
441 """Read and preserve remote.origin.fetch from an existing mirror.
442
443 This modifies self.fetch_specs.
444 """
445 if not self.exists():
446 return
447 try:
448 config_fetchspecs = subprocess.check_output(
449 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
450 cwd=self.mirror_path)
451 for fetchspec in config_fetchspecs.splitlines():
452 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
453 except subprocess.CalledProcessError:
454 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
455 'existing cache directory. You may need to manually edit '
456 '%s and "git cache fetch" again.'
457 % os.path.join(self.mirror_path, 'config'))
458
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000459 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
460 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000461 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
462 pack_files = []
463
464 if os.path.isdir(pack_dir):
465 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800466 self.print('%s has %d .pack files, re-bootstrapping if >%d' %
467 (self.mirror_path, len(pack_files), GC_AUTOPACKLIMIT))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000468
469 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000470 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000471 len(pack_files) > GC_AUTOPACKLIMIT)
472 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000473 if self.exists():
474 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
475 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000476 tempdir = tempfile.mkdtemp(
477 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
478 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
479 if bootstrapped:
480 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000481 gclient_utils.rmtree(self.mirror_path)
Ryan Tseng3beabd02017-03-15 13:57:58 -0700482 elif not self.exists() or not self.supported_project():
483 # Bootstrap failed due to either
484 # 1. No previous cache
485 # 2. Project doesn't have a bootstrap zip file
486 # Start with a bare git dir.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000487 self.RunGit(['init', '--bare'], cwd=tempdir)
488 else:
489 # Bootstrap failed, previous cache exists; warn and continue.
490 logging.warn(
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800491 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
492 'but failed. Continuing with non-optimized repository.'
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000493 % len(pack_files))
494 gclient_utils.rmtree(tempdir)
495 tempdir = None
496 else:
497 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
498 logging.warn(
499 'Shallow fetch requested, but repo cache already exists.')
500 return tempdir
501
502 def _fetch(self, rundir, verbose, depth):
503 self.config(rundir)
504 v = []
505 d = []
506 if verbose:
507 v = ['-v', '--progress']
508 if depth:
509 d = ['--depth', str(depth)]
510 fetch_cmd = ['fetch'] + v + d + ['origin']
511 fetch_specs = subprocess.check_output(
512 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
513 cwd=rundir).strip().splitlines()
514 for spec in fetch_specs:
515 try:
516 self.print('Fetching %s' % spec)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800517 with self.print_duration_of('fetch %s' % spec):
518 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000519 except subprocess.CalledProcessError:
520 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700521 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000522 logging.warn('Fetch of %s failed' % spec)
523
Vadim Shtayura08049e22017-10-11 00:14:52 +0000524 def populate(self, depth=None, shallow=False, bootstrap=False,
525 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000526 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000527 if shallow and not depth:
528 depth = 10000
529 gclient_utils.safe_makedirs(self.GetCachePath())
530
Vadim Shtayura08049e22017-10-11 00:14:52 +0000531 lockfile = Lockfile(self.mirror_path, lock_timeout)
532 if not ignore_lock:
533 lockfile.lock()
534
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000535 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000536 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000537 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000538 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000539 self._fetch(rundir, verbose, depth)
hinokadcd84042016-06-09 14:26:17 -0700540 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000541 # This is a major failure, we need to clean and force a bootstrap.
542 gclient_utils.rmtree(rundir)
543 self.print(GIT_CACHE_CORRUPT_MESSAGE)
544 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
545 assert tempdir
Andrii Shyshkalov82d8dcd2017-11-22 17:07:28 -0800546 self._fetch(tempdir, verbose, depth)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000547 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000548 if tempdir:
dnjb445ef52016-11-10 15:51:39 -0800549 if os.path.exists(self.mirror_path):
550 gclient_utils.rmtree(self.mirror_path)
dnj4625b5a2016-11-10 18:23:26 -0800551 self.Rename(tempdir, self.mirror_path)
Vadim Shtayura08049e22017-10-11 00:14:52 +0000552 if not ignore_lock:
553 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000554
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000555 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000556 # The files are named <git number>.zip
557 gen_number = subprocess.check_output(
558 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000559 # Run Garbage Collect to compress packfile.
560 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000561 # Creating a temp file and then deleting it ensures we can use this name.
562 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
563 os.remove(tmp_zipfile)
564 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
565 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000566 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
567 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000568 gsutil.call('cp', tmp_zipfile, dest_name)
569 os.remove(tmp_zipfile)
570
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000571 # Remove all other files in the same directory.
572 if prune:
573 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
574 for filename in ls_out.splitlines():
575 if filename == dest_name:
576 continue
577 gsutil.call('rm', filename)
578
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000579 @staticmethod
580 def DeleteTmpPackFiles(path):
581 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000582 if not os.path.isdir(pack_dir):
583 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000584 pack_files = [f for f in os.listdir(pack_dir) if
585 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
586 for f in pack_files:
587 f = os.path.join(pack_dir, f)
588 try:
589 os.remove(f)
590 logging.warn('Deleted stale temporary pack file %s' % f)
591 except OSError:
592 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000593
Vadim Shtayura08049e22017-10-11 00:14:52 +0000594 @classmethod
595 def BreakLocks(cls, path):
596 did_unlock = False
597 lf = Lockfile(path)
598 if lf.break_lock():
599 did_unlock = True
600 # Look for lock files that might have been left behind by an interrupted
601 # git process.
602 lf = os.path.join(path, 'config.lock')
603 if os.path.exists(lf):
604 os.remove(lf)
605 did_unlock = True
606 cls.DeleteTmpPackFiles(path)
607 return did_unlock
608
609 def unlock(self):
610 return self.BreakLocks(self.mirror_path)
611
612 @classmethod
613 def UnlockAll(cls):
614 cachepath = cls.GetCachePath()
615 if not cachepath:
616 return
617 dirlist = os.listdir(cachepath)
618 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
619 if os.path.isdir(os.path.join(cachepath, path))])
620 for dirent in dirlist:
621 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
622 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
623 elif (dirent.endswith('.lock') and
624 os.path.isfile(os.path.join(cachepath, dirent))):
625 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
626
627 unlocked_repos = []
628 for repo_dir in repo_dirs:
629 if cls.BreakLocks(repo_dir):
630 unlocked_repos.append(repo_dir)
631
632 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000633
agable@chromium.org5a306a22014-02-24 22:13:59 +0000634@subcommand.usage('[url of repo to check for caching]')
635def CMDexists(parser, args):
636 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000637 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000638 if not len(args) == 1:
639 parser.error('git cache exists only takes exactly one repo url.')
640 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000641 mirror = Mirror(url)
642 if mirror.exists():
643 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000644 return 0
645 return 1
646
647
hinoka@google.com563559c2014-04-02 00:36:24 +0000648@subcommand.usage('[url of repo to create a bootstrap zip file]')
649def CMDupdate_bootstrap(parser, args):
650 """Create and uploads a bootstrap tarball."""
651 # Lets just assert we can't do this on Windows.
652 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000653 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000654 return 1
655
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000656 parser.add_option('--prune', action='store_true',
657 help='Prune all other cached zipballs of the same repo.')
658
hinoka@google.com563559c2014-04-02 00:36:24 +0000659 # First, we need to ensure the cache is populated.
660 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000661 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000662 CMDpopulate(parser, populate_args)
663
664 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000665 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000666 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000667 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000668 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000669 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000670
671
agable@chromium.org5a306a22014-02-24 22:13:59 +0000672@subcommand.usage('[url of repo to add to or update in cache]')
673def CMDpopulate(parser, args):
674 """Ensure that the cache has all up-to-date objects for the given repo."""
675 parser.add_option('--depth', type='int',
676 help='Only cache DEPTH commits of history')
677 parser.add_option('--shallow', '-s', action='store_true',
678 help='Only cache 10000 commits of history')
679 parser.add_option('--ref', action='append',
680 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000681 parser.add_option('--no_bootstrap', '--no-bootstrap',
682 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000683 help='Don\'t bootstrap from Google Storage')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000684 parser.add_option('--ignore_locks', '--ignore-locks',
685 action='store_true',
686 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000687
agable@chromium.org5a306a22014-02-24 22:13:59 +0000688 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000689 if not len(args) == 1:
690 parser.error('git cache populate only takes exactly one repo url.')
691 url = args[0]
692
szager@chromium.org848fd492014-04-09 19:06:44 +0000693 mirror = Mirror(url, refs=options.ref)
694 kwargs = {
695 'verbose': options.verbose,
696 'shallow': options.shallow,
697 'bootstrap': not options.no_bootstrap,
Vadim Shtayura08049e22017-10-11 00:14:52 +0000698 'ignore_lock': options.ignore_locks,
699 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000700 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000701 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000702 kwargs['depth'] = options.depth
703 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000704
705
szager@chromium.orgf3145112014-08-07 21:02:36 +0000706@subcommand.usage('Fetch new commits into cache and current checkout')
707def CMDfetch(parser, args):
708 """Update mirror, and fetch in cwd."""
709 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000710 parser.add_option('--no_bootstrap', '--no-bootstrap',
711 action='store_true',
712 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000713 options, args = parser.parse_args(args)
714
715 # Figure out which remotes to fetch. This mimics the behavior of regular
716 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
717 # this will NOT try to traverse up the branching structure to find the
718 # ultimate remote to update.
719 remotes = []
720 if options.all:
721 assert not args, 'fatal: fetch --all does not take a repository argument'
722 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
723 elif args:
724 remotes = args
725 else:
726 current_branch = subprocess.check_output(
727 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
728 if current_branch != 'HEAD':
729 upstream = subprocess.check_output(
730 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
731 ).strip()
732 if upstream and upstream != '.':
733 remotes = [upstream]
734 if not remotes:
735 remotes = ['origin']
736
737 cachepath = Mirror.GetCachePath()
738 git_dir = os.path.abspath(subprocess.check_output(
739 [Mirror.git_exe, 'rev-parse', '--git-dir']))
740 git_dir = os.path.abspath(git_dir)
741 if git_dir.startswith(cachepath):
742 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000743 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000744 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000745 return 0
746 for remote in remotes:
747 remote_url = subprocess.check_output(
748 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
749 if remote_url.startswith(cachepath):
750 mirror = Mirror.FromPath(remote_url)
751 mirror.print = lambda *args: None
752 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000753 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000754 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000755 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
756 return 0
757
758
Vadim Shtayura08049e22017-10-11 00:14:52 +0000759@subcommand.usage('[url of repo to unlock, or -a|--all]')
760def CMDunlock(parser, args):
761 """Unlock one or all repos if their lock files are still around."""
762 parser.add_option('--force', '-f', action='store_true',
763 help='Actually perform the action')
764 parser.add_option('--all', '-a', action='store_true',
765 help='Unlock all repository caches')
766 options, args = parser.parse_args(args)
767 if len(args) > 1 or (len(args) == 0 and not options.all):
768 parser.error('git cache unlock takes exactly one repo url, or --all')
769
770 if not options.force:
771 cachepath = Mirror.GetCachePath()
772 lockfiles = [os.path.join(cachepath, path)
773 for path in os.listdir(cachepath)
774 if path.endswith('.lock') and os.path.isfile(path)]
775 parser.error('git cache unlock requires -f|--force to do anything. '
776 'Refusing to unlock the following repo caches: '
777 ', '.join(lockfiles))
778
779 unlocked_repos = []
780 if options.all:
781 unlocked_repos.extend(Mirror.UnlockAll())
782 else:
783 m = Mirror(args[0])
784 if m.unlock():
785 unlocked_repos.append(m.mirror_path)
786
787 if unlocked_repos:
788 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
789 unlocked_repos))
790
791
agable@chromium.org5a306a22014-02-24 22:13:59 +0000792class OptionParser(optparse.OptionParser):
793 """Wrapper class for OptionParser to handle global options."""
794
795 def __init__(self, *args, **kwargs):
796 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
797 self.add_option('-c', '--cache-dir',
798 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000799 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000800 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000801 self.add_option('-q', '--quiet', action='store_true',
802 help='Suppress all extraneous output')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000803 self.add_option('--timeout', type='int', default=0,
804 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000805
806 def parse_args(self, args=None, values=None):
807 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000808 if options.quiet:
809 options.verbose = 0
810
811 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
812 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000813
814 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000815 global_cache_dir = Mirror.GetCachePath()
816 except RuntimeError:
817 global_cache_dir = None
818 if options.cache_dir:
819 if global_cache_dir and (
820 os.path.abspath(options.cache_dir) !=
821 os.path.abspath(global_cache_dir)):
822 logging.warn('Overriding globally-configured cache directory.')
823 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000824
agable@chromium.org5a306a22014-02-24 22:13:59 +0000825 return options, args
826
827
828def main(argv):
829 dispatcher = subcommand.CommandDispatcher(__name__)
830 return dispatcher.execute(OptionParser(), argv)
831
832
833if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000834 try:
835 sys.exit(main(sys.argv[1:]))
836 except KeyboardInterrupt:
837 sys.stderr.write('interrupted\n')
838 sys.exit(1)