blob: bae45e71c2cca189e944b3809151100383bd5032 [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
agable@chromium.org5a306a22014-02-24 22:13:59 +00009import errno
10import logging
11import optparse
12import os
szager@chromium.org174766f2014-05-13 21:27:46 +000013import re
agable@chromium.org5a306a22014-02-24 22:13:59 +000014import tempfile
szager@chromium.org1132f5f2014-08-23 01:57:59 +000015import threading
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000016import time
agable@chromium.org5a306a22014-02-24 22:13:59 +000017import subprocess
18import sys
19import urlparse
hinoka@google.com776a2c32014-04-25 07:54:25 +000020import zipfile
agable@chromium.org5a306a22014-02-24 22:13:59 +000021
hinoka@google.com563559c2014-04-02 00:36:24 +000022from download_from_google_storage import Gsutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000023import gclient_utils
24import subcommand
25
szager@chromium.org301a7c32014-06-16 17:13:50 +000026# Analogous to gc.autopacklimit git config.
27GC_AUTOPACKLIMIT = 50
28
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000029GIT_CACHE_CORRUPT_MESSAGE = 'WARNING: The Git cache is corrupt.'
30
szager@chromium.org848fd492014-04-09 19:06:44 +000031try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080032 # pylint: disable=undefined-variable
szager@chromium.org848fd492014-04-09 19:06:44 +000033 WinErr = WindowsError
34except NameError:
35 class WinErr(Exception):
36 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000037
38class LockError(Exception):
39 pass
40
hinokadcd84042016-06-09 14:26:17 -070041class ClobberNeeded(Exception):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000042 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000043
dnj4625b5a2016-11-10 18:23:26 -080044
45def exponential_backoff_retry(fn, excs=(Exception,), name=None, count=10,
46 sleep_time=0.25, printerr=None):
47 """Executes |fn| up to |count| times, backing off exponentially.
48
49 Args:
50 fn (callable): The function to execute. If this raises a handled
51 exception, the function will retry with exponential backoff.
52 excs (tuple): A tuple of Exception types to handle. If one of these is
53 raised by |fn|, a retry will be attempted. If |fn| raises an Exception
54 that is not in this list, it will immediately pass through. If |excs|
55 is empty, the Exception base class will be used.
56 name (str): Optional operation name to print in the retry string.
57 count (int): The number of times to try before allowing the exception to
58 pass through.
59 sleep_time (float): The initial number of seconds to sleep in between
60 retries. This will be doubled each retry.
61 printerr (callable): Function that will be called with the error string upon
62 failures. If None, |logging.warning| will be used.
63
64 Returns: The return value of the successful fn.
65 """
66 printerr = printerr or logging.warning
67 for i in xrange(count):
68 try:
69 return fn()
70 except excs as e:
71 if (i+1) >= count:
72 raise
73
74 printerr('Retrying %s in %.2f second(s) (%d / %d attempts): %s' % (
75 (name or 'operation'), sleep_time, (i+1), count, e))
76 time.sleep(sleep_time)
77 sleep_time *= 2
78
79
agable@chromium.org5a306a22014-02-24 22:13:59 +000080class Lockfile(object):
81 """Class to represent a cross-platform process-specific lockfile."""
82
szager@chromium.orgdbb6f822016-02-02 22:59:30 +000083 def __init__(self, path, timeout=0):
agable@chromium.org5a306a22014-02-24 22:13:59 +000084 self.path = os.path.abspath(path)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +000085 self.timeout = timeout
agable@chromium.org5a306a22014-02-24 22:13:59 +000086 self.lockfile = self.path + ".lock"
87 self.pid = os.getpid()
88
89 def _read_pid(self):
90 """Read the pid stored in the lockfile.
91
92 Note: This method is potentially racy. By the time it returns the lockfile
93 may have been unlocked, removed, or stolen by some other process.
94 """
95 try:
96 with open(self.lockfile, 'r') as f:
97 pid = int(f.readline().strip())
98 except (IOError, ValueError):
99 pid = None
100 return pid
101
102 def _make_lockfile(self):
103 """Safely creates a lockfile containing the current pid."""
104 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
105 fd = os.open(self.lockfile, open_flags, 0o644)
106 f = os.fdopen(fd, 'w')
szager@chromium.org848fd492014-04-09 19:06:44 +0000107 print(self.pid, file=f)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000108 f.close()
109
110 def _remove_lockfile(self):
pgervais@chromium.orgf3726102014-04-17 17:24:15 +0000111 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
112
113 See gclient_utils.py:rmtree docstring for more explanation on the
114 windows case.
115 """
116 if sys.platform == 'win32':
117 lockfile = os.path.normcase(self.lockfile)
dnj4625b5a2016-11-10 18:23:26 -0800118
119 def delete():
pgervais@chromium.orgf3726102014-04-17 17:24:15 +0000120 exitcode = subprocess.call(['cmd.exe', '/c',
121 'del', '/f', '/q', lockfile])
dnj4625b5a2016-11-10 18:23:26 -0800122 if exitcode != 0:
123 raise LockError('Failed to remove lock: %s' % (lockfile,))
124 exponential_backoff_retry(
125 delete,
126 excs=(LockError,),
127 name='del [%s]' % (lockfile,))
pgervais@chromium.orgf3726102014-04-17 17:24:15 +0000128 else:
129 os.remove(self.lockfile)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000130
131 def lock(self):
132 """Acquire the lock.
133
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000134 This will block with a deadline of self.timeout seconds.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000135 """
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000136 elapsed = 0
137 while True:
138 try:
139 self._make_lockfile()
140 return
141 except OSError as e:
142 if elapsed < self.timeout:
nodir@chromium.org5b48e482016-03-18 20:27:54 +0000143 sleep_time = max(10, min(3, self.timeout - elapsed))
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000144 logging.info('Could not create git cache lockfile; '
145 'will retry after sleep(%d).', sleep_time);
146 elapsed += sleep_time
147 time.sleep(sleep_time)
148 continue
149 if e.errno == errno.EEXIST:
150 raise LockError("%s is already locked" % self.path)
151 else:
152 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000153
154 def unlock(self):
155 """Release the lock."""
szager@chromium.org108eced2014-06-19 21:22:43 +0000156 try:
157 if not self.is_locked():
158 raise LockError("%s is not locked" % self.path)
159 if not self.i_am_locking():
160 raise LockError("%s is locked, but not by me" % self.path)
161 self._remove_lockfile()
162 except WinErr:
163 # Windows is unreliable when it comes to file locking. YMMV.
164 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +0000165
166 def break_lock(self):
167 """Remove the lock, even if it was created by someone else."""
168 try:
169 self._remove_lockfile()
170 return True
171 except OSError as exc:
172 if exc.errno == errno.ENOENT:
173 return False
174 else:
175 raise
176
177 def is_locked(self):
178 """Test if the file is locked by anyone.
179
180 Note: This method is potentially racy. By the time it returns the lockfile
181 may have been unlocked, removed, or stolen by some other process.
182 """
183 return os.path.exists(self.lockfile)
184
185 def i_am_locking(self):
186 """Test if the file is locked by this process."""
187 return self.is_locked() and self.pid == self._read_pid()
188
agable@chromium.org5a306a22014-02-24 22:13:59 +0000189
szager@chromium.org848fd492014-04-09 19:06:44 +0000190class Mirror(object):
191
192 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
193 gsutil_exe = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +0000194 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
szager@chromium.org1132f5f2014-08-23 01:57:59 +0000195 cachepath_lock = threading.Lock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000196
szager@chromium.org66c8b852015-09-22 23:19:07 +0000197 @staticmethod
198 def parse_fetch_spec(spec):
199 """Parses and canonicalizes a fetch spec.
200
201 Returns (fetchspec, value_regex), where value_regex can be used
202 with 'git config --replace-all'.
203 """
204 parts = spec.split(':', 1)
205 src = parts[0].lstrip('+').rstrip('/')
206 if not src.startswith('refs/'):
207 src = 'refs/heads/%s' % src
208 dest = parts[1].rstrip('/') if len(parts) > 1 else src
209 regex = r'\+%s:.*' % src.replace('*', r'\*')
210 return ('+%s:%s' % (src, dest), regex)
211
szager@chromium.org848fd492014-04-09 19:06:44 +0000212 def __init__(self, url, refs=None, print_func=None):
213 self.url = url
szager@chromium.org66c8b852015-09-22 23:19:07 +0000214 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
szager@chromium.org848fd492014-04-09 19:06:44 +0000215 self.basedir = self.UrlToCacheDir(url)
216 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000217 if print_func:
218 self.print = self.print_without_file
219 self.print_func = print_func
220 else:
221 self.print = print
222
dnj4625b5a2016-11-10 18:23:26 -0800223 def print_without_file(self, message, **_kwargs):
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000224 self.print_func(message)
szager@chromium.org848fd492014-04-09 19:06:44 +0000225
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000226 @property
227 def bootstrap_bucket(self):
228 if 'chrome-internal' in self.url:
229 return 'chrome-git-cache'
230 else:
231 return 'chromium-git-cache'
232
szager@chromium.org174766f2014-05-13 21:27:46 +0000233 @classmethod
234 def FromPath(cls, path):
235 return cls(cls.CacheDirToUrl(path))
236
szager@chromium.org848fd492014-04-09 19:06:44 +0000237 @staticmethod
238 def UrlToCacheDir(url):
239 """Convert a git url to a normalized form for the cache dir path."""
240 parsed = urlparse.urlparse(url)
241 norm_url = parsed.netloc + parsed.path
242 if norm_url.endswith('.git'):
243 norm_url = norm_url[:-len('.git')]
244 return norm_url.replace('-', '--').replace('/', '-').lower()
245
246 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000247 def CacheDirToUrl(path):
248 """Convert a cache dir path to its corresponding url."""
249 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
250 return 'https://%s' % netpath
251
szager@chromium.org848fd492014-04-09 19:06:44 +0000252 @classmethod
253 def SetCachePath(cls, cachepath):
szager@chromium.org84c56002014-08-23 03:33:28 +0000254 with cls.cachepath_lock:
255 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000256
257 @classmethod
258 def GetCachePath(cls):
szager@chromium.org84c56002014-08-23 03:33:28 +0000259 with cls.cachepath_lock:
260 if not hasattr(cls, 'cachepath'):
261 try:
262 cachepath = subprocess.check_output(
263 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
264 except subprocess.CalledProcessError:
265 cachepath = None
266 if not cachepath:
267 raise RuntimeError(
268 'No global cache.cachepath git configuration found.')
269 setattr(cls, 'cachepath', cachepath)
270 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000271
dnj4625b5a2016-11-10 18:23:26 -0800272 def Rename(self, src, dst):
273 # This is somehow racy on Windows.
274 # Catching OSError because WindowsError isn't portable and
275 # pylint complains.
276 exponential_backoff_retry(
277 lambda: os.rename(src, dst),
278 excs=(OSError,),
279 name='rename [%s] => [%s]' % (src, dst),
280 printerr=self.print)
281
szager@chromium.org848fd492014-04-09 19:06:44 +0000282 def RunGit(self, cmd, **kwargs):
283 """Run git in a subprocess."""
284 cwd = kwargs.setdefault('cwd', self.mirror_path)
285 kwargs.setdefault('print_stdout', False)
286 kwargs.setdefault('filter_fn', self.print)
287 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
288 env.setdefault('GIT_ASKPASS', 'true')
289 env.setdefault('SSH_ASKPASS', 'true')
290 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
291 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
292
293 def config(self, cwd=None):
294 if cwd is None:
295 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000296
297 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinokadcd84042016-06-09 14:26:17 -0700298 try:
299 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
300 except subprocess.CalledProcessError:
301 # Hard error, need to clobber.
302 raise ClobberNeeded()
szager@chromium.org301a7c32014-06-16 17:13:50 +0000303
304 # Don't combine pack files into one big pack file. It's really slow for
305 # repositories, and there's no way to track progress and make sure it's
306 # not stuck.
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000307 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000308
309 # Allocate more RAM for cache-ing delta chains, for better performance
310 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000311 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000312 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000313
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000314 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000315 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000316 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000317 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000318 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000319 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000320 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000321
322 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000323 """Bootstrap the repo from Google Stroage if possible.
324
325 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
326 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000327
hinoka@google.com776a2c32014-04-25 07:54:25 +0000328 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000329 if (sys.platform.startswith('win') and
330 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000331 python_fallback = True
332 elif sys.platform.startswith('darwin'):
333 # The OSX version of unzip doesn't support zip64.
334 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000335 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000336 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000337
338 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000339 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000340 # Get the most recent version of the zipfile.
341 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
342 ls_out_sorted = sorted(ls_out.splitlines())
343 if not ls_out_sorted:
344 # This repo is not on Google Storage.
345 return False
346 latest_checkout = ls_out_sorted[-1]
347
348 # Download zip file to a temporary directory.
349 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000350 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000351 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000352 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000353 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000354 return False
355 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
356
hinoka@google.com776a2c32014-04-25 07:54:25 +0000357 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
358 if not python_fallback:
359 if sys.platform.startswith('win'):
360 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
361 else:
362 cmd = ['unzip', filename, '-d', directory]
363 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000364 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000365 try:
366 with zipfile.ZipFile(filename, 'r') as f:
367 f.printdir()
368 f.extractall(directory)
369 except Exception as e:
370 self.print('Encountered error: %s' % str(e), file=sys.stderr)
371 retcode = 1
372 else:
373 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000374 finally:
375 # Clean up the downloaded zipfile.
dnj4625b5a2016-11-10 18:23:26 -0800376 #
377 # This is somehow racy on Windows.
378 # Catching OSError because WindowsError isn't portable and
379 # pylint complains.
380 exponential_backoff_retry(
381 lambda: gclient_utils.rm_file_or_tree(tempdir),
382 excs=(OSError,),
383 name='rmtree [%s]' % (tempdir,),
384 printerr=self.print)
szager@chromium.org848fd492014-04-09 19:06:44 +0000385
386 if retcode:
387 self.print(
388 'Extracting bootstrap zipfile %s failed.\n'
389 'Resuming normal operations.' % filename)
390 return False
391 return True
392
393 def exists(self):
394 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
395
szager@chromium.org66c8b852015-09-22 23:19:07 +0000396 def _preserve_fetchspec(self):
397 """Read and preserve remote.origin.fetch from an existing mirror.
398
399 This modifies self.fetch_specs.
400 """
401 if not self.exists():
402 return
403 try:
404 config_fetchspecs = subprocess.check_output(
405 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
406 cwd=self.mirror_path)
407 for fetchspec in config_fetchspecs.splitlines():
408 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
409 except subprocess.CalledProcessError:
410 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
411 'existing cache directory. You may need to manually edit '
412 '%s and "git cache fetch" again.'
413 % os.path.join(self.mirror_path, 'config'))
414
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000415 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
416 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000417 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
418 pack_files = []
419
420 if os.path.isdir(pack_dir):
421 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
422
423 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000424 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000425 len(pack_files) > GC_AUTOPACKLIMIT)
426 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000427 if self.exists():
428 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
429 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000430 tempdir = tempfile.mkdtemp(
431 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
432 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
433 if bootstrapped:
434 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000435 gclient_utils.rmtree(self.mirror_path)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000436 elif not self.exists():
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000437 # Bootstrap failed, no previous cache; start with a bare git dir.
438 self.RunGit(['init', '--bare'], cwd=tempdir)
439 else:
440 # Bootstrap failed, previous cache exists; warn and continue.
441 logging.warn(
442 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
443 'but failed. Continuing with non-optimized repository.'
444 % len(pack_files))
445 gclient_utils.rmtree(tempdir)
446 tempdir = None
447 else:
448 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
449 logging.warn(
450 'Shallow fetch requested, but repo cache already exists.')
451 return tempdir
452
453 def _fetch(self, rundir, verbose, depth):
454 self.config(rundir)
455 v = []
456 d = []
457 if verbose:
458 v = ['-v', '--progress']
459 if depth:
460 d = ['--depth', str(depth)]
461 fetch_cmd = ['fetch'] + v + d + ['origin']
462 fetch_specs = subprocess.check_output(
463 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
464 cwd=rundir).strip().splitlines()
465 for spec in fetch_specs:
466 try:
467 self.print('Fetching %s' % spec)
468 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
469 except subprocess.CalledProcessError:
470 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700471 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000472 logging.warn('Fetch of %s failed' % spec)
473
szager@chromium.org848fd492014-04-09 19:06:44 +0000474 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000475 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000476 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000477 if shallow and not depth:
478 depth = 10000
479 gclient_utils.safe_makedirs(self.GetCachePath())
480
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000481 lockfile = Lockfile(self.mirror_path, lock_timeout)
szager@chromium.org108eced2014-06-19 21:22:43 +0000482 if not ignore_lock:
483 lockfile.lock()
484
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000485 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000486 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000487 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000488 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000489 self._fetch(rundir, verbose, depth)
hinokadcd84042016-06-09 14:26:17 -0700490 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000491 # This is a major failure, we need to clean and force a bootstrap.
492 gclient_utils.rmtree(rundir)
493 self.print(GIT_CACHE_CORRUPT_MESSAGE)
494 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
495 assert tempdir
496 self._fetch(tempdir or self.mirror_path, verbose, depth)
497 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000498 if tempdir:
dnjb445ef52016-11-10 15:51:39 -0800499 if os.path.exists(self.mirror_path):
500 gclient_utils.rmtree(self.mirror_path)
dnj4625b5a2016-11-10 18:23:26 -0800501 self.Rename(tempdir, self.mirror_path)
szager@chromium.org108eced2014-06-19 21:22:43 +0000502 if not ignore_lock:
503 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000504
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000505 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000506 # The files are named <git number>.zip
507 gen_number = subprocess.check_output(
508 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000509 # Run Garbage Collect to compress packfile.
510 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000511 # Creating a temp file and then deleting it ensures we can use this name.
512 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
513 os.remove(tmp_zipfile)
514 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
515 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000516 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
517 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000518 gsutil.call('cp', tmp_zipfile, dest_name)
519 os.remove(tmp_zipfile)
520
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000521 # Remove all other files in the same directory.
522 if prune:
523 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
524 for filename in ls_out.splitlines():
525 if filename == dest_name:
526 continue
527 gsutil.call('rm', filename)
528
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000529 @staticmethod
530 def DeleteTmpPackFiles(path):
531 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000532 if not os.path.isdir(pack_dir):
533 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000534 pack_files = [f for f in os.listdir(pack_dir) if
535 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
536 for f in pack_files:
537 f = os.path.join(pack_dir, f)
538 try:
539 os.remove(f)
540 logging.warn('Deleted stale temporary pack file %s' % f)
541 except OSError:
542 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000543
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000544 @classmethod
545 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000546 did_unlock = False
547 lf = Lockfile(path)
548 if lf.break_lock():
549 did_unlock = True
550 # Look for lock files that might have been left behind by an interrupted
551 # git process.
552 lf = os.path.join(path, 'config.lock')
553 if os.path.exists(lf):
554 os.remove(lf)
555 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000556 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000557 return did_unlock
558
szager@chromium.org848fd492014-04-09 19:06:44 +0000559 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000560 return self.BreakLocks(self.mirror_path)
561
562 @classmethod
563 def UnlockAll(cls):
564 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000565 if not cachepath:
566 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000567 dirlist = os.listdir(cachepath)
568 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
569 if os.path.isdir(os.path.join(cachepath, path))])
570 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000571 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000572 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000573 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000574 os.path.isfile(os.path.join(cachepath, dirent))):
575 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
576
577 unlocked_repos = []
578 for repo_dir in repo_dirs:
579 if cls.BreakLocks(repo_dir):
580 unlocked_repos.append(repo_dir)
581
582 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000583
agable@chromium.org5a306a22014-02-24 22:13:59 +0000584@subcommand.usage('[url of repo to check for caching]')
585def CMDexists(parser, args):
586 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000587 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000588 if not len(args) == 1:
589 parser.error('git cache exists only takes exactly one repo url.')
590 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000591 mirror = Mirror(url)
592 if mirror.exists():
593 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000594 return 0
595 return 1
596
597
hinoka@google.com563559c2014-04-02 00:36:24 +0000598@subcommand.usage('[url of repo to create a bootstrap zip file]')
599def CMDupdate_bootstrap(parser, args):
600 """Create and uploads a bootstrap tarball."""
601 # Lets just assert we can't do this on Windows.
602 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000603 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000604 return 1
605
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000606 parser.add_option('--prune', action='store_true',
607 help='Prune all other cached zipballs of the same repo.')
608
hinoka@google.com563559c2014-04-02 00:36:24 +0000609 # First, we need to ensure the cache is populated.
610 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000611 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000612 CMDpopulate(parser, populate_args)
613
614 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000615 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000616 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000617 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000618 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000619 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000620
621
agable@chromium.org5a306a22014-02-24 22:13:59 +0000622@subcommand.usage('[url of repo to add to or update in cache]')
623def CMDpopulate(parser, args):
624 """Ensure that the cache has all up-to-date objects for the given repo."""
625 parser.add_option('--depth', type='int',
626 help='Only cache DEPTH commits of history')
627 parser.add_option('--shallow', '-s', action='store_true',
628 help='Only cache 10000 commits of history')
629 parser.add_option('--ref', action='append',
630 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000631 parser.add_option('--no_bootstrap', '--no-bootstrap',
632 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000633 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000634 parser.add_option('--ignore_locks', '--ignore-locks',
635 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000636 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000637
agable@chromium.org5a306a22014-02-24 22:13:59 +0000638 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000639 if not len(args) == 1:
640 parser.error('git cache populate only takes exactly one repo url.')
641 url = args[0]
642
szager@chromium.org848fd492014-04-09 19:06:44 +0000643 mirror = Mirror(url, refs=options.ref)
644 kwargs = {
645 'verbose': options.verbose,
646 'shallow': options.shallow,
647 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000648 'ignore_lock': options.ignore_locks,
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000649 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000650 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000651 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000652 kwargs['depth'] = options.depth
653 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000654
655
szager@chromium.orgf3145112014-08-07 21:02:36 +0000656@subcommand.usage('Fetch new commits into cache and current checkout')
657def CMDfetch(parser, args):
658 """Update mirror, and fetch in cwd."""
659 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000660 parser.add_option('--no_bootstrap', '--no-bootstrap',
661 action='store_true',
662 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000663 options, args = parser.parse_args(args)
664
665 # Figure out which remotes to fetch. This mimics the behavior of regular
666 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
667 # this will NOT try to traverse up the branching structure to find the
668 # ultimate remote to update.
669 remotes = []
670 if options.all:
671 assert not args, 'fatal: fetch --all does not take a repository argument'
672 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
673 elif args:
674 remotes = args
675 else:
676 current_branch = subprocess.check_output(
677 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
678 if current_branch != 'HEAD':
679 upstream = subprocess.check_output(
680 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
681 ).strip()
682 if upstream and upstream != '.':
683 remotes = [upstream]
684 if not remotes:
685 remotes = ['origin']
686
687 cachepath = Mirror.GetCachePath()
688 git_dir = os.path.abspath(subprocess.check_output(
689 [Mirror.git_exe, 'rev-parse', '--git-dir']))
690 git_dir = os.path.abspath(git_dir)
691 if git_dir.startswith(cachepath):
692 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000693 mirror.populate(
694 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000695 return 0
696 for remote in remotes:
697 remote_url = subprocess.check_output(
698 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
699 if remote_url.startswith(cachepath):
700 mirror = Mirror.FromPath(remote_url)
701 mirror.print = lambda *args: None
702 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000703 mirror.populate(
704 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000705 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
706 return 0
707
708
agable@chromium.org5a306a22014-02-24 22:13:59 +0000709@subcommand.usage('[url of repo to unlock, or -a|--all]')
710def CMDunlock(parser, args):
711 """Unlock one or all repos if their lock files are still around."""
712 parser.add_option('--force', '-f', action='store_true',
713 help='Actually perform the action')
714 parser.add_option('--all', '-a', action='store_true',
715 help='Unlock all repository caches')
716 options, args = parser.parse_args(args)
717 if len(args) > 1 or (len(args) == 0 and not options.all):
718 parser.error('git cache unlock takes exactly one repo url, or --all')
719
agable@chromium.org5a306a22014-02-24 22:13:59 +0000720 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000721 cachepath = Mirror.GetCachePath()
722 lockfiles = [os.path.join(cachepath, path)
723 for path in os.listdir(cachepath)
724 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000725 parser.error('git cache unlock requires -f|--force to do anything. '
726 'Refusing to unlock the following repo caches: '
727 ', '.join(lockfiles))
728
szager@chromium.org848fd492014-04-09 19:06:44 +0000729 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000730 if options.all:
731 unlocked_repos.extend(Mirror.UnlockAll())
732 else:
733 m = Mirror(args[0])
734 if m.unlock():
735 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000736
szager@chromium.org848fd492014-04-09 19:06:44 +0000737 if unlocked_repos:
738 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
739 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000740
741
742class OptionParser(optparse.OptionParser):
743 """Wrapper class for OptionParser to handle global options."""
744
745 def __init__(self, *args, **kwargs):
746 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
747 self.add_option('-c', '--cache-dir',
748 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000749 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000750 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000751 self.add_option('-q', '--quiet', action='store_true',
752 help='Suppress all extraneous output')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000753 self.add_option('--timeout', type='int', default=0,
754 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000755
756 def parse_args(self, args=None, values=None):
757 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000758 if options.quiet:
759 options.verbose = 0
760
761 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
762 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000763
764 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000765 global_cache_dir = Mirror.GetCachePath()
766 except RuntimeError:
767 global_cache_dir = None
768 if options.cache_dir:
769 if global_cache_dir and (
770 os.path.abspath(options.cache_dir) !=
771 os.path.abspath(global_cache_dir)):
772 logging.warn('Overriding globally-configured cache directory.')
773 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000774
agable@chromium.org5a306a22014-02-24 22:13:59 +0000775 return options, args
776
777
778def main(argv):
779 dispatcher = subcommand.CommandDispatcher(__name__)
780 return dispatcher.execute(OptionParser(), argv)
781
782
783if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000784 try:
785 sys.exit(main(sys.argv[1:]))
786 except KeyboardInterrupt:
787 sys.stderr.write('interrupted\n')
788 sys.exit(1)