blob: 20b844514fa468c0500962bed8e9cc35fa087f0f [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
Vadim Shtayura08049e22017-10-11 00:14:52 +000038class 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
Vadim Shtayura08049e22017-10-11 00:14:52 +000080class Lockfile(object):
81 """Class to represent a cross-platform process-specific lockfile."""
82
83 def __init__(self, path, timeout=0):
84 self.path = os.path.abspath(path)
85 self.timeout = timeout
86 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')
107 print(self.pid, file=f)
108 f.close()
109
110 def _remove_lockfile(self):
111 """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)
118
119 def delete():
120 exitcode = subprocess.call(['cmd.exe', '/c',
121 'del', '/f', '/q', lockfile])
122 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,))
128 else:
129 os.remove(self.lockfile)
130
131 def lock(self):
132 """Acquire the lock.
133
134 This will block with a deadline of self.timeout seconds.
135 """
136 elapsed = 0
137 while True:
138 try:
139 self._make_lockfile()
140 return
141 except OSError as e:
142 if elapsed < self.timeout:
143 sleep_time = max(10, min(3, self.timeout - elapsed))
144 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))
153
154 def unlock(self):
155 """Release the lock."""
156 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
165
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
189
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')
Vadim Shtayura08049e22017-10-11 00:14:52 +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):
Ryan Tseng3beabd02017-03-15 13:57:58 -0700228 u = urlparse.urlparse(self.url)
229 if u.netloc == 'chromium.googlesource.com':
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000230 return 'chromium-git-cache'
Ryan Tseng3beabd02017-03-15 13:57:58 -0700231 elif u.netloc == 'chrome-internal.googlesource.com':
232 return 'chrome-git-cache'
233 # Not recognized.
234 return None
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000235
szager@chromium.org174766f2014-05-13 21:27:46 +0000236 @classmethod
237 def FromPath(cls, path):
238 return cls(cls.CacheDirToUrl(path))
239
szager@chromium.org848fd492014-04-09 19:06:44 +0000240 @staticmethod
241 def UrlToCacheDir(url):
242 """Convert a git url to a normalized form for the cache dir path."""
243 parsed = urlparse.urlparse(url)
244 norm_url = parsed.netloc + parsed.path
245 if norm_url.endswith('.git'):
246 norm_url = norm_url[:-len('.git')]
247 return norm_url.replace('-', '--').replace('/', '-').lower()
248
249 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000250 def CacheDirToUrl(path):
251 """Convert a cache dir path to its corresponding url."""
252 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
253 return 'https://%s' % netpath
254
szager@chromium.org848fd492014-04-09 19:06:44 +0000255 @classmethod
256 def SetCachePath(cls, cachepath):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000257 with cls.cachepath_lock:
258 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000259
260 @classmethod
261 def GetCachePath(cls):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000262 with cls.cachepath_lock:
263 if not hasattr(cls, 'cachepath'):
264 try:
265 cachepath = subprocess.check_output(
266 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
267 except subprocess.CalledProcessError:
268 cachepath = None
269 if not cachepath:
270 raise RuntimeError(
271 'No global cache.cachepath git configuration found.')
272 setattr(cls, 'cachepath', cachepath)
273 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000274
dnj4625b5a2016-11-10 18:23:26 -0800275 def Rename(self, src, dst):
276 # This is somehow racy on Windows.
277 # Catching OSError because WindowsError isn't portable and
278 # pylint complains.
279 exponential_backoff_retry(
280 lambda: os.rename(src, dst),
281 excs=(OSError,),
282 name='rename [%s] => [%s]' % (src, dst),
283 printerr=self.print)
284
szager@chromium.org848fd492014-04-09 19:06:44 +0000285 def RunGit(self, cmd, **kwargs):
286 """Run git in a subprocess."""
287 cwd = kwargs.setdefault('cwd', self.mirror_path)
288 kwargs.setdefault('print_stdout', False)
289 kwargs.setdefault('filter_fn', self.print)
290 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
291 env.setdefault('GIT_ASKPASS', 'true')
292 env.setdefault('SSH_ASKPASS', 'true')
293 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
294 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
295
296 def config(self, cwd=None):
297 if cwd is None:
298 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000299
300 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinokadcd84042016-06-09 14:26:17 -0700301 try:
302 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
303 except subprocess.CalledProcessError:
304 # Hard error, need to clobber.
305 raise ClobberNeeded()
szager@chromium.org301a7c32014-06-16 17:13:50 +0000306
307 # Don't combine pack files into one big pack file. It's really slow for
308 # repositories, and there's no way to track progress and make sure it's
309 # not stuck.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700310 if self.supported_project():
311 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000312
313 # Allocate more RAM for cache-ing delta chains, for better performance
314 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000315 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000316 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000317
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000318 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000319 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000320 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000321 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000322 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000323 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000324 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000325
326 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000327 """Bootstrap the repo from Google Stroage if possible.
328
329 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
330 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000331
Ryan Tseng3beabd02017-03-15 13:57:58 -0700332 if not self.bootstrap_bucket:
333 return False
hinoka@google.com776a2c32014-04-25 07:54:25 +0000334 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000335 if (sys.platform.startswith('win') and
336 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000337 python_fallback = True
338 elif sys.platform.startswith('darwin'):
339 # The OSX version of unzip doesn't support zip64.
340 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000341 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000342 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000343
344 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000345 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000346 # Get the most recent version of the zipfile.
347 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
348 ls_out_sorted = sorted(ls_out.splitlines())
349 if not ls_out_sorted:
350 # This repo is not on Google Storage.
351 return False
352 latest_checkout = ls_out_sorted[-1]
353
354 # Download zip file to a temporary directory.
355 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000356 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000357 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000358 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000359 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000360 return False
361 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
362
hinoka@google.com776a2c32014-04-25 07:54:25 +0000363 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
364 if not python_fallback:
365 if sys.platform.startswith('win'):
366 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
367 else:
368 cmd = ['unzip', filename, '-d', directory]
369 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000370 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000371 try:
372 with zipfile.ZipFile(filename, 'r') as f:
373 f.printdir()
374 f.extractall(directory)
375 except Exception as e:
376 self.print('Encountered error: %s' % str(e), file=sys.stderr)
377 retcode = 1
378 else:
379 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000380 finally:
381 # Clean up the downloaded zipfile.
dnj4625b5a2016-11-10 18:23:26 -0800382 #
383 # This is somehow racy on Windows.
384 # Catching OSError because WindowsError isn't portable and
385 # pylint complains.
386 exponential_backoff_retry(
387 lambda: gclient_utils.rm_file_or_tree(tempdir),
388 excs=(OSError,),
389 name='rmtree [%s]' % (tempdir,),
390 printerr=self.print)
szager@chromium.org848fd492014-04-09 19:06:44 +0000391
392 if retcode:
393 self.print(
394 'Extracting bootstrap zipfile %s failed.\n'
395 'Resuming normal operations.' % filename)
396 return False
397 return True
398
399 def exists(self):
400 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
401
Ryan Tseng3beabd02017-03-15 13:57:58 -0700402 def supported_project(self):
403 """Returns true if this repo is known to have a bootstrap zip file."""
404 u = urlparse.urlparse(self.url)
405 return u.netloc in [
406 'chromium.googlesource.com',
407 'chrome-internal.googlesource.com']
408
szager@chromium.org66c8b852015-09-22 23:19:07 +0000409 def _preserve_fetchspec(self):
410 """Read and preserve remote.origin.fetch from an existing mirror.
411
412 This modifies self.fetch_specs.
413 """
414 if not self.exists():
415 return
416 try:
417 config_fetchspecs = subprocess.check_output(
418 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
419 cwd=self.mirror_path)
420 for fetchspec in config_fetchspecs.splitlines():
421 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
422 except subprocess.CalledProcessError:
423 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
424 'existing cache directory. You may need to manually edit '
425 '%s and "git cache fetch" again.'
426 % os.path.join(self.mirror_path, 'config'))
427
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000428 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
429 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000430 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
431 pack_files = []
432
433 if os.path.isdir(pack_dir):
434 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
435
436 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000437 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000438 len(pack_files) > GC_AUTOPACKLIMIT)
439 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000440 if self.exists():
441 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
442 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000443 tempdir = tempfile.mkdtemp(
444 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
445 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
446 if bootstrapped:
447 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000448 gclient_utils.rmtree(self.mirror_path)
Ryan Tseng3beabd02017-03-15 13:57:58 -0700449 elif not self.exists() or not self.supported_project():
450 # Bootstrap failed due to either
451 # 1. No previous cache
452 # 2. Project doesn't have a bootstrap zip file
453 # Start with a bare git dir.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000454 self.RunGit(['init', '--bare'], cwd=tempdir)
455 else:
456 # Bootstrap failed, previous cache exists; warn and continue.
457 logging.warn(
458 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
459 'but failed. Continuing with non-optimized repository.'
460 % len(pack_files))
461 gclient_utils.rmtree(tempdir)
462 tempdir = None
463 else:
464 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
465 logging.warn(
466 'Shallow fetch requested, but repo cache already exists.')
467 return tempdir
468
469 def _fetch(self, rundir, verbose, depth):
470 self.config(rundir)
471 v = []
472 d = []
473 if verbose:
474 v = ['-v', '--progress']
475 if depth:
476 d = ['--depth', str(depth)]
477 fetch_cmd = ['fetch'] + v + d + ['origin']
478 fetch_specs = subprocess.check_output(
479 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
480 cwd=rundir).strip().splitlines()
481 for spec in fetch_specs:
482 try:
483 self.print('Fetching %s' % spec)
484 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
485 except subprocess.CalledProcessError:
486 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700487 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000488 logging.warn('Fetch of %s failed' % spec)
489
Vadim Shtayura08049e22017-10-11 00:14:52 +0000490 def populate(self, depth=None, shallow=False, bootstrap=False,
491 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000492 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000493 if shallow and not depth:
494 depth = 10000
495 gclient_utils.safe_makedirs(self.GetCachePath())
496
Vadim Shtayura08049e22017-10-11 00:14:52 +0000497 lockfile = Lockfile(self.mirror_path, lock_timeout)
498 if not ignore_lock:
499 lockfile.lock()
500
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000501 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000502 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000503 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000504 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000505 self._fetch(rundir, verbose, depth)
hinokadcd84042016-06-09 14:26:17 -0700506 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000507 # This is a major failure, we need to clean and force a bootstrap.
508 gclient_utils.rmtree(rundir)
509 self.print(GIT_CACHE_CORRUPT_MESSAGE)
510 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
511 assert tempdir
512 self._fetch(tempdir or self.mirror_path, verbose, depth)
513 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000514 if tempdir:
dnjb445ef52016-11-10 15:51:39 -0800515 if os.path.exists(self.mirror_path):
516 gclient_utils.rmtree(self.mirror_path)
dnj4625b5a2016-11-10 18:23:26 -0800517 self.Rename(tempdir, self.mirror_path)
Vadim Shtayura08049e22017-10-11 00:14:52 +0000518 if not ignore_lock:
519 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000520
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000521 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000522 # The files are named <git number>.zip
523 gen_number = subprocess.check_output(
524 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000525 # Run Garbage Collect to compress packfile.
526 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000527 # Creating a temp file and then deleting it ensures we can use this name.
528 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
529 os.remove(tmp_zipfile)
530 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
531 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000532 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
533 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000534 gsutil.call('cp', tmp_zipfile, dest_name)
535 os.remove(tmp_zipfile)
536
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000537 # Remove all other files in the same directory.
538 if prune:
539 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
540 for filename in ls_out.splitlines():
541 if filename == dest_name:
542 continue
543 gsutil.call('rm', filename)
544
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000545 @staticmethod
546 def DeleteTmpPackFiles(path):
547 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000548 if not os.path.isdir(pack_dir):
549 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000550 pack_files = [f for f in os.listdir(pack_dir) if
551 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
552 for f in pack_files:
553 f = os.path.join(pack_dir, f)
554 try:
555 os.remove(f)
556 logging.warn('Deleted stale temporary pack file %s' % f)
557 except OSError:
558 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000559
Vadim Shtayura08049e22017-10-11 00:14:52 +0000560 @classmethod
561 def BreakLocks(cls, path):
562 did_unlock = False
563 lf = Lockfile(path)
564 if lf.break_lock():
565 did_unlock = True
566 # Look for lock files that might have been left behind by an interrupted
567 # git process.
568 lf = os.path.join(path, 'config.lock')
569 if os.path.exists(lf):
570 os.remove(lf)
571 did_unlock = True
572 cls.DeleteTmpPackFiles(path)
573 return did_unlock
574
575 def unlock(self):
576 return self.BreakLocks(self.mirror_path)
577
578 @classmethod
579 def UnlockAll(cls):
580 cachepath = cls.GetCachePath()
581 if not cachepath:
582 return
583 dirlist = os.listdir(cachepath)
584 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
585 if os.path.isdir(os.path.join(cachepath, path))])
586 for dirent in dirlist:
587 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
588 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
589 elif (dirent.endswith('.lock') and
590 os.path.isfile(os.path.join(cachepath, dirent))):
591 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
592
593 unlocked_repos = []
594 for repo_dir in repo_dirs:
595 if cls.BreakLocks(repo_dir):
596 unlocked_repos.append(repo_dir)
597
598 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000599
agable@chromium.org5a306a22014-02-24 22:13:59 +0000600@subcommand.usage('[url of repo to check for caching]')
601def CMDexists(parser, args):
602 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000603 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000604 if not len(args) == 1:
605 parser.error('git cache exists only takes exactly one repo url.')
606 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000607 mirror = Mirror(url)
608 if mirror.exists():
609 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000610 return 0
611 return 1
612
613
hinoka@google.com563559c2014-04-02 00:36:24 +0000614@subcommand.usage('[url of repo to create a bootstrap zip file]')
615def CMDupdate_bootstrap(parser, args):
616 """Create and uploads a bootstrap tarball."""
617 # Lets just assert we can't do this on Windows.
618 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000619 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000620 return 1
621
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000622 parser.add_option('--prune', action='store_true',
623 help='Prune all other cached zipballs of the same repo.')
624
hinoka@google.com563559c2014-04-02 00:36:24 +0000625 # First, we need to ensure the cache is populated.
626 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000627 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000628 CMDpopulate(parser, populate_args)
629
630 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000631 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000632 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000633 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000634 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000635 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000636
637
agable@chromium.org5a306a22014-02-24 22:13:59 +0000638@subcommand.usage('[url of repo to add to or update in cache]')
639def CMDpopulate(parser, args):
640 """Ensure that the cache has all up-to-date objects for the given repo."""
641 parser.add_option('--depth', type='int',
642 help='Only cache DEPTH commits of history')
643 parser.add_option('--shallow', '-s', action='store_true',
644 help='Only cache 10000 commits of history')
645 parser.add_option('--ref', action='append',
646 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000647 parser.add_option('--no_bootstrap', '--no-bootstrap',
648 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000649 help='Don\'t bootstrap from Google Storage')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000650 parser.add_option('--ignore_locks', '--ignore-locks',
651 action='store_true',
652 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000653
agable@chromium.org5a306a22014-02-24 22:13:59 +0000654 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000655 if not len(args) == 1:
656 parser.error('git cache populate only takes exactly one repo url.')
657 url = args[0]
658
szager@chromium.org848fd492014-04-09 19:06:44 +0000659 mirror = Mirror(url, refs=options.ref)
660 kwargs = {
661 'verbose': options.verbose,
662 'shallow': options.shallow,
663 'bootstrap': not options.no_bootstrap,
Vadim Shtayura08049e22017-10-11 00:14:52 +0000664 'ignore_lock': options.ignore_locks,
665 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000666 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000667 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000668 kwargs['depth'] = options.depth
669 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000670
671
szager@chromium.orgf3145112014-08-07 21:02:36 +0000672@subcommand.usage('Fetch new commits into cache and current checkout')
673def CMDfetch(parser, args):
674 """Update mirror, and fetch in cwd."""
675 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000676 parser.add_option('--no_bootstrap', '--no-bootstrap',
677 action='store_true',
678 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000679 options, args = parser.parse_args(args)
680
681 # Figure out which remotes to fetch. This mimics the behavior of regular
682 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
683 # this will NOT try to traverse up the branching structure to find the
684 # ultimate remote to update.
685 remotes = []
686 if options.all:
687 assert not args, 'fatal: fetch --all does not take a repository argument'
688 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
689 elif args:
690 remotes = args
691 else:
692 current_branch = subprocess.check_output(
693 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
694 if current_branch != 'HEAD':
695 upstream = subprocess.check_output(
696 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
697 ).strip()
698 if upstream and upstream != '.':
699 remotes = [upstream]
700 if not remotes:
701 remotes = ['origin']
702
703 cachepath = Mirror.GetCachePath()
704 git_dir = os.path.abspath(subprocess.check_output(
705 [Mirror.git_exe, 'rev-parse', '--git-dir']))
706 git_dir = os.path.abspath(git_dir)
707 if git_dir.startswith(cachepath):
708 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000709 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000710 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000711 return 0
712 for remote in remotes:
713 remote_url = subprocess.check_output(
714 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
715 if remote_url.startswith(cachepath):
716 mirror = Mirror.FromPath(remote_url)
717 mirror.print = lambda *args: None
718 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000719 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000720 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000721 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
722 return 0
723
724
Vadim Shtayura08049e22017-10-11 00:14:52 +0000725@subcommand.usage('[url of repo to unlock, or -a|--all]')
726def CMDunlock(parser, args):
727 """Unlock one or all repos if their lock files are still around."""
728 parser.add_option('--force', '-f', action='store_true',
729 help='Actually perform the action')
730 parser.add_option('--all', '-a', action='store_true',
731 help='Unlock all repository caches')
732 options, args = parser.parse_args(args)
733 if len(args) > 1 or (len(args) == 0 and not options.all):
734 parser.error('git cache unlock takes exactly one repo url, or --all')
735
736 if not options.force:
737 cachepath = Mirror.GetCachePath()
738 lockfiles = [os.path.join(cachepath, path)
739 for path in os.listdir(cachepath)
740 if path.endswith('.lock') and os.path.isfile(path)]
741 parser.error('git cache unlock requires -f|--force to do anything. '
742 'Refusing to unlock the following repo caches: '
743 ', '.join(lockfiles))
744
745 unlocked_repos = []
746 if options.all:
747 unlocked_repos.extend(Mirror.UnlockAll())
748 else:
749 m = Mirror(args[0])
750 if m.unlock():
751 unlocked_repos.append(m.mirror_path)
752
753 if unlocked_repos:
754 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
755 unlocked_repos))
756
757
agable@chromium.org5a306a22014-02-24 22:13:59 +0000758class OptionParser(optparse.OptionParser):
759 """Wrapper class for OptionParser to handle global options."""
760
761 def __init__(self, *args, **kwargs):
762 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
763 self.add_option('-c', '--cache-dir',
764 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000765 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000766 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000767 self.add_option('-q', '--quiet', action='store_true',
768 help='Suppress all extraneous output')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000769 self.add_option('--timeout', type='int', default=0,
770 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000771
772 def parse_args(self, args=None, values=None):
773 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000774 if options.quiet:
775 options.verbose = 0
776
777 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
778 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000779
780 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000781 global_cache_dir = Mirror.GetCachePath()
782 except RuntimeError:
783 global_cache_dir = None
784 if options.cache_dir:
785 if global_cache_dir and (
786 os.path.abspath(options.cache_dir) !=
787 os.path.abspath(global_cache_dir)):
788 logging.warn('Overriding globally-configured cache directory.')
789 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000790
agable@chromium.org5a306a22014-02-24 22:13:59 +0000791 return options, args
792
793
794def main(argv):
795 dispatcher = subcommand.CommandDispatcher(__name__)
796 return dispatcher.execute(OptionParser(), argv)
797
798
799if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000800 try:
801 sys.exit(main(sys.argv[1:]))
802 except KeyboardInterrupt:
803 sys.stderr.write('interrupted\n')
804 sys.exit(1)