blob: 15849039655e9ece1fff54fe8abf7c5bec1786dc [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
Takuto Ikuta9fce2132017-12-14 10:44:28 +090029
30# TODO(tikuta): remove or revert this (crbug.com/749709)
Andrii Shyshkalovd51ed572017-11-23 14:43:16 -080031if sys.platform.startswith('win'):
Takuto Ikuta9fce2132017-12-14 10:44:28 +090032 GC_AUTOPACKLIMIT = 30
szager@chromium.org301a7c32014-06-16 17:13:50 +000033
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000034GIT_CACHE_CORRUPT_MESSAGE = 'WARNING: The Git cache is corrupt.'
35
szager@chromium.org848fd492014-04-09 19:06:44 +000036try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080037 # pylint: disable=undefined-variable
szager@chromium.org848fd492014-04-09 19:06:44 +000038 WinErr = WindowsError
39except NameError:
40 class WinErr(Exception):
41 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000042
Vadim Shtayura08049e22017-10-11 00:14:52 +000043class LockError(Exception):
44 pass
45
hinokadcd84042016-06-09 14:26:17 -070046class ClobberNeeded(Exception):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000047 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000048
dnj4625b5a2016-11-10 18:23:26 -080049
50def exponential_backoff_retry(fn, excs=(Exception,), name=None, count=10,
51 sleep_time=0.25, printerr=None):
52 """Executes |fn| up to |count| times, backing off exponentially.
53
54 Args:
55 fn (callable): The function to execute. If this raises a handled
56 exception, the function will retry with exponential backoff.
57 excs (tuple): A tuple of Exception types to handle. If one of these is
58 raised by |fn|, a retry will be attempted. If |fn| raises an Exception
59 that is not in this list, it will immediately pass through. If |excs|
60 is empty, the Exception base class will be used.
61 name (str): Optional operation name to print in the retry string.
62 count (int): The number of times to try before allowing the exception to
63 pass through.
64 sleep_time (float): The initial number of seconds to sleep in between
65 retries. This will be doubled each retry.
66 printerr (callable): Function that will be called with the error string upon
67 failures. If None, |logging.warning| will be used.
68
69 Returns: The return value of the successful fn.
70 """
71 printerr = printerr or logging.warning
72 for i in xrange(count):
73 try:
74 return fn()
75 except excs as e:
76 if (i+1) >= count:
77 raise
78
79 printerr('Retrying %s in %.2f second(s) (%d / %d attempts): %s' % (
80 (name or 'operation'), sleep_time, (i+1), count, e))
81 time.sleep(sleep_time)
82 sleep_time *= 2
83
84
Vadim Shtayura08049e22017-10-11 00:14:52 +000085class Lockfile(object):
86 """Class to represent a cross-platform process-specific lockfile."""
87
88 def __init__(self, path, timeout=0):
89 self.path = os.path.abspath(path)
90 self.timeout = timeout
91 self.lockfile = self.path + ".lock"
92 self.pid = os.getpid()
93
94 def _read_pid(self):
95 """Read the pid stored in the lockfile.
96
97 Note: This method is potentially racy. By the time it returns the lockfile
98 may have been unlocked, removed, or stolen by some other process.
99 """
100 try:
101 with open(self.lockfile, 'r') as f:
102 pid = int(f.readline().strip())
103 except (IOError, ValueError):
104 pid = None
105 return pid
106
107 def _make_lockfile(self):
108 """Safely creates a lockfile containing the current pid."""
109 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
110 fd = os.open(self.lockfile, open_flags, 0o644)
111 f = os.fdopen(fd, 'w')
112 print(self.pid, file=f)
113 f.close()
114
115 def _remove_lockfile(self):
116 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
117
118 See gclient_utils.py:rmtree docstring for more explanation on the
119 windows case.
120 """
121 if sys.platform == 'win32':
122 lockfile = os.path.normcase(self.lockfile)
123
124 def delete():
125 exitcode = subprocess.call(['cmd.exe', '/c',
126 'del', '/f', '/q', lockfile])
127 if exitcode != 0:
128 raise LockError('Failed to remove lock: %s' % (lockfile,))
129 exponential_backoff_retry(
130 delete,
131 excs=(LockError,),
132 name='del [%s]' % (lockfile,))
133 else:
134 os.remove(self.lockfile)
135
136 def lock(self):
137 """Acquire the lock.
138
139 This will block with a deadline of self.timeout seconds.
140 """
141 elapsed = 0
142 while True:
143 try:
144 self._make_lockfile()
145 return
146 except OSError as e:
147 if elapsed < self.timeout:
148 sleep_time = max(10, min(3, self.timeout - elapsed))
149 logging.info('Could not create git cache lockfile; '
150 'will retry after sleep(%d).', sleep_time);
151 elapsed += sleep_time
152 time.sleep(sleep_time)
153 continue
154 if e.errno == errno.EEXIST:
155 raise LockError("%s is already locked" % self.path)
156 else:
157 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
158
159 def unlock(self):
160 """Release the lock."""
161 try:
162 if not self.is_locked():
163 raise LockError("%s is not locked" % self.path)
164 if not self.i_am_locking():
165 raise LockError("%s is locked, but not by me" % self.path)
166 self._remove_lockfile()
167 except WinErr:
168 # Windows is unreliable when it comes to file locking. YMMV.
169 pass
170
171 def break_lock(self):
172 """Remove the lock, even if it was created by someone else."""
173 try:
174 self._remove_lockfile()
175 return True
176 except OSError as exc:
177 if exc.errno == errno.ENOENT:
178 return False
179 else:
180 raise
181
182 def is_locked(self):
183 """Test if the file is locked by anyone.
184
185 Note: This method is potentially racy. By the time it returns the lockfile
186 may have been unlocked, removed, or stolen by some other process.
187 """
188 return os.path.exists(self.lockfile)
189
190 def i_am_locking(self):
191 """Test if the file is locked by this process."""
192 return self.is_locked() and self.pid == self._read_pid()
193
194
szager@chromium.org848fd492014-04-09 19:06:44 +0000195class Mirror(object):
196
197 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
198 gsutil_exe = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +0000199 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000200 cachepath_lock = threading.Lock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000201
szager@chromium.org66c8b852015-09-22 23:19:07 +0000202 @staticmethod
203 def parse_fetch_spec(spec):
204 """Parses and canonicalizes a fetch spec.
205
206 Returns (fetchspec, value_regex), where value_regex can be used
207 with 'git config --replace-all'.
208 """
209 parts = spec.split(':', 1)
210 src = parts[0].lstrip('+').rstrip('/')
211 if not src.startswith('refs/'):
212 src = 'refs/heads/%s' % src
213 dest = parts[1].rstrip('/') if len(parts) > 1 else src
214 regex = r'\+%s:.*' % src.replace('*', r'\*')
215 return ('+%s:%s' % (src, dest), regex)
216
szager@chromium.org848fd492014-04-09 19:06:44 +0000217 def __init__(self, url, refs=None, print_func=None):
218 self.url = url
szager@chromium.org66c8b852015-09-22 23:19:07 +0000219 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
szager@chromium.org848fd492014-04-09 19:06:44 +0000220 self.basedir = self.UrlToCacheDir(url)
221 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000222 if print_func:
223 self.print = self.print_without_file
224 self.print_func = print_func
225 else:
226 self.print = print
227
dnj4625b5a2016-11-10 18:23:26 -0800228 def print_without_file(self, message, **_kwargs):
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000229 self.print_func(message)
szager@chromium.org848fd492014-04-09 19:06:44 +0000230
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800231 @contextlib.contextmanager
232 def print_duration_of(self, what):
233 start = time.time()
234 try:
235 yield
236 finally:
237 self.print('%s took %.1f minutes' % (what, (time.time() - start) / 60.0))
238
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000239 @property
240 def bootstrap_bucket(self):
Ryan Tseng3beabd02017-03-15 13:57:58 -0700241 u = urlparse.urlparse(self.url)
242 if u.netloc == 'chromium.googlesource.com':
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000243 return 'chromium-git-cache'
Ryan Tseng3beabd02017-03-15 13:57:58 -0700244 elif u.netloc == 'chrome-internal.googlesource.com':
245 return 'chrome-git-cache'
246 # Not recognized.
247 return None
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000248
szager@chromium.org174766f2014-05-13 21:27:46 +0000249 @classmethod
250 def FromPath(cls, path):
251 return cls(cls.CacheDirToUrl(path))
252
szager@chromium.org848fd492014-04-09 19:06:44 +0000253 @staticmethod
254 def UrlToCacheDir(url):
255 """Convert a git url to a normalized form for the cache dir path."""
256 parsed = urlparse.urlparse(url)
257 norm_url = parsed.netloc + parsed.path
258 if norm_url.endswith('.git'):
259 norm_url = norm_url[:-len('.git')]
260 return norm_url.replace('-', '--').replace('/', '-').lower()
261
262 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000263 def CacheDirToUrl(path):
264 """Convert a cache dir path to its corresponding url."""
265 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
266 return 'https://%s' % netpath
267
szager@chromium.org848fd492014-04-09 19:06:44 +0000268 @classmethod
269 def SetCachePath(cls, cachepath):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000270 with cls.cachepath_lock:
271 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000272
273 @classmethod
274 def GetCachePath(cls):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000275 with cls.cachepath_lock:
276 if not hasattr(cls, 'cachepath'):
277 try:
278 cachepath = subprocess.check_output(
279 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
280 except subprocess.CalledProcessError:
281 cachepath = None
282 if not cachepath:
283 raise RuntimeError(
284 'No global cache.cachepath git configuration found.')
285 setattr(cls, 'cachepath', cachepath)
286 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000287
dnj4625b5a2016-11-10 18:23:26 -0800288 def Rename(self, src, dst):
289 # This is somehow racy on Windows.
290 # Catching OSError because WindowsError isn't portable and
291 # pylint complains.
292 exponential_backoff_retry(
293 lambda: os.rename(src, dst),
294 excs=(OSError,),
295 name='rename [%s] => [%s]' % (src, dst),
296 printerr=self.print)
297
szager@chromium.org848fd492014-04-09 19:06:44 +0000298 def RunGit(self, cmd, **kwargs):
299 """Run git in a subprocess."""
300 cwd = kwargs.setdefault('cwd', self.mirror_path)
301 kwargs.setdefault('print_stdout', False)
302 kwargs.setdefault('filter_fn', self.print)
303 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
304 env.setdefault('GIT_ASKPASS', 'true')
305 env.setdefault('SSH_ASKPASS', 'true')
306 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
307 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
308
309 def config(self, cwd=None):
310 if cwd is None:
311 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000312
313 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinokadcd84042016-06-09 14:26:17 -0700314 try:
315 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
316 except subprocess.CalledProcessError:
317 # Hard error, need to clobber.
318 raise ClobberNeeded()
szager@chromium.org301a7c32014-06-16 17:13:50 +0000319
320 # Don't combine pack files into one big pack file. It's really slow for
321 # repositories, and there's no way to track progress and make sure it's
322 # not stuck.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700323 if self.supported_project():
324 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000325
326 # Allocate more RAM for cache-ing delta chains, for better performance
327 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000328 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000329 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000330
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000331 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000332 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000333 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000334 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000335 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000336 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000337 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000338
339 def bootstrap_repo(self, directory):
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800340 """Bootstrap the repo from Google Storage if possible.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000341
342 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
343 """
Ryan Tseng3beabd02017-03-15 13:57:58 -0700344 if not self.bootstrap_bucket:
345 return False
hinoka@google.com776a2c32014-04-25 07:54:25 +0000346 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000347 if (sys.platform.startswith('win') and
348 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000349 python_fallback = True
350 elif sys.platform.startswith('darwin'):
351 # The OSX version of unzip doesn't support zip64.
352 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000353 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000354 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000355
356 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000357 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000358 # Get the most recent version of the zipfile.
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800359 _, ls_out, ls_err = gsutil.check_call('ls', gs_folder)
szager@chromium.org848fd492014-04-09 19:06:44 +0000360 ls_out_sorted = sorted(ls_out.splitlines())
361 if not ls_out_sorted:
362 # This repo is not on Google Storage.
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800363 self.print('No bootstrap file for %s found in %s, stderr:\n %s' %
364 (self.mirror_path, self.bootstrap_bucket,
365 ' '.join((ls_err or '').splitlines(True))))
szager@chromium.org848fd492014-04-09 19:06:44 +0000366 return False
367 latest_checkout = ls_out_sorted[-1]
368
369 # Download zip file to a temporary directory.
370 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000371 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000372 self.print('Downloading %s' % latest_checkout)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800373 with self.print_duration_of('download'):
374 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000375 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000376 return False
377 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
378
hinoka@google.com776a2c32014-04-25 07:54:25 +0000379 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800380 with self.print_duration_of('unzip'):
381 if not python_fallback:
382 if sys.platform.startswith('win'):
383 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
384 else:
385 cmd = ['unzip', filename, '-d', directory]
386 retcode = subprocess.call(cmd)
hinoka@google.com776a2c32014-04-25 07:54:25 +0000387 else:
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800388 try:
389 with zipfile.ZipFile(filename, 'r') as f:
390 f.printdir()
391 f.extractall(directory)
392 except Exception as e:
393 self.print('Encountered error: %s' % str(e), file=sys.stderr)
394 retcode = 1
395 else:
396 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000397 finally:
398 # Clean up the downloaded zipfile.
dnj4625b5a2016-11-10 18:23:26 -0800399 #
400 # This is somehow racy on Windows.
401 # Catching OSError because WindowsError isn't portable and
402 # pylint complains.
403 exponential_backoff_retry(
404 lambda: gclient_utils.rm_file_or_tree(tempdir),
405 excs=(OSError,),
406 name='rmtree [%s]' % (tempdir,),
407 printerr=self.print)
szager@chromium.org848fd492014-04-09 19:06:44 +0000408
409 if retcode:
410 self.print(
411 'Extracting bootstrap zipfile %s failed.\n'
412 'Resuming normal operations.' % filename)
413 return False
414 return True
415
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800416 def contains_revision(self, revision):
417 if not self.exists():
418 return False
419
420 if sys.platform.startswith('win'):
421 # Windows .bat scripts use ^ as escape sequence, which means we have to
422 # escape it with itself for every .bat invocation.
423 needle = '%s^^^^{commit}' % revision
424 else:
425 needle = '%s^{commit}' % revision
426 try:
427 # cat-file exits with 0 on success, that is git object of given hash was
428 # found.
429 self.RunGit(['cat-file', '-e', needle])
430 return True
431 except subprocess.CalledProcessError:
432 return False
433
szager@chromium.org848fd492014-04-09 19:06:44 +0000434 def exists(self):
435 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
436
Ryan Tseng3beabd02017-03-15 13:57:58 -0700437 def supported_project(self):
438 """Returns true if this repo is known to have a bootstrap zip file."""
439 u = urlparse.urlparse(self.url)
440 return u.netloc in [
441 'chromium.googlesource.com',
442 'chrome-internal.googlesource.com']
443
szager@chromium.org66c8b852015-09-22 23:19:07 +0000444 def _preserve_fetchspec(self):
445 """Read and preserve remote.origin.fetch from an existing mirror.
446
447 This modifies self.fetch_specs.
448 """
449 if not self.exists():
450 return
451 try:
452 config_fetchspecs = subprocess.check_output(
453 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
454 cwd=self.mirror_path)
455 for fetchspec in config_fetchspecs.splitlines():
456 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
457 except subprocess.CalledProcessError:
458 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
459 'existing cache directory. You may need to manually edit '
460 '%s and "git cache fetch" again.'
461 % os.path.join(self.mirror_path, 'config'))
462
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000463 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
464 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000465 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
466 pack_files = []
467
468 if os.path.isdir(pack_dir):
469 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800470 self.print('%s has %d .pack files, re-bootstrapping if >%d' %
471 (self.mirror_path, len(pack_files), GC_AUTOPACKLIMIT))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000472
473 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000474 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000475 len(pack_files) > GC_AUTOPACKLIMIT)
476 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000477 if self.exists():
478 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
479 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000480 tempdir = tempfile.mkdtemp(
481 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
482 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
483 if bootstrapped:
484 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000485 gclient_utils.rmtree(self.mirror_path)
Ryan Tseng3beabd02017-03-15 13:57:58 -0700486 elif not self.exists() or not self.supported_project():
487 # Bootstrap failed due to either
488 # 1. No previous cache
489 # 2. Project doesn't have a bootstrap zip file
490 # Start with a bare git dir.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000491 self.RunGit(['init', '--bare'], cwd=tempdir)
492 else:
493 # Bootstrap failed, previous cache exists; warn and continue.
494 logging.warn(
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800495 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
496 'but failed. Continuing with non-optimized repository.'
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000497 % len(pack_files))
498 gclient_utils.rmtree(tempdir)
499 tempdir = None
500 else:
501 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
502 logging.warn(
503 'Shallow fetch requested, but repo cache already exists.')
504 return tempdir
505
506 def _fetch(self, rundir, verbose, depth):
507 self.config(rundir)
508 v = []
509 d = []
510 if verbose:
511 v = ['-v', '--progress']
512 if depth:
513 d = ['--depth', str(depth)]
514 fetch_cmd = ['fetch'] + v + d + ['origin']
515 fetch_specs = subprocess.check_output(
516 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
517 cwd=rundir).strip().splitlines()
518 for spec in fetch_specs:
519 try:
520 self.print('Fetching %s' % spec)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800521 with self.print_duration_of('fetch %s' % spec):
522 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000523 except subprocess.CalledProcessError:
524 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700525 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000526 logging.warn('Fetch of %s failed' % spec)
527
Vadim Shtayura08049e22017-10-11 00:14:52 +0000528 def populate(self, depth=None, shallow=False, bootstrap=False,
529 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000530 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000531 if shallow and not depth:
532 depth = 10000
533 gclient_utils.safe_makedirs(self.GetCachePath())
534
Vadim Shtayura08049e22017-10-11 00:14:52 +0000535 lockfile = Lockfile(self.mirror_path, lock_timeout)
536 if not ignore_lock:
537 lockfile.lock()
538
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000539 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000540 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000541 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000542 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000543 self._fetch(rundir, verbose, depth)
hinokadcd84042016-06-09 14:26:17 -0700544 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000545 # This is a major failure, we need to clean and force a bootstrap.
546 gclient_utils.rmtree(rundir)
547 self.print(GIT_CACHE_CORRUPT_MESSAGE)
548 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
549 assert tempdir
Andrii Shyshkalov82d8dcd2017-11-22 17:07:28 -0800550 self._fetch(tempdir, verbose, depth)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000551 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000552 if tempdir:
dnjb445ef52016-11-10 15:51:39 -0800553 if os.path.exists(self.mirror_path):
554 gclient_utils.rmtree(self.mirror_path)
dnj4625b5a2016-11-10 18:23:26 -0800555 self.Rename(tempdir, self.mirror_path)
Vadim Shtayura08049e22017-10-11 00:14:52 +0000556 if not ignore_lock:
557 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000558
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000559 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000560 # The files are named <git number>.zip
561 gen_number = subprocess.check_output(
562 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000563 # Run Garbage Collect to compress packfile.
564 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000565 # Creating a temp file and then deleting it ensures we can use this name.
566 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
567 os.remove(tmp_zipfile)
568 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
569 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000570 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
571 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000572 gsutil.call('cp', tmp_zipfile, dest_name)
573 os.remove(tmp_zipfile)
574
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000575 # Remove all other files in the same directory.
576 if prune:
577 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
578 for filename in ls_out.splitlines():
579 if filename == dest_name:
580 continue
581 gsutil.call('rm', filename)
582
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000583 @staticmethod
584 def DeleteTmpPackFiles(path):
585 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000586 if not os.path.isdir(pack_dir):
587 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000588 pack_files = [f for f in os.listdir(pack_dir) if
589 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
590 for f in pack_files:
591 f = os.path.join(pack_dir, f)
592 try:
593 os.remove(f)
594 logging.warn('Deleted stale temporary pack file %s' % f)
595 except OSError:
596 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000597
Vadim Shtayura08049e22017-10-11 00:14:52 +0000598 @classmethod
599 def BreakLocks(cls, path):
600 did_unlock = False
601 lf = Lockfile(path)
602 if lf.break_lock():
603 did_unlock = True
604 # Look for lock files that might have been left behind by an interrupted
605 # git process.
606 lf = os.path.join(path, 'config.lock')
607 if os.path.exists(lf):
608 os.remove(lf)
609 did_unlock = True
610 cls.DeleteTmpPackFiles(path)
611 return did_unlock
612
613 def unlock(self):
614 return self.BreakLocks(self.mirror_path)
615
616 @classmethod
617 def UnlockAll(cls):
618 cachepath = cls.GetCachePath()
619 if not cachepath:
620 return
621 dirlist = os.listdir(cachepath)
622 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
623 if os.path.isdir(os.path.join(cachepath, path))])
624 for dirent in dirlist:
625 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
626 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
627 elif (dirent.endswith('.lock') and
628 os.path.isfile(os.path.join(cachepath, dirent))):
629 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
630
631 unlocked_repos = []
632 for repo_dir in repo_dirs:
633 if cls.BreakLocks(repo_dir):
634 unlocked_repos.append(repo_dir)
635
636 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000637
agable@chromium.org5a306a22014-02-24 22:13:59 +0000638@subcommand.usage('[url of repo to check for caching]')
639def CMDexists(parser, args):
640 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000641 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000642 if not len(args) == 1:
643 parser.error('git cache exists only takes exactly one repo url.')
644 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000645 mirror = Mirror(url)
646 if mirror.exists():
647 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000648 return 0
649 return 1
650
651
hinoka@google.com563559c2014-04-02 00:36:24 +0000652@subcommand.usage('[url of repo to create a bootstrap zip file]')
653def CMDupdate_bootstrap(parser, args):
654 """Create and uploads a bootstrap tarball."""
655 # Lets just assert we can't do this on Windows.
656 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000657 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000658 return 1
659
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000660 parser.add_option('--prune', action='store_true',
661 help='Prune all other cached zipballs of the same repo.')
662
hinoka@google.com563559c2014-04-02 00:36:24 +0000663 # First, we need to ensure the cache is populated.
664 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000665 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000666 CMDpopulate(parser, populate_args)
667
668 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000669 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000670 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000671 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000672 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000673 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000674
675
agable@chromium.org5a306a22014-02-24 22:13:59 +0000676@subcommand.usage('[url of repo to add to or update in cache]')
677def CMDpopulate(parser, args):
678 """Ensure that the cache has all up-to-date objects for the given repo."""
679 parser.add_option('--depth', type='int',
680 help='Only cache DEPTH commits of history')
681 parser.add_option('--shallow', '-s', action='store_true',
682 help='Only cache 10000 commits of history')
683 parser.add_option('--ref', action='append',
684 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000685 parser.add_option('--no_bootstrap', '--no-bootstrap',
686 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000687 help='Don\'t bootstrap from Google Storage')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000688 parser.add_option('--ignore_locks', '--ignore-locks',
689 action='store_true',
690 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000691
agable@chromium.org5a306a22014-02-24 22:13:59 +0000692 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000693 if not len(args) == 1:
694 parser.error('git cache populate only takes exactly one repo url.')
695 url = args[0]
696
szager@chromium.org848fd492014-04-09 19:06:44 +0000697 mirror = Mirror(url, refs=options.ref)
698 kwargs = {
699 'verbose': options.verbose,
700 'shallow': options.shallow,
701 'bootstrap': not options.no_bootstrap,
Vadim Shtayura08049e22017-10-11 00:14:52 +0000702 'ignore_lock': options.ignore_locks,
703 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000704 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000705 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000706 kwargs['depth'] = options.depth
707 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000708
709
szager@chromium.orgf3145112014-08-07 21:02:36 +0000710@subcommand.usage('Fetch new commits into cache and current checkout')
711def CMDfetch(parser, args):
712 """Update mirror, and fetch in cwd."""
713 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000714 parser.add_option('--no_bootstrap', '--no-bootstrap',
715 action='store_true',
716 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000717 options, args = parser.parse_args(args)
718
719 # Figure out which remotes to fetch. This mimics the behavior of regular
720 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
721 # this will NOT try to traverse up the branching structure to find the
722 # ultimate remote to update.
723 remotes = []
724 if options.all:
725 assert not args, 'fatal: fetch --all does not take a repository argument'
726 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
727 elif args:
728 remotes = args
729 else:
730 current_branch = subprocess.check_output(
731 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
732 if current_branch != 'HEAD':
733 upstream = subprocess.check_output(
734 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
735 ).strip()
736 if upstream and upstream != '.':
737 remotes = [upstream]
738 if not remotes:
739 remotes = ['origin']
740
741 cachepath = Mirror.GetCachePath()
742 git_dir = os.path.abspath(subprocess.check_output(
743 [Mirror.git_exe, 'rev-parse', '--git-dir']))
744 git_dir = os.path.abspath(git_dir)
745 if git_dir.startswith(cachepath):
746 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000747 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000748 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000749 return 0
750 for remote in remotes:
751 remote_url = subprocess.check_output(
752 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
753 if remote_url.startswith(cachepath):
754 mirror = Mirror.FromPath(remote_url)
755 mirror.print = lambda *args: None
756 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000757 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000758 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000759 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
760 return 0
761
762
Vadim Shtayura08049e22017-10-11 00:14:52 +0000763@subcommand.usage('[url of repo to unlock, or -a|--all]')
764def CMDunlock(parser, args):
765 """Unlock one or all repos if their lock files are still around."""
766 parser.add_option('--force', '-f', action='store_true',
767 help='Actually perform the action')
768 parser.add_option('--all', '-a', action='store_true',
769 help='Unlock all repository caches')
770 options, args = parser.parse_args(args)
771 if len(args) > 1 or (len(args) == 0 and not options.all):
772 parser.error('git cache unlock takes exactly one repo url, or --all')
773
774 if not options.force:
775 cachepath = Mirror.GetCachePath()
776 lockfiles = [os.path.join(cachepath, path)
777 for path in os.listdir(cachepath)
778 if path.endswith('.lock') and os.path.isfile(path)]
779 parser.error('git cache unlock requires -f|--force to do anything. '
780 'Refusing to unlock the following repo caches: '
781 ', '.join(lockfiles))
782
783 unlocked_repos = []
784 if options.all:
785 unlocked_repos.extend(Mirror.UnlockAll())
786 else:
787 m = Mirror(args[0])
788 if m.unlock():
789 unlocked_repos.append(m.mirror_path)
790
791 if unlocked_repos:
792 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
793 unlocked_repos))
794
795
agable@chromium.org5a306a22014-02-24 22:13:59 +0000796class OptionParser(optparse.OptionParser):
797 """Wrapper class for OptionParser to handle global options."""
798
799 def __init__(self, *args, **kwargs):
800 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
801 self.add_option('-c', '--cache-dir',
802 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000803 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000804 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000805 self.add_option('-q', '--quiet', action='store_true',
806 help='Suppress all extraneous output')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000807 self.add_option('--timeout', type='int', default=0,
808 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000809
810 def parse_args(self, args=None, values=None):
811 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000812 if options.quiet:
813 options.verbose = 0
814
815 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
816 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000817
818 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000819 global_cache_dir = Mirror.GetCachePath()
820 except RuntimeError:
821 global_cache_dir = None
822 if options.cache_dir:
823 if global_cache_dir and (
824 os.path.abspath(options.cache_dir) !=
825 os.path.abspath(global_cache_dir)):
826 logging.warn('Overriding globally-configured cache directory.')
827 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000828
agable@chromium.org5a306a22014-02-24 22:13:59 +0000829 return options, args
830
831
832def main(argv):
833 dispatcher = subcommand.CommandDispatcher(__name__)
834 return dispatcher.execute(OptionParser(), argv)
835
836
837if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000838 try:
839 sys.exit(main(sys.argv[1:]))
840 except KeyboardInterrupt:
841 sys.stderr.write('interrupted\n')
842 sys.exit(1)