blob: aedac25c515a23b33901ac02b17cae5fc0ece863 [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
Andrii Shyshkalovd51ed572017-11-23 14:43:16 -080029if sys.platform.startswith('win'):
30 GC_AUTOPACKLIMIT = 9
szager@chromium.org301a7c32014-06-16 17:13:50 +000031
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000032GIT_CACHE_CORRUPT_MESSAGE = 'WARNING: The Git cache is corrupt.'
33
szager@chromium.org848fd492014-04-09 19:06:44 +000034try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080035 # pylint: disable=undefined-variable
szager@chromium.org848fd492014-04-09 19:06:44 +000036 WinErr = WindowsError
37except NameError:
38 class WinErr(Exception):
39 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000040
Vadim Shtayura08049e22017-10-11 00:14:52 +000041class LockError(Exception):
42 pass
43
hinokadcd84042016-06-09 14:26:17 -070044class ClobberNeeded(Exception):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000045 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000046
dnj4625b5a2016-11-10 18:23:26 -080047
48def exponential_backoff_retry(fn, excs=(Exception,), name=None, count=10,
49 sleep_time=0.25, printerr=None):
50 """Executes |fn| up to |count| times, backing off exponentially.
51
52 Args:
53 fn (callable): The function to execute. If this raises a handled
54 exception, the function will retry with exponential backoff.
55 excs (tuple): A tuple of Exception types to handle. If one of these is
56 raised by |fn|, a retry will be attempted. If |fn| raises an Exception
57 that is not in this list, it will immediately pass through. If |excs|
58 is empty, the Exception base class will be used.
59 name (str): Optional operation name to print in the retry string.
60 count (int): The number of times to try before allowing the exception to
61 pass through.
62 sleep_time (float): The initial number of seconds to sleep in between
63 retries. This will be doubled each retry.
64 printerr (callable): Function that will be called with the error string upon
65 failures. If None, |logging.warning| will be used.
66
67 Returns: The return value of the successful fn.
68 """
69 printerr = printerr or logging.warning
70 for i in xrange(count):
71 try:
72 return fn()
73 except excs as e:
74 if (i+1) >= count:
75 raise
76
77 printerr('Retrying %s in %.2f second(s) (%d / %d attempts): %s' % (
78 (name or 'operation'), sleep_time, (i+1), count, e))
79 time.sleep(sleep_time)
80 sleep_time *= 2
81
82
Vadim Shtayura08049e22017-10-11 00:14:52 +000083class Lockfile(object):
84 """Class to represent a cross-platform process-specific lockfile."""
85
86 def __init__(self, path, timeout=0):
87 self.path = os.path.abspath(path)
88 self.timeout = timeout
89 self.lockfile = self.path + ".lock"
90 self.pid = os.getpid()
91
92 def _read_pid(self):
93 """Read the pid stored in the lockfile.
94
95 Note: This method is potentially racy. By the time it returns the lockfile
96 may have been unlocked, removed, or stolen by some other process.
97 """
98 try:
99 with open(self.lockfile, 'r') as f:
100 pid = int(f.readline().strip())
101 except (IOError, ValueError):
102 pid = None
103 return pid
104
105 def _make_lockfile(self):
106 """Safely creates a lockfile containing the current pid."""
107 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
108 fd = os.open(self.lockfile, open_flags, 0o644)
109 f = os.fdopen(fd, 'w')
110 print(self.pid, file=f)
111 f.close()
112
113 def _remove_lockfile(self):
114 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
115
116 See gclient_utils.py:rmtree docstring for more explanation on the
117 windows case.
118 """
119 if sys.platform == 'win32':
120 lockfile = os.path.normcase(self.lockfile)
121
122 def delete():
123 exitcode = subprocess.call(['cmd.exe', '/c',
124 'del', '/f', '/q', lockfile])
125 if exitcode != 0:
126 raise LockError('Failed to remove lock: %s' % (lockfile,))
127 exponential_backoff_retry(
128 delete,
129 excs=(LockError,),
130 name='del [%s]' % (lockfile,))
131 else:
132 os.remove(self.lockfile)
133
134 def lock(self):
135 """Acquire the lock.
136
137 This will block with a deadline of self.timeout seconds.
138 """
139 elapsed = 0
140 while True:
141 try:
142 self._make_lockfile()
143 return
144 except OSError as e:
145 if elapsed < self.timeout:
146 sleep_time = max(10, min(3, self.timeout - elapsed))
147 logging.info('Could not create git cache lockfile; '
148 'will retry after sleep(%d).', sleep_time);
149 elapsed += sleep_time
150 time.sleep(sleep_time)
151 continue
152 if e.errno == errno.EEXIST:
153 raise LockError("%s is already locked" % self.path)
154 else:
155 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
156
157 def unlock(self):
158 """Release the lock."""
159 try:
160 if not self.is_locked():
161 raise LockError("%s is not locked" % self.path)
162 if not self.i_am_locking():
163 raise LockError("%s is locked, but not by me" % self.path)
164 self._remove_lockfile()
165 except WinErr:
166 # Windows is unreliable when it comes to file locking. YMMV.
167 pass
168
169 def break_lock(self):
170 """Remove the lock, even if it was created by someone else."""
171 try:
172 self._remove_lockfile()
173 return True
174 except OSError as exc:
175 if exc.errno == errno.ENOENT:
176 return False
177 else:
178 raise
179
180 def is_locked(self):
181 """Test if the file is locked by anyone.
182
183 Note: This method is potentially racy. By the time it returns the lockfile
184 may have been unlocked, removed, or stolen by some other process.
185 """
186 return os.path.exists(self.lockfile)
187
188 def i_am_locking(self):
189 """Test if the file is locked by this process."""
190 return self.is_locked() and self.pid == self._read_pid()
191
192
szager@chromium.org848fd492014-04-09 19:06:44 +0000193class Mirror(object):
194
195 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
196 gsutil_exe = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +0000197 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000198 cachepath_lock = threading.Lock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000199
szager@chromium.org66c8b852015-09-22 23:19:07 +0000200 @staticmethod
201 def parse_fetch_spec(spec):
202 """Parses and canonicalizes a fetch spec.
203
204 Returns (fetchspec, value_regex), where value_regex can be used
205 with 'git config --replace-all'.
206 """
207 parts = spec.split(':', 1)
208 src = parts[0].lstrip('+').rstrip('/')
209 if not src.startswith('refs/'):
210 src = 'refs/heads/%s' % src
211 dest = parts[1].rstrip('/') if len(parts) > 1 else src
212 regex = r'\+%s:.*' % src.replace('*', r'\*')
213 return ('+%s:%s' % (src, dest), regex)
214
szager@chromium.org848fd492014-04-09 19:06:44 +0000215 def __init__(self, url, refs=None, print_func=None):
216 self.url = url
szager@chromium.org66c8b852015-09-22 23:19:07 +0000217 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
szager@chromium.org848fd492014-04-09 19:06:44 +0000218 self.basedir = self.UrlToCacheDir(url)
219 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000220 if print_func:
221 self.print = self.print_without_file
222 self.print_func = print_func
223 else:
224 self.print = print
225
dnj4625b5a2016-11-10 18:23:26 -0800226 def print_without_file(self, message, **_kwargs):
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000227 self.print_func(message)
szager@chromium.org848fd492014-04-09 19:06:44 +0000228
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800229 @contextlib.contextmanager
230 def print_duration_of(self, what):
231 start = time.time()
232 try:
233 yield
234 finally:
235 self.print('%s took %.1f minutes' % (what, (time.time() - start) / 60.0))
236
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000237 @property
238 def bootstrap_bucket(self):
Ryan Tseng3beabd02017-03-15 13:57:58 -0700239 u = urlparse.urlparse(self.url)
240 if u.netloc == 'chromium.googlesource.com':
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000241 return 'chromium-git-cache'
Ryan Tseng3beabd02017-03-15 13:57:58 -0700242 elif u.netloc == 'chrome-internal.googlesource.com':
243 return 'chrome-git-cache'
244 # Not recognized.
245 return None
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000246
szager@chromium.org174766f2014-05-13 21:27:46 +0000247 @classmethod
248 def FromPath(cls, path):
249 return cls(cls.CacheDirToUrl(path))
250
szager@chromium.org848fd492014-04-09 19:06:44 +0000251 @staticmethod
252 def UrlToCacheDir(url):
253 """Convert a git url to a normalized form for the cache dir path."""
254 parsed = urlparse.urlparse(url)
255 norm_url = parsed.netloc + parsed.path
256 if norm_url.endswith('.git'):
257 norm_url = norm_url[:-len('.git')]
258 return norm_url.replace('-', '--').replace('/', '-').lower()
259
260 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000261 def CacheDirToUrl(path):
262 """Convert a cache dir path to its corresponding url."""
263 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
264 return 'https://%s' % netpath
265
szager@chromium.org848fd492014-04-09 19:06:44 +0000266 @classmethod
267 def SetCachePath(cls, cachepath):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000268 with cls.cachepath_lock:
269 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000270
271 @classmethod
272 def GetCachePath(cls):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000273 with cls.cachepath_lock:
274 if not hasattr(cls, 'cachepath'):
275 try:
276 cachepath = subprocess.check_output(
277 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
278 except subprocess.CalledProcessError:
279 cachepath = None
280 if not cachepath:
281 raise RuntimeError(
282 'No global cache.cachepath git configuration found.')
283 setattr(cls, 'cachepath', cachepath)
284 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000285
dnj4625b5a2016-11-10 18:23:26 -0800286 def Rename(self, src, dst):
287 # This is somehow racy on Windows.
288 # Catching OSError because WindowsError isn't portable and
289 # pylint complains.
290 exponential_backoff_retry(
291 lambda: os.rename(src, dst),
292 excs=(OSError,),
293 name='rename [%s] => [%s]' % (src, dst),
294 printerr=self.print)
295
szager@chromium.org848fd492014-04-09 19:06:44 +0000296 def RunGit(self, cmd, **kwargs):
297 """Run git in a subprocess."""
298 cwd = kwargs.setdefault('cwd', self.mirror_path)
299 kwargs.setdefault('print_stdout', False)
300 kwargs.setdefault('filter_fn', self.print)
301 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
302 env.setdefault('GIT_ASKPASS', 'true')
303 env.setdefault('SSH_ASKPASS', 'true')
304 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
305 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
306
307 def config(self, cwd=None):
308 if cwd is None:
309 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000310
311 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinokadcd84042016-06-09 14:26:17 -0700312 try:
313 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
314 except subprocess.CalledProcessError:
315 # Hard error, need to clobber.
316 raise ClobberNeeded()
szager@chromium.org301a7c32014-06-16 17:13:50 +0000317
318 # Don't combine pack files into one big pack file. It's really slow for
319 # repositories, and there's no way to track progress and make sure it's
320 # not stuck.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700321 if self.supported_project():
322 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000323
324 # Allocate more RAM for cache-ing delta chains, for better performance
325 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000326 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000327 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000328
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000329 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000330 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000331 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000332 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000333 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000334 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000335 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000336
337 def bootstrap_repo(self, directory):
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800338 """Bootstrap the repo from Google Storage if possible.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000339
340 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
341 """
Ryan Tseng3beabd02017-03-15 13:57:58 -0700342 if not self.bootstrap_bucket:
343 return False
hinoka@google.com776a2c32014-04-25 07:54:25 +0000344 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000345 if (sys.platform.startswith('win') and
346 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000347 python_fallback = True
348 elif sys.platform.startswith('darwin'):
349 # The OSX version of unzip doesn't support zip64.
350 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000351 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000352 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000353
354 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000355 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000356 # Get the most recent version of the zipfile.
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800357 _, ls_out, ls_err = gsutil.check_call('ls', gs_folder)
szager@chromium.org848fd492014-04-09 19:06:44 +0000358 ls_out_sorted = sorted(ls_out.splitlines())
359 if not ls_out_sorted:
360 # This repo is not on Google Storage.
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800361 self.print('No bootstrap file for %s found in %s, stderr:\n %s' %
362 (self.mirror_path, self.bootstrap_bucket,
363 ' '.join((ls_err or '').splitlines(True))))
szager@chromium.org848fd492014-04-09 19:06:44 +0000364 return False
365 latest_checkout = ls_out_sorted[-1]
366
367 # Download zip file to a temporary directory.
368 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000369 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000370 self.print('Downloading %s' % latest_checkout)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800371 with self.print_duration_of('download'):
372 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000373 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000374 return False
375 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
376
hinoka@google.com776a2c32014-04-25 07:54:25 +0000377 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800378 with self.print_duration_of('unzip'):
379 if not python_fallback:
380 if sys.platform.startswith('win'):
381 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
382 else:
383 cmd = ['unzip', filename, '-d', directory]
384 retcode = subprocess.call(cmd)
hinoka@google.com776a2c32014-04-25 07:54:25 +0000385 else:
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800386 try:
387 with zipfile.ZipFile(filename, 'r') as f:
388 f.printdir()
389 f.extractall(directory)
390 except Exception as e:
391 self.print('Encountered error: %s' % str(e), file=sys.stderr)
392 retcode = 1
393 else:
394 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000395 finally:
396 # Clean up the downloaded zipfile.
dnj4625b5a2016-11-10 18:23:26 -0800397 #
398 # This is somehow racy on Windows.
399 # Catching OSError because WindowsError isn't portable and
400 # pylint complains.
401 exponential_backoff_retry(
402 lambda: gclient_utils.rm_file_or_tree(tempdir),
403 excs=(OSError,),
404 name='rmtree [%s]' % (tempdir,),
405 printerr=self.print)
szager@chromium.org848fd492014-04-09 19:06:44 +0000406
407 if retcode:
408 self.print(
409 'Extracting bootstrap zipfile %s failed.\n'
410 'Resuming normal operations.' % filename)
411 return False
412 return True
413
414 def exists(self):
415 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
416
Ryan Tseng3beabd02017-03-15 13:57:58 -0700417 def supported_project(self):
418 """Returns true if this repo is known to have a bootstrap zip file."""
419 u = urlparse.urlparse(self.url)
420 return u.netloc in [
421 'chromium.googlesource.com',
422 'chrome-internal.googlesource.com']
423
szager@chromium.org66c8b852015-09-22 23:19:07 +0000424 def _preserve_fetchspec(self):
425 """Read and preserve remote.origin.fetch from an existing mirror.
426
427 This modifies self.fetch_specs.
428 """
429 if not self.exists():
430 return
431 try:
432 config_fetchspecs = subprocess.check_output(
433 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
434 cwd=self.mirror_path)
435 for fetchspec in config_fetchspecs.splitlines():
436 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
437 except subprocess.CalledProcessError:
438 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
439 'existing cache directory. You may need to manually edit '
440 '%s and "git cache fetch" again.'
441 % os.path.join(self.mirror_path, 'config'))
442
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000443 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
444 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000445 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
446 pack_files = []
447
448 if os.path.isdir(pack_dir):
449 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800450 self.print('%s has %d .pack files, re-bootstrapping if >%d' %
451 (self.mirror_path, len(pack_files), GC_AUTOPACKLIMIT))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000452
453 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000454 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000455 len(pack_files) > GC_AUTOPACKLIMIT)
456 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000457 if self.exists():
458 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
459 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000460 tempdir = tempfile.mkdtemp(
461 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
462 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
463 if bootstrapped:
464 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000465 gclient_utils.rmtree(self.mirror_path)
Ryan Tseng3beabd02017-03-15 13:57:58 -0700466 elif not self.exists() or not self.supported_project():
467 # Bootstrap failed due to either
468 # 1. No previous cache
469 # 2. Project doesn't have a bootstrap zip file
470 # Start with a bare git dir.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000471 self.RunGit(['init', '--bare'], cwd=tempdir)
472 else:
473 # Bootstrap failed, previous cache exists; warn and continue.
474 logging.warn(
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800475 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
476 'but failed. Continuing with non-optimized repository.'
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000477 % len(pack_files))
478 gclient_utils.rmtree(tempdir)
479 tempdir = None
480 else:
481 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
482 logging.warn(
483 'Shallow fetch requested, but repo cache already exists.')
484 return tempdir
485
486 def _fetch(self, rundir, verbose, depth):
487 self.config(rundir)
488 v = []
489 d = []
490 if verbose:
491 v = ['-v', '--progress']
492 if depth:
493 d = ['--depth', str(depth)]
494 fetch_cmd = ['fetch'] + v + d + ['origin']
495 fetch_specs = subprocess.check_output(
496 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
497 cwd=rundir).strip().splitlines()
498 for spec in fetch_specs:
499 try:
500 self.print('Fetching %s' % spec)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800501 with self.print_duration_of('fetch %s' % spec):
502 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000503 except subprocess.CalledProcessError:
504 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700505 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000506 logging.warn('Fetch of %s failed' % spec)
507
Vadim Shtayura08049e22017-10-11 00:14:52 +0000508 def populate(self, depth=None, shallow=False, bootstrap=False,
509 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000510 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000511 if shallow and not depth:
512 depth = 10000
513 gclient_utils.safe_makedirs(self.GetCachePath())
514
Vadim Shtayura08049e22017-10-11 00:14:52 +0000515 lockfile = Lockfile(self.mirror_path, lock_timeout)
516 if not ignore_lock:
517 lockfile.lock()
518
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000519 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000520 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000521 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000522 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000523 self._fetch(rundir, verbose, depth)
hinokadcd84042016-06-09 14:26:17 -0700524 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000525 # This is a major failure, we need to clean and force a bootstrap.
526 gclient_utils.rmtree(rundir)
527 self.print(GIT_CACHE_CORRUPT_MESSAGE)
528 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
529 assert tempdir
Andrii Shyshkalov82d8dcd2017-11-22 17:07:28 -0800530 self._fetch(tempdir, verbose, depth)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000531 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000532 if tempdir:
dnjb445ef52016-11-10 15:51:39 -0800533 if os.path.exists(self.mirror_path):
534 gclient_utils.rmtree(self.mirror_path)
dnj4625b5a2016-11-10 18:23:26 -0800535 self.Rename(tempdir, self.mirror_path)
Vadim Shtayura08049e22017-10-11 00:14:52 +0000536 if not ignore_lock:
537 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000538
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000539 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000540 # The files are named <git number>.zip
541 gen_number = subprocess.check_output(
542 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000543 # Run Garbage Collect to compress packfile.
544 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000545 # Creating a temp file and then deleting it ensures we can use this name.
546 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
547 os.remove(tmp_zipfile)
548 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
549 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000550 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
551 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000552 gsutil.call('cp', tmp_zipfile, dest_name)
553 os.remove(tmp_zipfile)
554
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000555 # Remove all other files in the same directory.
556 if prune:
557 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
558 for filename in ls_out.splitlines():
559 if filename == dest_name:
560 continue
561 gsutil.call('rm', filename)
562
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000563 @staticmethod
564 def DeleteTmpPackFiles(path):
565 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000566 if not os.path.isdir(pack_dir):
567 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000568 pack_files = [f for f in os.listdir(pack_dir) if
569 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
570 for f in pack_files:
571 f = os.path.join(pack_dir, f)
572 try:
573 os.remove(f)
574 logging.warn('Deleted stale temporary pack file %s' % f)
575 except OSError:
576 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000577
Vadim Shtayura08049e22017-10-11 00:14:52 +0000578 @classmethod
579 def BreakLocks(cls, path):
580 did_unlock = False
581 lf = Lockfile(path)
582 if lf.break_lock():
583 did_unlock = True
584 # Look for lock files that might have been left behind by an interrupted
585 # git process.
586 lf = os.path.join(path, 'config.lock')
587 if os.path.exists(lf):
588 os.remove(lf)
589 did_unlock = True
590 cls.DeleteTmpPackFiles(path)
591 return did_unlock
592
593 def unlock(self):
594 return self.BreakLocks(self.mirror_path)
595
596 @classmethod
597 def UnlockAll(cls):
598 cachepath = cls.GetCachePath()
599 if not cachepath:
600 return
601 dirlist = os.listdir(cachepath)
602 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
603 if os.path.isdir(os.path.join(cachepath, path))])
604 for dirent in dirlist:
605 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
606 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
607 elif (dirent.endswith('.lock') and
608 os.path.isfile(os.path.join(cachepath, dirent))):
609 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
610
611 unlocked_repos = []
612 for repo_dir in repo_dirs:
613 if cls.BreakLocks(repo_dir):
614 unlocked_repos.append(repo_dir)
615
616 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000617
agable@chromium.org5a306a22014-02-24 22:13:59 +0000618@subcommand.usage('[url of repo to check for caching]')
619def CMDexists(parser, args):
620 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000621 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000622 if not len(args) == 1:
623 parser.error('git cache exists only takes exactly one repo url.')
624 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000625 mirror = Mirror(url)
626 if mirror.exists():
627 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000628 return 0
629 return 1
630
631
hinoka@google.com563559c2014-04-02 00:36:24 +0000632@subcommand.usage('[url of repo to create a bootstrap zip file]')
633def CMDupdate_bootstrap(parser, args):
634 """Create and uploads a bootstrap tarball."""
635 # Lets just assert we can't do this on Windows.
636 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000637 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000638 return 1
639
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000640 parser.add_option('--prune', action='store_true',
641 help='Prune all other cached zipballs of the same repo.')
642
hinoka@google.com563559c2014-04-02 00:36:24 +0000643 # First, we need to ensure the cache is populated.
644 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000645 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000646 CMDpopulate(parser, populate_args)
647
648 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000649 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000650 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000651 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000652 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000653 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000654
655
agable@chromium.org5a306a22014-02-24 22:13:59 +0000656@subcommand.usage('[url of repo to add to or update in cache]')
657def CMDpopulate(parser, args):
658 """Ensure that the cache has all up-to-date objects for the given repo."""
659 parser.add_option('--depth', type='int',
660 help='Only cache DEPTH commits of history')
661 parser.add_option('--shallow', '-s', action='store_true',
662 help='Only cache 10000 commits of history')
663 parser.add_option('--ref', action='append',
664 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000665 parser.add_option('--no_bootstrap', '--no-bootstrap',
666 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000667 help='Don\'t bootstrap from Google Storage')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000668 parser.add_option('--ignore_locks', '--ignore-locks',
669 action='store_true',
670 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000671
agable@chromium.org5a306a22014-02-24 22:13:59 +0000672 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000673 if not len(args) == 1:
674 parser.error('git cache populate only takes exactly one repo url.')
675 url = args[0]
676
szager@chromium.org848fd492014-04-09 19:06:44 +0000677 mirror = Mirror(url, refs=options.ref)
678 kwargs = {
679 'verbose': options.verbose,
680 'shallow': options.shallow,
681 'bootstrap': not options.no_bootstrap,
Vadim Shtayura08049e22017-10-11 00:14:52 +0000682 'ignore_lock': options.ignore_locks,
683 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000684 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000685 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000686 kwargs['depth'] = options.depth
687 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000688
689
szager@chromium.orgf3145112014-08-07 21:02:36 +0000690@subcommand.usage('Fetch new commits into cache and current checkout')
691def CMDfetch(parser, args):
692 """Update mirror, and fetch in cwd."""
693 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000694 parser.add_option('--no_bootstrap', '--no-bootstrap',
695 action='store_true',
696 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000697 options, args = parser.parse_args(args)
698
699 # Figure out which remotes to fetch. This mimics the behavior of regular
700 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
701 # this will NOT try to traverse up the branching structure to find the
702 # ultimate remote to update.
703 remotes = []
704 if options.all:
705 assert not args, 'fatal: fetch --all does not take a repository argument'
706 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
707 elif args:
708 remotes = args
709 else:
710 current_branch = subprocess.check_output(
711 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
712 if current_branch != 'HEAD':
713 upstream = subprocess.check_output(
714 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
715 ).strip()
716 if upstream and upstream != '.':
717 remotes = [upstream]
718 if not remotes:
719 remotes = ['origin']
720
721 cachepath = Mirror.GetCachePath()
722 git_dir = os.path.abspath(subprocess.check_output(
723 [Mirror.git_exe, 'rev-parse', '--git-dir']))
724 git_dir = os.path.abspath(git_dir)
725 if git_dir.startswith(cachepath):
726 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000727 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000728 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000729 return 0
730 for remote in remotes:
731 remote_url = subprocess.check_output(
732 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
733 if remote_url.startswith(cachepath):
734 mirror = Mirror.FromPath(remote_url)
735 mirror.print = lambda *args: None
736 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000737 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000738 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000739 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
740 return 0
741
742
Vadim Shtayura08049e22017-10-11 00:14:52 +0000743@subcommand.usage('[url of repo to unlock, or -a|--all]')
744def CMDunlock(parser, args):
745 """Unlock one or all repos if their lock files are still around."""
746 parser.add_option('--force', '-f', action='store_true',
747 help='Actually perform the action')
748 parser.add_option('--all', '-a', action='store_true',
749 help='Unlock all repository caches')
750 options, args = parser.parse_args(args)
751 if len(args) > 1 or (len(args) == 0 and not options.all):
752 parser.error('git cache unlock takes exactly one repo url, or --all')
753
754 if not options.force:
755 cachepath = Mirror.GetCachePath()
756 lockfiles = [os.path.join(cachepath, path)
757 for path in os.listdir(cachepath)
758 if path.endswith('.lock') and os.path.isfile(path)]
759 parser.error('git cache unlock requires -f|--force to do anything. '
760 'Refusing to unlock the following repo caches: '
761 ', '.join(lockfiles))
762
763 unlocked_repos = []
764 if options.all:
765 unlocked_repos.extend(Mirror.UnlockAll())
766 else:
767 m = Mirror(args[0])
768 if m.unlock():
769 unlocked_repos.append(m.mirror_path)
770
771 if unlocked_repos:
772 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
773 unlocked_repos))
774
775
agable@chromium.org5a306a22014-02-24 22:13:59 +0000776class OptionParser(optparse.OptionParser):
777 """Wrapper class for OptionParser to handle global options."""
778
779 def __init__(self, *args, **kwargs):
780 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
781 self.add_option('-c', '--cache-dir',
782 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000783 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000784 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000785 self.add_option('-q', '--quiet', action='store_true',
786 help='Suppress all extraneous output')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000787 self.add_option('--timeout', type='int', default=0,
788 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000789
790 def parse_args(self, args=None, values=None):
791 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000792 if options.quiet:
793 options.verbose = 0
794
795 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
796 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000797
798 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000799 global_cache_dir = Mirror.GetCachePath()
800 except RuntimeError:
801 global_cache_dir = None
802 if options.cache_dir:
803 if global_cache_dir and (
804 os.path.abspath(options.cache_dir) !=
805 os.path.abspath(global_cache_dir)):
806 logging.warn('Overriding globally-configured cache directory.')
807 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000808
agable@chromium.org5a306a22014-02-24 22:13:59 +0000809 return options, args
810
811
812def main(argv):
813 dispatcher = subcommand.CommandDispatcher(__name__)
814 return dispatcher.execute(OptionParser(), argv)
815
816
817if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000818 try:
819 sys.exit(main(sys.argv[1:]))
820 except KeyboardInterrupt:
821 sys.stderr.write('interrupted\n')
822 sys.exit(1)