blob: 2bcc139a8131901d475a39da1f2232c4f0f2c3a4 [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
29
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
412 def exists(self):
413 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
414
Ryan Tseng3beabd02017-03-15 13:57:58 -0700415 def supported_project(self):
416 """Returns true if this repo is known to have a bootstrap zip file."""
417 u = urlparse.urlparse(self.url)
418 return u.netloc in [
419 'chromium.googlesource.com',
420 'chrome-internal.googlesource.com']
421
szager@chromium.org66c8b852015-09-22 23:19:07 +0000422 def _preserve_fetchspec(self):
423 """Read and preserve remote.origin.fetch from an existing mirror.
424
425 This modifies self.fetch_specs.
426 """
427 if not self.exists():
428 return
429 try:
430 config_fetchspecs = subprocess.check_output(
431 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
432 cwd=self.mirror_path)
433 for fetchspec in config_fetchspecs.splitlines():
434 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
435 except subprocess.CalledProcessError:
436 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
437 'existing cache directory. You may need to manually edit '
438 '%s and "git cache fetch" again.'
439 % os.path.join(self.mirror_path, 'config'))
440
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000441 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
442 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000443 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
444 pack_files = []
445
446 if os.path.isdir(pack_dir):
447 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800448 self.print('%s has %d .pack files, re-bootstrapping if >%d' %
449 (self.mirror_path, len(pack_files), GC_AUTOPACKLIMIT))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000450
451 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000452 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000453 len(pack_files) > GC_AUTOPACKLIMIT)
454 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000455 if self.exists():
456 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
457 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000458 tempdir = tempfile.mkdtemp(
459 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
460 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
461 if bootstrapped:
462 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000463 gclient_utils.rmtree(self.mirror_path)
Ryan Tseng3beabd02017-03-15 13:57:58 -0700464 elif not self.exists() or not self.supported_project():
465 # Bootstrap failed due to either
466 # 1. No previous cache
467 # 2. Project doesn't have a bootstrap zip file
468 # Start with a bare git dir.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000469 self.RunGit(['init', '--bare'], cwd=tempdir)
470 else:
471 # Bootstrap failed, previous cache exists; warn and continue.
472 logging.warn(
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800473 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
474 'but failed. Continuing with non-optimized repository.'
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000475 % len(pack_files))
476 gclient_utils.rmtree(tempdir)
477 tempdir = None
478 else:
479 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
480 logging.warn(
481 'Shallow fetch requested, but repo cache already exists.')
482 return tempdir
483
484 def _fetch(self, rundir, verbose, depth):
485 self.config(rundir)
486 v = []
487 d = []
488 if verbose:
489 v = ['-v', '--progress']
490 if depth:
491 d = ['--depth', str(depth)]
492 fetch_cmd = ['fetch'] + v + d + ['origin']
493 fetch_specs = subprocess.check_output(
494 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
495 cwd=rundir).strip().splitlines()
496 for spec in fetch_specs:
497 try:
498 self.print('Fetching %s' % spec)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800499 with self.print_duration_of('fetch %s' % spec):
500 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000501 except subprocess.CalledProcessError:
502 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700503 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000504 logging.warn('Fetch of %s failed' % spec)
505
Vadim Shtayura08049e22017-10-11 00:14:52 +0000506 def populate(self, depth=None, shallow=False, bootstrap=False,
507 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000508 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000509 if shallow and not depth:
510 depth = 10000
511 gclient_utils.safe_makedirs(self.GetCachePath())
512
Vadim Shtayura08049e22017-10-11 00:14:52 +0000513 lockfile = Lockfile(self.mirror_path, lock_timeout)
514 if not ignore_lock:
515 lockfile.lock()
516
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000517 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000518 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000519 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000520 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000521 self._fetch(rundir, verbose, depth)
hinokadcd84042016-06-09 14:26:17 -0700522 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000523 # This is a major failure, we need to clean and force a bootstrap.
524 gclient_utils.rmtree(rundir)
525 self.print(GIT_CACHE_CORRUPT_MESSAGE)
526 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
527 assert tempdir
Andrii Shyshkalov82d8dcd2017-11-22 17:07:28 -0800528 self._fetch(tempdir, verbose, depth)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000529 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000530 if tempdir:
dnjb445ef52016-11-10 15:51:39 -0800531 if os.path.exists(self.mirror_path):
532 gclient_utils.rmtree(self.mirror_path)
dnj4625b5a2016-11-10 18:23:26 -0800533 self.Rename(tempdir, self.mirror_path)
Vadim Shtayura08049e22017-10-11 00:14:52 +0000534 if not ignore_lock:
535 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000536
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000537 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000538 # The files are named <git number>.zip
539 gen_number = subprocess.check_output(
540 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000541 # Run Garbage Collect to compress packfile.
542 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000543 # Creating a temp file and then deleting it ensures we can use this name.
544 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
545 os.remove(tmp_zipfile)
546 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
547 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000548 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
549 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000550 gsutil.call('cp', tmp_zipfile, dest_name)
551 os.remove(tmp_zipfile)
552
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000553 # Remove all other files in the same directory.
554 if prune:
555 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
556 for filename in ls_out.splitlines():
557 if filename == dest_name:
558 continue
559 gsutil.call('rm', filename)
560
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000561 @staticmethod
562 def DeleteTmpPackFiles(path):
563 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000564 if not os.path.isdir(pack_dir):
565 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000566 pack_files = [f for f in os.listdir(pack_dir) if
567 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
568 for f in pack_files:
569 f = os.path.join(pack_dir, f)
570 try:
571 os.remove(f)
572 logging.warn('Deleted stale temporary pack file %s' % f)
573 except OSError:
574 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000575
Vadim Shtayura08049e22017-10-11 00:14:52 +0000576 @classmethod
577 def BreakLocks(cls, path):
578 did_unlock = False
579 lf = Lockfile(path)
580 if lf.break_lock():
581 did_unlock = True
582 # Look for lock files that might have been left behind by an interrupted
583 # git process.
584 lf = os.path.join(path, 'config.lock')
585 if os.path.exists(lf):
586 os.remove(lf)
587 did_unlock = True
588 cls.DeleteTmpPackFiles(path)
589 return did_unlock
590
591 def unlock(self):
592 return self.BreakLocks(self.mirror_path)
593
594 @classmethod
595 def UnlockAll(cls):
596 cachepath = cls.GetCachePath()
597 if not cachepath:
598 return
599 dirlist = os.listdir(cachepath)
600 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
601 if os.path.isdir(os.path.join(cachepath, path))])
602 for dirent in dirlist:
603 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
604 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
605 elif (dirent.endswith('.lock') and
606 os.path.isfile(os.path.join(cachepath, dirent))):
607 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
608
609 unlocked_repos = []
610 for repo_dir in repo_dirs:
611 if cls.BreakLocks(repo_dir):
612 unlocked_repos.append(repo_dir)
613
614 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000615
agable@chromium.org5a306a22014-02-24 22:13:59 +0000616@subcommand.usage('[url of repo to check for caching]')
617def CMDexists(parser, args):
618 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000619 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000620 if not len(args) == 1:
621 parser.error('git cache exists only takes exactly one repo url.')
622 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000623 mirror = Mirror(url)
624 if mirror.exists():
625 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000626 return 0
627 return 1
628
629
hinoka@google.com563559c2014-04-02 00:36:24 +0000630@subcommand.usage('[url of repo to create a bootstrap zip file]')
631def CMDupdate_bootstrap(parser, args):
632 """Create and uploads a bootstrap tarball."""
633 # Lets just assert we can't do this on Windows.
634 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000635 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000636 return 1
637
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000638 parser.add_option('--prune', action='store_true',
639 help='Prune all other cached zipballs of the same repo.')
640
hinoka@google.com563559c2014-04-02 00:36:24 +0000641 # First, we need to ensure the cache is populated.
642 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000643 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000644 CMDpopulate(parser, populate_args)
645
646 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000647 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000648 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000649 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000650 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000651 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000652
653
agable@chromium.org5a306a22014-02-24 22:13:59 +0000654@subcommand.usage('[url of repo to add to or update in cache]')
655def CMDpopulate(parser, args):
656 """Ensure that the cache has all up-to-date objects for the given repo."""
657 parser.add_option('--depth', type='int',
658 help='Only cache DEPTH commits of history')
659 parser.add_option('--shallow', '-s', action='store_true',
660 help='Only cache 10000 commits of history')
661 parser.add_option('--ref', action='append',
662 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000663 parser.add_option('--no_bootstrap', '--no-bootstrap',
664 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000665 help='Don\'t bootstrap from Google Storage')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000666 parser.add_option('--ignore_locks', '--ignore-locks',
667 action='store_true',
668 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000669
agable@chromium.org5a306a22014-02-24 22:13:59 +0000670 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000671 if not len(args) == 1:
672 parser.error('git cache populate only takes exactly one repo url.')
673 url = args[0]
674
szager@chromium.org848fd492014-04-09 19:06:44 +0000675 mirror = Mirror(url, refs=options.ref)
676 kwargs = {
677 'verbose': options.verbose,
678 'shallow': options.shallow,
679 'bootstrap': not options.no_bootstrap,
Vadim Shtayura08049e22017-10-11 00:14:52 +0000680 'ignore_lock': options.ignore_locks,
681 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000682 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000683 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000684 kwargs['depth'] = options.depth
685 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000686
687
szager@chromium.orgf3145112014-08-07 21:02:36 +0000688@subcommand.usage('Fetch new commits into cache and current checkout')
689def CMDfetch(parser, args):
690 """Update mirror, and fetch in cwd."""
691 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000692 parser.add_option('--no_bootstrap', '--no-bootstrap',
693 action='store_true',
694 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000695 options, args = parser.parse_args(args)
696
697 # Figure out which remotes to fetch. This mimics the behavior of regular
698 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
699 # this will NOT try to traverse up the branching structure to find the
700 # ultimate remote to update.
701 remotes = []
702 if options.all:
703 assert not args, 'fatal: fetch --all does not take a repository argument'
704 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
705 elif args:
706 remotes = args
707 else:
708 current_branch = subprocess.check_output(
709 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
710 if current_branch != 'HEAD':
711 upstream = subprocess.check_output(
712 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
713 ).strip()
714 if upstream and upstream != '.':
715 remotes = [upstream]
716 if not remotes:
717 remotes = ['origin']
718
719 cachepath = Mirror.GetCachePath()
720 git_dir = os.path.abspath(subprocess.check_output(
721 [Mirror.git_exe, 'rev-parse', '--git-dir']))
722 git_dir = os.path.abspath(git_dir)
723 if git_dir.startswith(cachepath):
724 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000725 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000726 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000727 return 0
728 for remote in remotes:
729 remote_url = subprocess.check_output(
730 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
731 if remote_url.startswith(cachepath):
732 mirror = Mirror.FromPath(remote_url)
733 mirror.print = lambda *args: None
734 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000735 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000736 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000737 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
738 return 0
739
740
Vadim Shtayura08049e22017-10-11 00:14:52 +0000741@subcommand.usage('[url of repo to unlock, or -a|--all]')
742def CMDunlock(parser, args):
743 """Unlock one or all repos if their lock files are still around."""
744 parser.add_option('--force', '-f', action='store_true',
745 help='Actually perform the action')
746 parser.add_option('--all', '-a', action='store_true',
747 help='Unlock all repository caches')
748 options, args = parser.parse_args(args)
749 if len(args) > 1 or (len(args) == 0 and not options.all):
750 parser.error('git cache unlock takes exactly one repo url, or --all')
751
752 if not options.force:
753 cachepath = Mirror.GetCachePath()
754 lockfiles = [os.path.join(cachepath, path)
755 for path in os.listdir(cachepath)
756 if path.endswith('.lock') and os.path.isfile(path)]
757 parser.error('git cache unlock requires -f|--force to do anything. '
758 'Refusing to unlock the following repo caches: '
759 ', '.join(lockfiles))
760
761 unlocked_repos = []
762 if options.all:
763 unlocked_repos.extend(Mirror.UnlockAll())
764 else:
765 m = Mirror(args[0])
766 if m.unlock():
767 unlocked_repos.append(m.mirror_path)
768
769 if unlocked_repos:
770 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
771 unlocked_repos))
772
773
agable@chromium.org5a306a22014-02-24 22:13:59 +0000774class OptionParser(optparse.OptionParser):
775 """Wrapper class for OptionParser to handle global options."""
776
777 def __init__(self, *args, **kwargs):
778 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
779 self.add_option('-c', '--cache-dir',
780 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000781 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000782 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000783 self.add_option('-q', '--quiet', action='store_true',
784 help='Suppress all extraneous output')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000785 self.add_option('--timeout', type='int', default=0,
786 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000787
788 def parse_args(self, args=None, values=None):
789 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000790 if options.quiet:
791 options.verbose = 0
792
793 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
794 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000795
796 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000797 global_cache_dir = Mirror.GetCachePath()
798 except RuntimeError:
799 global_cache_dir = None
800 if options.cache_dir:
801 if global_cache_dir and (
802 os.path.abspath(options.cache_dir) !=
803 os.path.abspath(global_cache_dir)):
804 logging.warn('Overriding globally-configured cache directory.')
805 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000806
agable@chromium.org5a306a22014-02-24 22:13:59 +0000807 return options, args
808
809
810def main(argv):
811 dispatcher = subcommand.CommandDispatcher(__name__)
812 return dispatcher.execute(OptionParser(), argv)
813
814
815if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000816 try:
817 sys.exit(main(sys.argv[1:]))
818 except KeyboardInterrupt:
819 sys.stderr.write('interrupted\n')
820 sys.exit(1)