blob: 01b6842e3d4b2ef5bd2cb02966691e08fda5d021 [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
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800414 def contains_revision(self, revision):
415 if not self.exists():
416 return False
417
418 if sys.platform.startswith('win'):
419 # Windows .bat scripts use ^ as escape sequence, which means we have to
420 # escape it with itself for every .bat invocation.
421 needle = '%s^^^^{commit}' % revision
422 else:
423 needle = '%s^{commit}' % revision
424 try:
425 # cat-file exits with 0 on success, that is git object of given hash was
426 # found.
427 self.RunGit(['cat-file', '-e', needle])
428 return True
429 except subprocess.CalledProcessError:
430 return False
431
szager@chromium.org848fd492014-04-09 19:06:44 +0000432 def exists(self):
433 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
434
Ryan Tseng3beabd02017-03-15 13:57:58 -0700435 def supported_project(self):
436 """Returns true if this repo is known to have a bootstrap zip file."""
437 u = urlparse.urlparse(self.url)
438 return u.netloc in [
439 'chromium.googlesource.com',
440 'chrome-internal.googlesource.com']
441
szager@chromium.org66c8b852015-09-22 23:19:07 +0000442 def _preserve_fetchspec(self):
443 """Read and preserve remote.origin.fetch from an existing mirror.
444
445 This modifies self.fetch_specs.
446 """
447 if not self.exists():
448 return
449 try:
450 config_fetchspecs = subprocess.check_output(
451 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
452 cwd=self.mirror_path)
453 for fetchspec in config_fetchspecs.splitlines():
454 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
455 except subprocess.CalledProcessError:
456 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
457 'existing cache directory. You may need to manually edit '
458 '%s and "git cache fetch" again.'
459 % os.path.join(self.mirror_path, 'config'))
460
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000461 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
462 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000463 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
464 pack_files = []
465
466 if os.path.isdir(pack_dir):
467 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800468 self.print('%s has %d .pack files, re-bootstrapping if >%d' %
469 (self.mirror_path, len(pack_files), GC_AUTOPACKLIMIT))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000470
471 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000472 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000473 len(pack_files) > GC_AUTOPACKLIMIT)
474 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000475 if self.exists():
476 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
477 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000478 tempdir = tempfile.mkdtemp(
479 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
480 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
481 if bootstrapped:
482 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000483 gclient_utils.rmtree(self.mirror_path)
Ryan Tseng3beabd02017-03-15 13:57:58 -0700484 elif not self.exists() or not self.supported_project():
485 # Bootstrap failed due to either
486 # 1. No previous cache
487 # 2. Project doesn't have a bootstrap zip file
488 # Start with a bare git dir.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000489 self.RunGit(['init', '--bare'], cwd=tempdir)
490 else:
491 # Bootstrap failed, previous cache exists; warn and continue.
492 logging.warn(
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800493 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
494 'but failed. Continuing with non-optimized repository.'
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000495 % len(pack_files))
496 gclient_utils.rmtree(tempdir)
497 tempdir = None
498 else:
499 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
500 logging.warn(
501 'Shallow fetch requested, but repo cache already exists.')
502 return tempdir
503
504 def _fetch(self, rundir, verbose, depth):
505 self.config(rundir)
506 v = []
507 d = []
508 if verbose:
509 v = ['-v', '--progress']
510 if depth:
511 d = ['--depth', str(depth)]
512 fetch_cmd = ['fetch'] + v + d + ['origin']
513 fetch_specs = subprocess.check_output(
514 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
515 cwd=rundir).strip().splitlines()
516 for spec in fetch_specs:
517 try:
518 self.print('Fetching %s' % spec)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800519 with self.print_duration_of('fetch %s' % spec):
520 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000521 except subprocess.CalledProcessError:
522 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700523 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000524 logging.warn('Fetch of %s failed' % spec)
525
Vadim Shtayura08049e22017-10-11 00:14:52 +0000526 def populate(self, depth=None, shallow=False, bootstrap=False,
527 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000528 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000529 if shallow and not depth:
530 depth = 10000
531 gclient_utils.safe_makedirs(self.GetCachePath())
532
Vadim Shtayura08049e22017-10-11 00:14:52 +0000533 lockfile = Lockfile(self.mirror_path, lock_timeout)
534 if not ignore_lock:
535 lockfile.lock()
536
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000537 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000538 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000539 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000540 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000541 self._fetch(rundir, verbose, depth)
hinokadcd84042016-06-09 14:26:17 -0700542 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000543 # This is a major failure, we need to clean and force a bootstrap.
544 gclient_utils.rmtree(rundir)
545 self.print(GIT_CACHE_CORRUPT_MESSAGE)
546 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
547 assert tempdir
Andrii Shyshkalov82d8dcd2017-11-22 17:07:28 -0800548 self._fetch(tempdir, verbose, depth)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000549 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000550 if tempdir:
dnjb445ef52016-11-10 15:51:39 -0800551 if os.path.exists(self.mirror_path):
552 gclient_utils.rmtree(self.mirror_path)
dnj4625b5a2016-11-10 18:23:26 -0800553 self.Rename(tempdir, self.mirror_path)
Vadim Shtayura08049e22017-10-11 00:14:52 +0000554 if not ignore_lock:
555 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000556
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000557 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000558 # The files are named <git number>.zip
559 gen_number = subprocess.check_output(
560 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000561 # Run Garbage Collect to compress packfile.
562 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000563 # Creating a temp file and then deleting it ensures we can use this name.
564 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
565 os.remove(tmp_zipfile)
566 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
567 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000568 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
569 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000570 gsutil.call('cp', tmp_zipfile, dest_name)
571 os.remove(tmp_zipfile)
572
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000573 # Remove all other files in the same directory.
574 if prune:
575 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
576 for filename in ls_out.splitlines():
577 if filename == dest_name:
578 continue
579 gsutil.call('rm', filename)
580
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000581 @staticmethod
582 def DeleteTmpPackFiles(path):
583 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000584 if not os.path.isdir(pack_dir):
585 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000586 pack_files = [f for f in os.listdir(pack_dir) if
587 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
588 for f in pack_files:
589 f = os.path.join(pack_dir, f)
590 try:
591 os.remove(f)
592 logging.warn('Deleted stale temporary pack file %s' % f)
593 except OSError:
594 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000595
Vadim Shtayura08049e22017-10-11 00:14:52 +0000596 @classmethod
597 def BreakLocks(cls, path):
598 did_unlock = False
599 lf = Lockfile(path)
600 if lf.break_lock():
601 did_unlock = True
602 # Look for lock files that might have been left behind by an interrupted
603 # git process.
604 lf = os.path.join(path, 'config.lock')
605 if os.path.exists(lf):
606 os.remove(lf)
607 did_unlock = True
608 cls.DeleteTmpPackFiles(path)
609 return did_unlock
610
611 def unlock(self):
612 return self.BreakLocks(self.mirror_path)
613
614 @classmethod
615 def UnlockAll(cls):
616 cachepath = cls.GetCachePath()
617 if not cachepath:
618 return
619 dirlist = os.listdir(cachepath)
620 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
621 if os.path.isdir(os.path.join(cachepath, path))])
622 for dirent in dirlist:
623 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
624 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
625 elif (dirent.endswith('.lock') and
626 os.path.isfile(os.path.join(cachepath, dirent))):
627 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
628
629 unlocked_repos = []
630 for repo_dir in repo_dirs:
631 if cls.BreakLocks(repo_dir):
632 unlocked_repos.append(repo_dir)
633
634 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000635
agable@chromium.org5a306a22014-02-24 22:13:59 +0000636@subcommand.usage('[url of repo to check for caching]')
637def CMDexists(parser, args):
638 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000639 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000640 if not len(args) == 1:
641 parser.error('git cache exists only takes exactly one repo url.')
642 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000643 mirror = Mirror(url)
644 if mirror.exists():
645 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000646 return 0
647 return 1
648
649
hinoka@google.com563559c2014-04-02 00:36:24 +0000650@subcommand.usage('[url of repo to create a bootstrap zip file]')
651def CMDupdate_bootstrap(parser, args):
652 """Create and uploads a bootstrap tarball."""
653 # Lets just assert we can't do this on Windows.
654 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000655 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000656 return 1
657
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000658 parser.add_option('--prune', action='store_true',
659 help='Prune all other cached zipballs of the same repo.')
660
hinoka@google.com563559c2014-04-02 00:36:24 +0000661 # First, we need to ensure the cache is populated.
662 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000663 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000664 CMDpopulate(parser, populate_args)
665
666 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000667 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000668 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000669 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000670 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000671 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000672
673
agable@chromium.org5a306a22014-02-24 22:13:59 +0000674@subcommand.usage('[url of repo to add to or update in cache]')
675def CMDpopulate(parser, args):
676 """Ensure that the cache has all up-to-date objects for the given repo."""
677 parser.add_option('--depth', type='int',
678 help='Only cache DEPTH commits of history')
679 parser.add_option('--shallow', '-s', action='store_true',
680 help='Only cache 10000 commits of history')
681 parser.add_option('--ref', action='append',
682 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000683 parser.add_option('--no_bootstrap', '--no-bootstrap',
684 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000685 help='Don\'t bootstrap from Google Storage')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000686 parser.add_option('--ignore_locks', '--ignore-locks',
687 action='store_true',
688 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000689
agable@chromium.org5a306a22014-02-24 22:13:59 +0000690 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000691 if not len(args) == 1:
692 parser.error('git cache populate only takes exactly one repo url.')
693 url = args[0]
694
szager@chromium.org848fd492014-04-09 19:06:44 +0000695 mirror = Mirror(url, refs=options.ref)
696 kwargs = {
697 'verbose': options.verbose,
698 'shallow': options.shallow,
699 'bootstrap': not options.no_bootstrap,
Vadim Shtayura08049e22017-10-11 00:14:52 +0000700 'ignore_lock': options.ignore_locks,
701 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000702 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000703 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000704 kwargs['depth'] = options.depth
705 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000706
707
szager@chromium.orgf3145112014-08-07 21:02:36 +0000708@subcommand.usage('Fetch new commits into cache and current checkout')
709def CMDfetch(parser, args):
710 """Update mirror, and fetch in cwd."""
711 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000712 parser.add_option('--no_bootstrap', '--no-bootstrap',
713 action='store_true',
714 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000715 options, args = parser.parse_args(args)
716
717 # Figure out which remotes to fetch. This mimics the behavior of regular
718 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
719 # this will NOT try to traverse up the branching structure to find the
720 # ultimate remote to update.
721 remotes = []
722 if options.all:
723 assert not args, 'fatal: fetch --all does not take a repository argument'
724 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
725 elif args:
726 remotes = args
727 else:
728 current_branch = subprocess.check_output(
729 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
730 if current_branch != 'HEAD':
731 upstream = subprocess.check_output(
732 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
733 ).strip()
734 if upstream and upstream != '.':
735 remotes = [upstream]
736 if not remotes:
737 remotes = ['origin']
738
739 cachepath = Mirror.GetCachePath()
740 git_dir = os.path.abspath(subprocess.check_output(
741 [Mirror.git_exe, 'rev-parse', '--git-dir']))
742 git_dir = os.path.abspath(git_dir)
743 if git_dir.startswith(cachepath):
744 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000745 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000746 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000747 return 0
748 for remote in remotes:
749 remote_url = subprocess.check_output(
750 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
751 if remote_url.startswith(cachepath):
752 mirror = Mirror.FromPath(remote_url)
753 mirror.print = lambda *args: None
754 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000755 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000756 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000757 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
758 return 0
759
760
Vadim Shtayura08049e22017-10-11 00:14:52 +0000761@subcommand.usage('[url of repo to unlock, or -a|--all]')
762def CMDunlock(parser, args):
763 """Unlock one or all repos if their lock files are still around."""
764 parser.add_option('--force', '-f', action='store_true',
765 help='Actually perform the action')
766 parser.add_option('--all', '-a', action='store_true',
767 help='Unlock all repository caches')
768 options, args = parser.parse_args(args)
769 if len(args) > 1 or (len(args) == 0 and not options.all):
770 parser.error('git cache unlock takes exactly one repo url, or --all')
771
772 if not options.force:
773 cachepath = Mirror.GetCachePath()
774 lockfiles = [os.path.join(cachepath, path)
775 for path in os.listdir(cachepath)
776 if path.endswith('.lock') and os.path.isfile(path)]
777 parser.error('git cache unlock requires -f|--force to do anything. '
778 'Refusing to unlock the following repo caches: '
779 ', '.join(lockfiles))
780
781 unlocked_repos = []
782 if options.all:
783 unlocked_repos.extend(Mirror.UnlockAll())
784 else:
785 m = Mirror(args[0])
786 if m.unlock():
787 unlocked_repos.append(m.mirror_path)
788
789 if unlocked_repos:
790 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
791 unlocked_repos))
792
793
agable@chromium.org5a306a22014-02-24 22:13:59 +0000794class OptionParser(optparse.OptionParser):
795 """Wrapper class for OptionParser to handle global options."""
796
797 def __init__(self, *args, **kwargs):
798 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
799 self.add_option('-c', '--cache-dir',
800 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000801 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000802 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000803 self.add_option('-q', '--quiet', action='store_true',
804 help='Suppress all extraneous output')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000805 self.add_option('--timeout', type='int', default=0,
806 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000807
808 def parse_args(self, args=None, values=None):
809 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000810 if options.quiet:
811 options.verbose = 0
812
813 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
814 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000815
816 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000817 global_cache_dir = Mirror.GetCachePath()
818 except RuntimeError:
819 global_cache_dir = None
820 if options.cache_dir:
821 if global_cache_dir and (
822 os.path.abspath(options.cache_dir) !=
823 os.path.abspath(global_cache_dir)):
824 logging.warn('Overriding globally-configured cache directory.')
825 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000826
agable@chromium.org5a306a22014-02-24 22:13:59 +0000827 return options, args
828
829
830def main(argv):
831 dispatcher = subcommand.CommandDispatcher(__name__)
832 return dispatcher.execute(OptionParser(), argv)
833
834
835if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000836 try:
837 sys.exit(main(sys.argv[1:]))
838 except KeyboardInterrupt:
839 sys.stderr.write('interrupted\n')
840 sys.exit(1)