blob: 4a1ec03a059a0230a3bb045a4d1a4b0092c2b5ca [file] [log] [blame]
Edward Lesmes98eda3f2019-08-12 21:09:53 +00001#!/usr/bin/env python
agable@chromium.org5a306a22014-02-24 22:13:59 +00002# 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
Raul Tambreb946b232019-03-26 14:48:46 +00009
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -080010import contextlib
agable@chromium.org5a306a22014-02-24 22:13:59 +000011import errno
12import logging
13import optparse
14import os
szager@chromium.org174766f2014-05-13 21:27:46 +000015import re
John Budorick47ec0692019-05-01 15:04:28 +000016import subprocess
17import sys
agable@chromium.org5a306a22014-02-24 22:13:59 +000018import tempfile
szager@chromium.org1132f5f2014-08-23 01:57:59 +000019import threading
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000020import time
Raul Tambreb946b232019-03-26 14:48:46 +000021
22try:
23 import urlparse
24except ImportError: # For Py3 compatibility
25 import urllib.parse as urlparse
26
hinoka@google.com563559c2014-04-02 00:36:24 +000027from download_from_google_storage import Gsutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000028import gclient_utils
29import subcommand
30
szager@chromium.org301a7c32014-06-16 17:13:50 +000031# Analogous to gc.autopacklimit git config.
32GC_AUTOPACKLIMIT = 50
Takuto Ikuta9fce2132017-12-14 10:44:28 +090033
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
Edward Lesmes451e8ba2019-10-01 22:15:33 +000072 for i in range(count):
dnj4625b5a2016-11-10 18:23:26 -080073 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
Robert Iannuccia19649b2018-06-29 16:31:45 +0000202 UNSET_CACHEPATH = object()
203
204 # Used for tests
205 _GIT_CONFIG_LOCATION = []
206
szager@chromium.org66c8b852015-09-22 23:19:07 +0000207 @staticmethod
208 def parse_fetch_spec(spec):
209 """Parses and canonicalizes a fetch spec.
210
211 Returns (fetchspec, value_regex), where value_regex can be used
212 with 'git config --replace-all'.
213 """
214 parts = spec.split(':', 1)
215 src = parts[0].lstrip('+').rstrip('/')
216 if not src.startswith('refs/'):
217 src = 'refs/heads/%s' % src
218 dest = parts[1].rstrip('/') if len(parts) > 1 else src
219 regex = r'\+%s:.*' % src.replace('*', r'\*')
220 return ('+%s:%s' % (src, dest), regex)
221
szager@chromium.org848fd492014-04-09 19:06:44 +0000222 def __init__(self, url, refs=None, print_func=None):
223 self.url = url
szager@chromium.org66c8b852015-09-22 23:19:07 +0000224 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
szager@chromium.org848fd492014-04-09 19:06:44 +0000225 self.basedir = self.UrlToCacheDir(url)
226 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000227 if print_func:
228 self.print = self.print_without_file
229 self.print_func = print_func
230 else:
231 self.print = print
232
dnj4625b5a2016-11-10 18:23:26 -0800233 def print_without_file(self, message, **_kwargs):
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000234 self.print_func(message)
szager@chromium.org848fd492014-04-09 19:06:44 +0000235
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800236 @contextlib.contextmanager
237 def print_duration_of(self, what):
238 start = time.time()
239 try:
240 yield
241 finally:
242 self.print('%s took %.1f minutes' % (what, (time.time() - start) / 60.0))
243
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000244 @property
245 def bootstrap_bucket(self):
Andrii Shyshkalov4b79c382019-04-15 23:48:35 +0000246 b = os.getenv('OVERRIDE_BOOTSTRAP_BUCKET')
247 if b:
248 return b
Ryan Tseng3beabd02017-03-15 13:57:58 -0700249 u = urlparse.urlparse(self.url)
250 if u.netloc == 'chromium.googlesource.com':
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000251 return 'chromium-git-cache'
Andrii Shyshkalov4b79c382019-04-15 23:48:35 +0000252 # TODO(tandrii): delete once LUCI migration is completed.
253 # Only public hosts will be supported going forward.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700254 elif u.netloc == 'chrome-internal.googlesource.com':
255 return 'chrome-git-cache'
256 # Not recognized.
257 return None
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000258
Karen Qiandcad7492019-04-26 03:11:16 +0000259 @property
260 def _gs_path(self):
261 return 'gs://%s/v2/%s' % (self.bootstrap_bucket, self.basedir)
262
szager@chromium.org174766f2014-05-13 21:27:46 +0000263 @classmethod
264 def FromPath(cls, path):
265 return cls(cls.CacheDirToUrl(path))
266
szager@chromium.org848fd492014-04-09 19:06:44 +0000267 @staticmethod
268 def UrlToCacheDir(url):
269 """Convert a git url to a normalized form for the cache dir path."""
270 parsed = urlparse.urlparse(url)
Edward Lesmes451e8ba2019-10-01 22:15:33 +0000271 # Get rid of the port. This is only needed for Windows tests, since tests
272 # serve git from git://localhost:port/git, but Windows doesn't like ':' in
273 # paths.
274 netloc = parsed.netloc
275 if ':' in netloc:
276 netloc = netloc.split(':', 1)[0]
277 norm_url = netloc + parsed.path
szager@chromium.org848fd492014-04-09 19:06:44 +0000278 if norm_url.endswith('.git'):
279 norm_url = norm_url[:-len('.git')]
Dirk Prankedb589542019-04-12 21:07:01 +0000280
281 # Use the same dir for authenticated URLs and unauthenticated URLs.
282 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
283
szager@chromium.org848fd492014-04-09 19:06:44 +0000284 return norm_url.replace('-', '--').replace('/', '-').lower()
285
286 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000287 def CacheDirToUrl(path):
288 """Convert a cache dir path to its corresponding url."""
289 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
290 return 'https://%s' % netpath
291
szager@chromium.org848fd492014-04-09 19:06:44 +0000292 @classmethod
293 def SetCachePath(cls, cachepath):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000294 with cls.cachepath_lock:
295 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000296
297 @classmethod
298 def GetCachePath(cls):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000299 with cls.cachepath_lock:
300 if not hasattr(cls, 'cachepath'):
301 try:
302 cachepath = subprocess.check_output(
Robert Iannuccia19649b2018-06-29 16:31:45 +0000303 [cls.git_exe, 'config'] +
304 cls._GIT_CONFIG_LOCATION +
305 ['cache.cachepath']).strip()
Vadim Shtayura08049e22017-10-11 00:14:52 +0000306 except subprocess.CalledProcessError:
Robert Iannuccia19649b2018-06-29 16:31:45 +0000307 cachepath = os.environ.get('GIT_CACHE_PATH', cls.UNSET_CACHEPATH)
Vadim Shtayura08049e22017-10-11 00:14:52 +0000308 setattr(cls, 'cachepath', cachepath)
Robert Iannuccia19649b2018-06-29 16:31:45 +0000309
310 ret = getattr(cls, 'cachepath')
311 if ret is cls.UNSET_CACHEPATH:
312 raise RuntimeError('No cache.cachepath git configuration or '
313 '$GIT_CACHE_PATH is set.')
314 return ret
szager@chromium.org848fd492014-04-09 19:06:44 +0000315
Karen Qianccd2b4d2019-05-03 22:25:59 +0000316 @staticmethod
317 def _GetMostRecentCacheDirectory(ls_out_set):
318 ready_file_pattern = re.compile(r'.*/(\d+).ready$')
319 ready_dirs = []
320
321 for name in ls_out_set:
322 m = ready_file_pattern.match(name)
323 # Given <path>/<number>.ready,
324 # we are interested in <path>/<number> directory
325 if m and (name[:-len('.ready')] + '/') in ls_out_set:
326 ready_dirs.append((int(m.group(1)), name[:-len('.ready')]))
327
328 if not ready_dirs:
329 return None
330
331 return max(ready_dirs)[1]
332
dnj4625b5a2016-11-10 18:23:26 -0800333 def Rename(self, src, dst):
334 # This is somehow racy on Windows.
335 # Catching OSError because WindowsError isn't portable and
336 # pylint complains.
337 exponential_backoff_retry(
338 lambda: os.rename(src, dst),
339 excs=(OSError,),
340 name='rename [%s] => [%s]' % (src, dst),
341 printerr=self.print)
342
szager@chromium.org848fd492014-04-09 19:06:44 +0000343 def RunGit(self, cmd, **kwargs):
344 """Run git in a subprocess."""
345 cwd = kwargs.setdefault('cwd', self.mirror_path)
346 kwargs.setdefault('print_stdout', False)
347 kwargs.setdefault('filter_fn', self.print)
348 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
349 env.setdefault('GIT_ASKPASS', 'true')
350 env.setdefault('SSH_ASKPASS', 'true')
351 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
352 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
353
Edward Lemur579c9862018-07-13 23:17:51 +0000354 def config(self, cwd=None, reset_fetch_config=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000355 if cwd is None:
356 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000357
Erik Chenfd843fa2019-10-17 21:48:37 +0000358 # Print diagnostics and ignore errors.
359 try:
360 self.print('git exe: %s' % (self.git_exe,))
361 self.RunGit(['version'], cwd=cwd)
362 self.RunGit(['config', 'protocol.version'], cwd=cwd)
363 except subprocess.CalledProcessError as e:
364 pass
365
Edward Lemur579c9862018-07-13 23:17:51 +0000366 if reset_fetch_config:
Edward Lemur2f38df62018-07-14 02:13:21 +0000367 try:
368 self.RunGit(['config', '--unset-all', 'remote.origin.fetch'], cwd=cwd)
369 except subprocess.CalledProcessError as e:
370 # If exit code was 5, it means we attempted to unset a config that
371 # didn't exist. Ignore it.
372 if e.returncode != 5:
373 raise
Edward Lemur579c9862018-07-13 23:17:51 +0000374
szager@chromium.org301a7c32014-06-16 17:13:50 +0000375 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinokadcd84042016-06-09 14:26:17 -0700376 try:
377 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
378 except subprocess.CalledProcessError:
379 # Hard error, need to clobber.
380 raise ClobberNeeded()
szager@chromium.org301a7c32014-06-16 17:13:50 +0000381
382 # Don't combine pack files into one big pack file. It's really slow for
383 # repositories, and there's no way to track progress and make sure it's
384 # not stuck.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700385 if self.supported_project():
386 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000387
388 # Allocate more RAM for cache-ing delta chains, for better performance
389 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000390 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000391 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000392
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000393 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000394 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000395 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000396 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000397 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000398 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000399 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000400
401 def bootstrap_repo(self, directory):
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800402 """Bootstrap the repo from Google Storage if possible.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000403
404 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
405 """
Ryan Tseng3beabd02017-03-15 13:57:58 -0700406 if not self.bootstrap_bucket:
407 return False
szager@chromium.org848fd492014-04-09 19:06:44 +0000408
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000409 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
Yuwei Huanga1fbdff2019-02-01 21:51:15 +0000410
Karen Qian0cbd5a52019-04-29 20:14:50 +0000411 # Get the most recent version of the directory.
412 # This is determined from the most recent version of a .ready file.
413 # The .ready file is only uploaded when an entire directory has been
414 # uploaded to GS.
415 _, ls_out, ls_err = gsutil.check_call('ls', self._gs_path)
Karen Qianccd2b4d2019-05-03 22:25:59 +0000416 ls_out_set = set(ls_out.strip().splitlines())
417 latest_dir = self._GetMostRecentCacheDirectory(ls_out_set)
Yuwei Huanga1fbdff2019-02-01 21:51:15 +0000418
Karen Qianccd2b4d2019-05-03 22:25:59 +0000419 if not latest_dir:
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800420 self.print('No bootstrap file for %s found in %s, stderr:\n %s' %
421 (self.mirror_path, self.bootstrap_bucket,
Karen Qian0cbd5a52019-04-29 20:14:50 +0000422 ' '.join((ls_err or '').splitlines(True))))
szager@chromium.org848fd492014-04-09 19:06:44 +0000423 return False
szager@chromium.org848fd492014-04-09 19:06:44 +0000424
szager@chromium.org848fd492014-04-09 19:06:44 +0000425 try:
Karen Qian0cbd5a52019-04-29 20:14:50 +0000426 # create new temporary directory locally
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000427 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
Karen Qian0cbd5a52019-04-29 20:14:50 +0000428 self.RunGit(['init', '--bare'], cwd=tempdir)
429 self.print('Downloading files in %s/* into %s.' %
430 (latest_dir, tempdir))
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800431 with self.print_duration_of('download'):
Karen Qian0cbd5a52019-04-29 20:14:50 +0000432 code = gsutil.call('-m', 'cp', '-r', latest_dir + "/*",
433 tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000434 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000435 return False
Karen Qian0cbd5a52019-04-29 20:14:50 +0000436 except Exception as e:
437 self.print('Encountered error: %s' % str(e), file=sys.stderr)
438 gclient_utils.rmtree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000439 return False
Karen Qian0cbd5a52019-04-29 20:14:50 +0000440 # delete the old directory
441 if os.path.exists(directory):
442 gclient_utils.rmtree(directory)
443 self.Rename(tempdir, directory)
szager@chromium.org848fd492014-04-09 19:06:44 +0000444 return True
445
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800446 def contains_revision(self, revision):
447 if not self.exists():
448 return False
449
450 if sys.platform.startswith('win'):
451 # Windows .bat scripts use ^ as escape sequence, which means we have to
452 # escape it with itself for every .bat invocation.
453 needle = '%s^^^^{commit}' % revision
454 else:
455 needle = '%s^{commit}' % revision
456 try:
457 # cat-file exits with 0 on success, that is git object of given hash was
458 # found.
459 self.RunGit(['cat-file', '-e', needle])
460 return True
461 except subprocess.CalledProcessError:
462 return False
463
szager@chromium.org848fd492014-04-09 19:06:44 +0000464 def exists(self):
465 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
466
Ryan Tseng3beabd02017-03-15 13:57:58 -0700467 def supported_project(self):
468 """Returns true if this repo is known to have a bootstrap zip file."""
469 u = urlparse.urlparse(self.url)
470 return u.netloc in [
471 'chromium.googlesource.com',
472 'chrome-internal.googlesource.com']
473
szager@chromium.org66c8b852015-09-22 23:19:07 +0000474 def _preserve_fetchspec(self):
475 """Read and preserve remote.origin.fetch from an existing mirror.
476
477 This modifies self.fetch_specs.
478 """
479 if not self.exists():
480 return
481 try:
482 config_fetchspecs = subprocess.check_output(
483 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
484 cwd=self.mirror_path)
485 for fetchspec in config_fetchspecs.splitlines():
486 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
487 except subprocess.CalledProcessError:
488 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
489 'existing cache directory. You may need to manually edit '
490 '%s and "git cache fetch" again.'
491 % os.path.join(self.mirror_path, 'config'))
492
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000493 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000494 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
495 pack_files = []
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000496 if os.path.isdir(pack_dir):
497 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800498 self.print('%s has %d .pack files, re-bootstrapping if >%d' %
Karen Qian0cbd5a52019-04-29 20:14:50 +0000499 (self.mirror_path, len(pack_files), GC_AUTOPACKLIMIT))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000500
501 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000502 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000503 len(pack_files) > GC_AUTOPACKLIMIT)
Karen Qian0cbd5a52019-04-29 20:14:50 +0000504
505 if not should_bootstrap:
506 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
507 logging.warn(
508 'Shallow fetch requested, but repo cache already exists.')
509 return
510
511 if self.exists():
512 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
513 self._preserve_fetchspec()
514 else:
John Budorick47ec0692019-05-01 15:04:28 +0000515 if os.path.exists(self.mirror_path):
516 # If the mirror path exists but self.exists() returns false, we're
517 # in an unexpected state. Nuke the previous mirror directory and
518 # start fresh.
519 gclient_utils.rmtree(self.mirror_path)
Karen Qian0cbd5a52019-04-29 20:14:50 +0000520 os.mkdir(self.mirror_path)
521
522 bootstrapped = (not depth and bootstrap and
523 self.bootstrap_repo(self.mirror_path))
524
525 if not bootstrapped:
526 if not self.exists() or not self.supported_project():
527 # Bootstrap failed due to:
528 # 1. No previous cache.
529 # 2. Project doesn't have a bootstrap folder.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700530 # Start with a bare git dir.
Karen Qian0cbd5a52019-04-29 20:14:50 +0000531 self.RunGit(['init', '--bare'], cwd=self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000532 else:
533 # Bootstrap failed, previous cache exists; warn and continue.
534 logging.warn(
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800535 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
536 'but failed. Continuing with non-optimized repository.'
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000537 % len(pack_files))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000538
danakjc41f72c2019-11-05 17:12:01 +0000539 def _fetch(self, rundir, verbose, depth, no_fetch_tags, reset_fetch_config):
Edward Lemur579c9862018-07-13 23:17:51 +0000540 self.config(rundir, reset_fetch_config)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000541 v = []
542 d = []
danakjc41f72c2019-11-05 17:12:01 +0000543 t = []
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000544 if verbose:
545 v = ['-v', '--progress']
546 if depth:
547 d = ['--depth', str(depth)]
danakjc41f72c2019-11-05 17:12:01 +0000548 if no_fetch_tags:
549 t = ['--no-tags']
550 fetch_cmd = ['fetch'] + v + d + t + ['origin']
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000551 fetch_specs = subprocess.check_output(
552 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
553 cwd=rundir).strip().splitlines()
554 for spec in fetch_specs:
Edward Lemurdf746d02019-07-27 00:42:46 +0000555 spec = spec.decode()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000556 try:
557 self.print('Fetching %s' % spec)
John Budorick48823c22019-10-30 19:48:57 +0000558 env = os.environ.copy()
559 env.update({
560 'GIT_TRACE_PACKET': '1',
561 'GIT_TR2_EVENT': '1',
562 'GIT_TRACE2_EVENT': '1',
563 'GIT_TRACE_CURL': '1',
564 'GIT_TRACE_CURL_NO_DATA': '1'
565 })
Erik Chenfcde3ba2019-10-18 01:12:11 +0000566 # Only print first 30000 packets. We can use nonlocal keyword once we
Erik Chenfd843fa2019-10-17 21:48:37 +0000567 # switch to python 3.
568 packet_count = [0]
569
570 def FilterPacket(log_line):
571 if 'packet:' in log_line:
572 packet_count[0] += 1
Erik Chenfcde3ba2019-10-18 01:12:11 +0000573 if packet_count[0] == 30000:
Erik Chenfd843fa2019-10-17 21:48:37 +0000574 self.print('Truncating remaining packets')
Erik Chenfcde3ba2019-10-18 01:12:11 +0000575 if packet_count[0] >= 30000:
Erik Chenfd843fa2019-10-17 21:48:37 +0000576 return
577 self.print(log_line)
578
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800579 with self.print_duration_of('fetch %s' % spec):
Erik Chenfd843fa2019-10-17 21:48:37 +0000580 self.RunGit(
581 fetch_cmd + [spec],
582 cwd=rundir,
583 retry=True,
John Budorick48823c22019-10-30 19:48:57 +0000584 env=env,
Erik Chenfd843fa2019-10-17 21:48:37 +0000585 filter_fn=FilterPacket)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000586 except subprocess.CalledProcessError:
587 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700588 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000589 logging.warn('Fetch of %s failed' % spec)
590
danakjc41f72c2019-11-05 17:12:01 +0000591 def populate(self,
592 depth=None,
593 no_fetch_tags=False,
594 shallow=False,
595 bootstrap=False,
596 verbose=False,
597 ignore_lock=False,
598 lock_timeout=0,
Edward Lemur579c9862018-07-13 23:17:51 +0000599 reset_fetch_config=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000600 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000601 if shallow and not depth:
602 depth = 10000
603 gclient_utils.safe_makedirs(self.GetCachePath())
604
Vadim Shtayura08049e22017-10-11 00:14:52 +0000605 lockfile = Lockfile(self.mirror_path, lock_timeout)
606 if not ignore_lock:
607 lockfile.lock()
608
szager@chromium.org108eced2014-06-19 21:22:43 +0000609 try:
Karen Qian0cbd5a52019-04-29 20:14:50 +0000610 self._ensure_bootstrapped(depth, bootstrap)
danakjc41f72c2019-11-05 17:12:01 +0000611 self._fetch(self.mirror_path, verbose, depth, no_fetch_tags,
612 reset_fetch_config)
hinokadcd84042016-06-09 14:26:17 -0700613 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000614 # This is a major failure, we need to clean and force a bootstrap.
Karen Qian0cbd5a52019-04-29 20:14:50 +0000615 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000616 self.print(GIT_CACHE_CORRUPT_MESSAGE)
Karen Qian0cbd5a52019-04-29 20:14:50 +0000617 self._ensure_bootstrapped(depth, bootstrap, force=True)
danakjc41f72c2019-11-05 17:12:01 +0000618 self._fetch(self.mirror_path, verbose, depth, no_fetch_tags,
619 reset_fetch_config)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000620 finally:
Vadim Shtayura08049e22017-10-11 00:14:52 +0000621 if not ignore_lock:
622 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000623
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000624 def update_bootstrap(self, prune=False, gc_aggressive=False):
Karen Qiandcad7492019-04-26 03:11:16 +0000625 # The folder is <git number>
szager@chromium.org848fd492014-04-09 19:06:44 +0000626 gen_number = subprocess.check_output(
627 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
Karen Qiandcad7492019-04-26 03:11:16 +0000628 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
629
630 src_name = self.mirror_path
Karen Qianccd2b4d2019-05-03 22:25:59 +0000631 dest_prefix = '%s/%s' % (self._gs_path, gen_number)
Karen Qiandcad7492019-04-26 03:11:16 +0000632
Karen Qianccd2b4d2019-05-03 22:25:59 +0000633 # ls_out lists contents in the format: gs://blah/blah/123...
634 _, ls_out, _ = gsutil.check_call('ls', self._gs_path)
Karen Qiandcad7492019-04-26 03:11:16 +0000635
Karen Qianccd2b4d2019-05-03 22:25:59 +0000636 # Check to see if folder already exists in gs
637 ls_out_set = set(ls_out.strip().splitlines())
638 if (dest_prefix + '/' in ls_out_set and
639 dest_prefix + '.ready' in ls_out_set):
640 print('Cache %s already exists.' % dest_prefix)
Karen Qiandcad7492019-04-26 03:11:16 +0000641 return
642
Andrii Shyshkalov199182f2019-04-26 16:01:20 +0000643 # Run Garbage Collect to compress packfile.
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000644 gc_args = ['gc', '--prune=all']
645 if gc_aggressive:
646 gc_args.append('--aggressive')
647 self.RunGit(gc_args)
Andrii Shyshkalov199182f2019-04-26 16:01:20 +0000648
Karen Qianccd2b4d2019-05-03 22:25:59 +0000649 gsutil.call('-m', 'cp', '-r', src_name, dest_prefix)
Karen Qiandcad7492019-04-26 03:11:16 +0000650
Karen Qianccd2b4d2019-05-03 22:25:59 +0000651 # Create .ready file and upload
Karen Qiandcad7492019-04-26 03:11:16 +0000652 _, ready_file_name = tempfile.mkstemp(suffix='.ready')
653 try:
Karen Qianccd2b4d2019-05-03 22:25:59 +0000654 gsutil.call('cp', ready_file_name, '%s.ready' % (dest_prefix))
Karen Qiandcad7492019-04-26 03:11:16 +0000655 finally:
656 os.remove(ready_file_name)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000657
Karen Qianccd2b4d2019-05-03 22:25:59 +0000658 # remove all other directory/.ready files in the same gs_path
659 # except for the directory/.ready file previously created
660 # which can be used for bootstrapping while the current one is
661 # being uploaded
662 if not prune:
663 return
664 prev_dest_prefix = self._GetMostRecentCacheDirectory(ls_out_set)
665 if not prev_dest_prefix:
666 return
667 for path in ls_out_set:
668 if (path == prev_dest_prefix + '/' or
669 path == prev_dest_prefix + '.ready'):
670 continue
671 if path.endswith('.ready'):
672 gsutil.call('rm', path)
673 continue
674 gsutil.call('-m', 'rm', '-r', path)
675
676
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000677 @staticmethod
678 def DeleteTmpPackFiles(path):
679 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000680 if not os.path.isdir(pack_dir):
681 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000682 pack_files = [f for f in os.listdir(pack_dir) if
683 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
684 for f in pack_files:
685 f = os.path.join(pack_dir, f)
686 try:
687 os.remove(f)
688 logging.warn('Deleted stale temporary pack file %s' % f)
689 except OSError:
690 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000691
Vadim Shtayura08049e22017-10-11 00:14:52 +0000692 @classmethod
693 def BreakLocks(cls, path):
694 did_unlock = False
695 lf = Lockfile(path)
696 if lf.break_lock():
697 did_unlock = True
698 # Look for lock files that might have been left behind by an interrupted
699 # git process.
700 lf = os.path.join(path, 'config.lock')
701 if os.path.exists(lf):
702 os.remove(lf)
703 did_unlock = True
704 cls.DeleteTmpPackFiles(path)
705 return did_unlock
706
707 def unlock(self):
708 return self.BreakLocks(self.mirror_path)
709
710 @classmethod
711 def UnlockAll(cls):
712 cachepath = cls.GetCachePath()
713 if not cachepath:
714 return
715 dirlist = os.listdir(cachepath)
716 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
717 if os.path.isdir(os.path.join(cachepath, path))])
718 for dirent in dirlist:
719 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
720 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
721 elif (dirent.endswith('.lock') and
722 os.path.isfile(os.path.join(cachepath, dirent))):
723 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
724
725 unlocked_repos = []
726 for repo_dir in repo_dirs:
727 if cls.BreakLocks(repo_dir):
728 unlocked_repos.append(repo_dir)
729
730 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000731
agable@chromium.org5a306a22014-02-24 22:13:59 +0000732@subcommand.usage('[url of repo to check for caching]')
733def CMDexists(parser, args):
734 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000735 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000736 if not len(args) == 1:
737 parser.error('git cache exists only takes exactly one repo url.')
738 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000739 mirror = Mirror(url)
740 if mirror.exists():
741 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000742 return 0
743 return 1
744
745
hinoka@google.com563559c2014-04-02 00:36:24 +0000746@subcommand.usage('[url of repo to create a bootstrap zip file]')
747def CMDupdate_bootstrap(parser, args):
748 """Create and uploads a bootstrap tarball."""
749 # Lets just assert we can't do this on Windows.
750 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000751 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000752 return 1
753
Robert Iannucci0081c0f2019-09-29 08:30:54 +0000754 parser.add_option('--skip-populate', action='store_true',
755 help='Skips "populate" step if mirror already exists.')
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000756 parser.add_option('--gc-aggressive', action='store_true',
757 help='Run aggressive repacking of the repo.')
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000758 parser.add_option('--prune', action='store_true',
Andrii Shyshkalov7a2205c2019-04-26 05:14:36 +0000759 help='Prune all other cached bundles of the same repo.')
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000760
hinoka@google.com563559c2014-04-02 00:36:24 +0000761 populate_args = args[:]
Robert Iannucci0081c0f2019-09-29 08:30:54 +0000762 options, args = parser.parse_args(args)
763 url = args[0]
764 mirror = Mirror(url)
765 if not options.skip_populate or not mirror.exists():
766 CMDpopulate(parser, populate_args)
767 else:
768 print('Skipped populate step.')
hinoka@google.com563559c2014-04-02 00:36:24 +0000769
770 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000771 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000772 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000773 mirror = Mirror(url)
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000774 mirror.update_bootstrap(options.prune, options.gc_aggressive)
szager@chromium.org848fd492014-04-09 19:06:44 +0000775 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000776
777
agable@chromium.org5a306a22014-02-24 22:13:59 +0000778@subcommand.usage('[url of repo to add to or update in cache]')
779def CMDpopulate(parser, args):
780 """Ensure that the cache has all up-to-date objects for the given repo."""
781 parser.add_option('--depth', type='int',
782 help='Only cache DEPTH commits of history')
danakjc41f72c2019-11-05 17:12:01 +0000783 parser.add_option(
784 '--no-fetch-tags',
785 action='store_true',
786 help=('Don\'t fetch tags from the server. This can speed up '
787 'fetch considerably when there are many tags.'))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000788 parser.add_option('--shallow', '-s', action='store_true',
789 help='Only cache 10000 commits of history')
790 parser.add_option('--ref', action='append',
791 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000792 parser.add_option('--no_bootstrap', '--no-bootstrap',
793 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000794 help='Don\'t bootstrap from Google Storage')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000795 parser.add_option('--ignore_locks', '--ignore-locks',
796 action='store_true',
797 help='Don\'t try to lock repository')
Robert Iannucci09315982019-10-05 08:12:03 +0000798 parser.add_option('--break-locks',
799 action='store_true',
800 help='Break any existing lock instead of just ignoring it')
Edward Lemur579c9862018-07-13 23:17:51 +0000801 parser.add_option('--reset-fetch-config', action='store_true', default=False,
802 help='Reset the fetch config before populating the cache.')
hinoka@google.com563559c2014-04-02 00:36:24 +0000803
agable@chromium.org5a306a22014-02-24 22:13:59 +0000804 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000805 if not len(args) == 1:
806 parser.error('git cache populate only takes exactly one repo url.')
807 url = args[0]
808
szager@chromium.org848fd492014-04-09 19:06:44 +0000809 mirror = Mirror(url, refs=options.ref)
Robert Iannucci09315982019-10-05 08:12:03 +0000810 if options.break_locks:
811 mirror.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000812 kwargs = {
danakjc41f72c2019-11-05 17:12:01 +0000813 'no_fetch_tags': options.no_fetch_tags,
szager@chromium.org848fd492014-04-09 19:06:44 +0000814 'verbose': options.verbose,
815 'shallow': options.shallow,
816 'bootstrap': not options.no_bootstrap,
Vadim Shtayura08049e22017-10-11 00:14:52 +0000817 'ignore_lock': options.ignore_locks,
818 'lock_timeout': options.timeout,
Edward Lemur579c9862018-07-13 23:17:51 +0000819 'reset_fetch_config': options.reset_fetch_config,
szager@chromium.org848fd492014-04-09 19:06:44 +0000820 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000821 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000822 kwargs['depth'] = options.depth
823 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000824
825
szager@chromium.orgf3145112014-08-07 21:02:36 +0000826@subcommand.usage('Fetch new commits into cache and current checkout')
827def CMDfetch(parser, args):
828 """Update mirror, and fetch in cwd."""
829 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000830 parser.add_option('--no_bootstrap', '--no-bootstrap',
831 action='store_true',
832 help='Don\'t (re)bootstrap from Google Storage')
danakjc41f72c2019-11-05 17:12:01 +0000833 parser.add_option(
834 '--no-fetch-tags',
835 action='store_true',
836 help=('Don\'t fetch tags from the server. This can speed up '
837 'fetch considerably when there are many tags.'))
szager@chromium.orgf3145112014-08-07 21:02:36 +0000838 options, args = parser.parse_args(args)
839
840 # Figure out which remotes to fetch. This mimics the behavior of regular
841 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
842 # this will NOT try to traverse up the branching structure to find the
843 # ultimate remote to update.
844 remotes = []
845 if options.all:
846 assert not args, 'fatal: fetch --all does not take a repository argument'
847 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
848 elif args:
849 remotes = args
850 else:
851 current_branch = subprocess.check_output(
852 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
853 if current_branch != 'HEAD':
854 upstream = subprocess.check_output(
855 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
856 ).strip()
857 if upstream and upstream != '.':
858 remotes = [upstream]
859 if not remotes:
860 remotes = ['origin']
861
862 cachepath = Mirror.GetCachePath()
863 git_dir = os.path.abspath(subprocess.check_output(
864 [Mirror.git_exe, 'rev-parse', '--git-dir']))
865 git_dir = os.path.abspath(git_dir)
866 if git_dir.startswith(cachepath):
867 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000868 mirror.populate(
danakjc41f72c2019-11-05 17:12:01 +0000869 bootstrap=not options.no_bootstrap,
870 no_fetch_tags=options.no_fetch_tags,
871 lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000872 return 0
873 for remote in remotes:
874 remote_url = subprocess.check_output(
875 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
876 if remote_url.startswith(cachepath):
877 mirror = Mirror.FromPath(remote_url)
878 mirror.print = lambda *args: None
879 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000880 mirror.populate(
danakjc41f72c2019-11-05 17:12:01 +0000881 bootstrap=not options.no_bootstrap,
882 no_fetch_tags=options.no_fetch_tags,
883 lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000884 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
885 return 0
886
887
Vadim Shtayura08049e22017-10-11 00:14:52 +0000888@subcommand.usage('[url of repo to unlock, or -a|--all]')
889def CMDunlock(parser, args):
890 """Unlock one or all repos if their lock files are still around."""
891 parser.add_option('--force', '-f', action='store_true',
892 help='Actually perform the action')
893 parser.add_option('--all', '-a', action='store_true',
894 help='Unlock all repository caches')
895 options, args = parser.parse_args(args)
896 if len(args) > 1 or (len(args) == 0 and not options.all):
897 parser.error('git cache unlock takes exactly one repo url, or --all')
898
899 if not options.force:
900 cachepath = Mirror.GetCachePath()
901 lockfiles = [os.path.join(cachepath, path)
902 for path in os.listdir(cachepath)
903 if path.endswith('.lock') and os.path.isfile(path)]
904 parser.error('git cache unlock requires -f|--force to do anything. '
905 'Refusing to unlock the following repo caches: '
906 ', '.join(lockfiles))
907
908 unlocked_repos = []
909 if options.all:
910 unlocked_repos.extend(Mirror.UnlockAll())
911 else:
912 m = Mirror(args[0])
913 if m.unlock():
914 unlocked_repos.append(m.mirror_path)
915
916 if unlocked_repos:
917 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
918 unlocked_repos))
919
920
agable@chromium.org5a306a22014-02-24 22:13:59 +0000921class OptionParser(optparse.OptionParser):
922 """Wrapper class for OptionParser to handle global options."""
923
924 def __init__(self, *args, **kwargs):
925 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
926 self.add_option('-c', '--cache-dir',
Robert Iannuccia19649b2018-06-29 16:31:45 +0000927 help=(
928 'Path to the directory containing the caches. Normally '
929 'deduced from git config cache.cachepath or '
930 '$GIT_CACHE_PATH.'))
szager@chromium.org2c391af2014-05-23 09:07:15 +0000931 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000932 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000933 self.add_option('-q', '--quiet', action='store_true',
934 help='Suppress all extraneous output')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000935 self.add_option('--timeout', type='int', default=0,
936 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000937
938 def parse_args(self, args=None, values=None):
939 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000940 if options.quiet:
941 options.verbose = 0
942
943 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
944 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000945
946 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000947 global_cache_dir = Mirror.GetCachePath()
948 except RuntimeError:
949 global_cache_dir = None
950 if options.cache_dir:
951 if global_cache_dir and (
952 os.path.abspath(options.cache_dir) !=
953 os.path.abspath(global_cache_dir)):
954 logging.warn('Overriding globally-configured cache directory.')
955 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000956
agable@chromium.org5a306a22014-02-24 22:13:59 +0000957 return options, args
958
959
960def main(argv):
961 dispatcher = subcommand.CommandDispatcher(__name__)
962 return dispatcher.execute(OptionParser(), argv)
963
964
965if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000966 try:
967 sys.exit(main(sys.argv[1:]))
968 except KeyboardInterrupt:
969 sys.stderr.write('interrupted\n')
Edward Lemurdf746d02019-07-27 00:42:46 +0000970 sys.exit(1)