blob: 0ca9c7ddf93e3df1ea12ee2d582a1becba89be0c [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."""
Edward Lemure9024d02019-11-19 18:47:46 +0000270 if os.path.isdir(url):
271 # Ignore the drive letter in Windows
272 url = os.path.splitdrive(url)[1]
273 return url.replace('-', '--').replace(os.sep, '-')
274
szager@chromium.org848fd492014-04-09 19:06:44 +0000275 parsed = urlparse.urlparse(url)
Edward Lemure9024d02019-11-19 18:47:46 +0000276 norm_url = parsed.netloc + parsed.path
szager@chromium.org848fd492014-04-09 19:06:44 +0000277 if norm_url.endswith('.git'):
278 norm_url = norm_url[:-len('.git')]
Dirk Prankedb589542019-04-12 21:07:01 +0000279
280 # Use the same dir for authenticated URLs and unauthenticated URLs.
281 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
282
szager@chromium.org848fd492014-04-09 19:06:44 +0000283 return norm_url.replace('-', '--').replace('/', '-').lower()
284
285 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000286 def CacheDirToUrl(path):
287 """Convert a cache dir path to its corresponding url."""
288 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
289 return 'https://%s' % netpath
290
szager@chromium.org848fd492014-04-09 19:06:44 +0000291 @classmethod
292 def SetCachePath(cls, cachepath):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000293 with cls.cachepath_lock:
294 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000295
296 @classmethod
297 def GetCachePath(cls):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000298 with cls.cachepath_lock:
299 if not hasattr(cls, 'cachepath'):
300 try:
301 cachepath = subprocess.check_output(
Robert Iannuccia19649b2018-06-29 16:31:45 +0000302 [cls.git_exe, 'config'] +
303 cls._GIT_CONFIG_LOCATION +
Edward Lesmes4c3eb702020-03-25 21:09:30 +0000304 ['cache.cachepath']).decode('utf-8', 'ignore').strip()
Vadim Shtayura08049e22017-10-11 00:14:52 +0000305 except subprocess.CalledProcessError:
Robert Iannuccia19649b2018-06-29 16:31:45 +0000306 cachepath = os.environ.get('GIT_CACHE_PATH', cls.UNSET_CACHEPATH)
Vadim Shtayura08049e22017-10-11 00:14:52 +0000307 setattr(cls, 'cachepath', cachepath)
Robert Iannuccia19649b2018-06-29 16:31:45 +0000308
309 ret = getattr(cls, 'cachepath')
310 if ret is cls.UNSET_CACHEPATH:
311 raise RuntimeError('No cache.cachepath git configuration or '
312 '$GIT_CACHE_PATH is set.')
313 return ret
szager@chromium.org848fd492014-04-09 19:06:44 +0000314
Karen Qianccd2b4d2019-05-03 22:25:59 +0000315 @staticmethod
316 def _GetMostRecentCacheDirectory(ls_out_set):
317 ready_file_pattern = re.compile(r'.*/(\d+).ready$')
318 ready_dirs = []
319
320 for name in ls_out_set:
321 m = ready_file_pattern.match(name)
322 # Given <path>/<number>.ready,
323 # we are interested in <path>/<number> directory
324 if m and (name[:-len('.ready')] + '/') in ls_out_set:
325 ready_dirs.append((int(m.group(1)), name[:-len('.ready')]))
326
327 if not ready_dirs:
328 return None
329
330 return max(ready_dirs)[1]
331
dnj4625b5a2016-11-10 18:23:26 -0800332 def Rename(self, src, dst):
333 # This is somehow racy on Windows.
334 # Catching OSError because WindowsError isn't portable and
335 # pylint complains.
336 exponential_backoff_retry(
337 lambda: os.rename(src, dst),
338 excs=(OSError,),
339 name='rename [%s] => [%s]' % (src, dst),
340 printerr=self.print)
341
szager@chromium.org848fd492014-04-09 19:06:44 +0000342 def RunGit(self, cmd, **kwargs):
343 """Run git in a subprocess."""
344 cwd = kwargs.setdefault('cwd', self.mirror_path)
345 kwargs.setdefault('print_stdout', False)
346 kwargs.setdefault('filter_fn', self.print)
347 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
348 env.setdefault('GIT_ASKPASS', 'true')
349 env.setdefault('SSH_ASKPASS', 'true')
350 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
351 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
352
Edward Lemur579c9862018-07-13 23:17:51 +0000353 def config(self, cwd=None, reset_fetch_config=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000354 if cwd is None:
355 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000356
Edward Lemur579c9862018-07-13 23:17:51 +0000357 if reset_fetch_config:
Edward Lemur2f38df62018-07-14 02:13:21 +0000358 try:
359 self.RunGit(['config', '--unset-all', 'remote.origin.fetch'], cwd=cwd)
360 except subprocess.CalledProcessError as e:
361 # If exit code was 5, it means we attempted to unset a config that
362 # didn't exist. Ignore it.
363 if e.returncode != 5:
364 raise
Edward Lemur579c9862018-07-13 23:17:51 +0000365
szager@chromium.org301a7c32014-06-16 17:13:50 +0000366 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinokadcd84042016-06-09 14:26:17 -0700367 try:
368 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
369 except subprocess.CalledProcessError:
370 # Hard error, need to clobber.
371 raise ClobberNeeded()
szager@chromium.org301a7c32014-06-16 17:13:50 +0000372
373 # Don't combine pack files into one big pack file. It's really slow for
374 # repositories, and there's no way to track progress and make sure it's
375 # not stuck.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700376 if self.supported_project():
377 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000378
379 # Allocate more RAM for cache-ing delta chains, for better performance
380 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000381 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000382 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000383
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000384 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000385 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000386 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000387 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000388 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000389 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000390 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000391
392 def bootstrap_repo(self, directory):
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800393 """Bootstrap the repo from Google Storage if possible.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000394
395 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
396 """
Ryan Tseng3beabd02017-03-15 13:57:58 -0700397 if not self.bootstrap_bucket:
398 return False
szager@chromium.org848fd492014-04-09 19:06:44 +0000399
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000400 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
Yuwei Huanga1fbdff2019-02-01 21:51:15 +0000401
Karen Qian0cbd5a52019-04-29 20:14:50 +0000402 # Get the most recent version of the directory.
403 # This is determined from the most recent version of a .ready file.
404 # The .ready file is only uploaded when an entire directory has been
405 # uploaded to GS.
406 _, ls_out, ls_err = gsutil.check_call('ls', self._gs_path)
Karen Qianccd2b4d2019-05-03 22:25:59 +0000407 ls_out_set = set(ls_out.strip().splitlines())
408 latest_dir = self._GetMostRecentCacheDirectory(ls_out_set)
Yuwei Huanga1fbdff2019-02-01 21:51:15 +0000409
Karen Qianccd2b4d2019-05-03 22:25:59 +0000410 if not latest_dir:
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800411 self.print('No bootstrap file for %s found in %s, stderr:\n %s' %
412 (self.mirror_path, self.bootstrap_bucket,
Karen Qian0cbd5a52019-04-29 20:14:50 +0000413 ' '.join((ls_err or '').splitlines(True))))
szager@chromium.org848fd492014-04-09 19:06:44 +0000414 return False
szager@chromium.org848fd492014-04-09 19:06:44 +0000415
szager@chromium.org848fd492014-04-09 19:06:44 +0000416 try:
Karen Qian0cbd5a52019-04-29 20:14:50 +0000417 # create new temporary directory locally
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000418 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
Karen Qian0cbd5a52019-04-29 20:14:50 +0000419 self.RunGit(['init', '--bare'], cwd=tempdir)
420 self.print('Downloading files in %s/* into %s.' %
421 (latest_dir, tempdir))
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800422 with self.print_duration_of('download'):
Karen Qian0cbd5a52019-04-29 20:14:50 +0000423 code = gsutil.call('-m', 'cp', '-r', latest_dir + "/*",
424 tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000425 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000426 return False
Karen Qian0cbd5a52019-04-29 20:14:50 +0000427 except Exception as e:
428 self.print('Encountered error: %s' % str(e), file=sys.stderr)
429 gclient_utils.rmtree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000430 return False
Karen Qian0cbd5a52019-04-29 20:14:50 +0000431 # delete the old directory
432 if os.path.exists(directory):
433 gclient_utils.rmtree(directory)
434 self.Rename(tempdir, directory)
szager@chromium.org848fd492014-04-09 19:06:44 +0000435 return True
436
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800437 def contains_revision(self, revision):
438 if not self.exists():
439 return False
440
441 if sys.platform.startswith('win'):
442 # Windows .bat scripts use ^ as escape sequence, which means we have to
443 # escape it with itself for every .bat invocation.
444 needle = '%s^^^^{commit}' % revision
445 else:
446 needle = '%s^{commit}' % revision
447 try:
448 # cat-file exits with 0 on success, that is git object of given hash was
449 # found.
450 self.RunGit(['cat-file', '-e', needle])
451 return True
452 except subprocess.CalledProcessError:
453 return False
454
szager@chromium.org848fd492014-04-09 19:06:44 +0000455 def exists(self):
456 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
457
Ryan Tseng3beabd02017-03-15 13:57:58 -0700458 def supported_project(self):
459 """Returns true if this repo is known to have a bootstrap zip file."""
460 u = urlparse.urlparse(self.url)
461 return u.netloc in [
462 'chromium.googlesource.com',
463 'chrome-internal.googlesource.com']
464
szager@chromium.org66c8b852015-09-22 23:19:07 +0000465 def _preserve_fetchspec(self):
466 """Read and preserve remote.origin.fetch from an existing mirror.
467
468 This modifies self.fetch_specs.
469 """
470 if not self.exists():
471 return
472 try:
473 config_fetchspecs = subprocess.check_output(
474 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
Edward Lesmes4c3eb702020-03-25 21:09:30 +0000475 cwd=self.mirror_path).decode('utf-8', 'ignore')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000476 for fetchspec in config_fetchspecs.splitlines():
477 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
478 except subprocess.CalledProcessError:
479 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
480 'existing cache directory. You may need to manually edit '
481 '%s and "git cache fetch" again.'
482 % os.path.join(self.mirror_path, 'config'))
483
Edward Lesmes34f71ab2020-03-25 21:24:00 +0000484 def _ensure_bootstrapped(
485 self, depth, bootstrap, reset_fetch_config, force=False):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000486 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
487 pack_files = []
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000488 if os.path.isdir(pack_dir):
489 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
Edward Lesmes34f71ab2020-03-25 21:24:00 +0000490 self.print('%s has %d .pack files, re-bootstrapping if >%d or ==0' %
Karen Qian0cbd5a52019-04-29 20:14:50 +0000491 (self.mirror_path, len(pack_files), GC_AUTOPACKLIMIT))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000492
493 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000494 not self.exists() or
Edward Lesmes34f71ab2020-03-25 21:24:00 +0000495 len(pack_files) > GC_AUTOPACKLIMIT or
496 len(pack_files) == 0)
Karen Qian0cbd5a52019-04-29 20:14:50 +0000497
498 if not should_bootstrap:
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
503
Edward Lesmes34f71ab2020-03-25 21:24:00 +0000504 if not self.exists():
John Budorick47ec0692019-05-01 15:04:28 +0000505 if os.path.exists(self.mirror_path):
506 # If the mirror path exists but self.exists() returns false, we're
507 # in an unexpected state. Nuke the previous mirror directory and
508 # start fresh.
509 gclient_utils.rmtree(self.mirror_path)
Karen Qian0cbd5a52019-04-29 20:14:50 +0000510 os.mkdir(self.mirror_path)
Edward Lesmes34f71ab2020-03-25 21:24:00 +0000511 elif not reset_fetch_config:
512 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
513 self._preserve_fetchspec()
Karen Qian0cbd5a52019-04-29 20:14:50 +0000514
515 bootstrapped = (not depth and bootstrap and
516 self.bootstrap_repo(self.mirror_path))
517
518 if not bootstrapped:
519 if not self.exists() or not self.supported_project():
520 # Bootstrap failed due to:
521 # 1. No previous cache.
522 # 2. Project doesn't have a bootstrap folder.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700523 # Start with a bare git dir.
Karen Qian0cbd5a52019-04-29 20:14:50 +0000524 self.RunGit(['init', '--bare'], cwd=self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000525 else:
526 # Bootstrap failed, previous cache exists; warn and continue.
527 logging.warn(
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800528 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
529 'but failed. Continuing with non-optimized repository.'
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000530 % len(pack_files))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000531
Josip Sokcevic6afaa6c2020-05-08 18:20:17 +0000532 def _fetch(self,
533 rundir,
534 verbose,
535 depth,
536 no_fetch_tags,
537 reset_fetch_config,
538 prune=True):
Edward Lemur579c9862018-07-13 23:17:51 +0000539 self.config(rundir, reset_fetch_config)
Josip Sokcevic6afaa6c2020-05-08 18:20:17 +0000540
541 fetch_cmd = ['fetch']
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000542 if verbose:
Josip Sokcevic6afaa6c2020-05-08 18:20:17 +0000543 fetch_cmd.extend(['-v', '--progress'])
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000544 if depth:
Josip Sokcevic6afaa6c2020-05-08 18:20:17 +0000545 fetch_cmd.extend(['--depth', str(depth)])
danakjc41f72c2019-11-05 17:12:01 +0000546 if no_fetch_tags:
Josip Sokcevic6afaa6c2020-05-08 18:20:17 +0000547 fetch_cmd.append('--no-tags')
548 if prune:
549 fetch_cmd.append('--prune')
550 fetch_cmd.append('origin')
551
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000552 fetch_specs = subprocess.check_output(
553 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
Edward Lesmes4c3eb702020-03-25 21:09:30 +0000554 cwd=rundir).decode('utf-8', 'ignore').strip().splitlines()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000555 for spec in fetch_specs:
556 try:
557 self.print('Fetching %s' % spec)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800558 with self.print_duration_of('fetch %s' % spec):
John Budorick3da78c42019-11-14 20:06:30 +0000559 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000560 except subprocess.CalledProcessError:
561 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700562 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000563 logging.warn('Fetch of %s failed' % spec)
564
danakjc41f72c2019-11-05 17:12:01 +0000565 def populate(self,
566 depth=None,
567 no_fetch_tags=False,
568 shallow=False,
569 bootstrap=False,
570 verbose=False,
571 ignore_lock=False,
572 lock_timeout=0,
Edward Lemur579c9862018-07-13 23:17:51 +0000573 reset_fetch_config=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000574 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000575 if shallow and not depth:
576 depth = 10000
577 gclient_utils.safe_makedirs(self.GetCachePath())
578
Vadim Shtayura08049e22017-10-11 00:14:52 +0000579 lockfile = Lockfile(self.mirror_path, lock_timeout)
580 if not ignore_lock:
581 lockfile.lock()
582
szager@chromium.org108eced2014-06-19 21:22:43 +0000583 try:
Edward Lesmes34f71ab2020-03-25 21:24:00 +0000584 self._ensure_bootstrapped(depth, bootstrap, reset_fetch_config)
Josip Sokcevic6afaa6c2020-05-08 18:20:17 +0000585 self._fetch(self.mirror_path, verbose, depth, no_fetch_tags,
586 reset_fetch_config)
hinokadcd84042016-06-09 14:26:17 -0700587 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000588 # This is a major failure, we need to clean and force a bootstrap.
Karen Qian0cbd5a52019-04-29 20:14:50 +0000589 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000590 self.print(GIT_CACHE_CORRUPT_MESSAGE)
Edward Lesmes34f71ab2020-03-25 21:24:00 +0000591 self._ensure_bootstrapped(
592 depth, bootstrap, reset_fetch_config, force=True)
Josip Sokcevic6afaa6c2020-05-08 18:20:17 +0000593 self._fetch(self.mirror_path, verbose, depth, no_fetch_tags,
594 reset_fetch_config)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000595 finally:
Vadim Shtayura08049e22017-10-11 00:14:52 +0000596 if not ignore_lock:
597 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000598
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000599 def update_bootstrap(self, prune=False, gc_aggressive=False):
Karen Qiandcad7492019-04-26 03:11:16 +0000600 # The folder is <git number>
szager@chromium.org848fd492014-04-09 19:06:44 +0000601 gen_number = subprocess.check_output(
Edward Lesmes4c3eb702020-03-25 21:09:30 +0000602 [self.git_exe, 'number', 'master'],
603 cwd=self.mirror_path).decode('utf-8', 'ignore').strip()
Karen Qiandcad7492019-04-26 03:11:16 +0000604 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
605
606 src_name = self.mirror_path
Karen Qianccd2b4d2019-05-03 22:25:59 +0000607 dest_prefix = '%s/%s' % (self._gs_path, gen_number)
Karen Qiandcad7492019-04-26 03:11:16 +0000608
Karen Qianccd2b4d2019-05-03 22:25:59 +0000609 # ls_out lists contents in the format: gs://blah/blah/123...
610 _, ls_out, _ = gsutil.check_call('ls', self._gs_path)
Karen Qiandcad7492019-04-26 03:11:16 +0000611
Karen Qianccd2b4d2019-05-03 22:25:59 +0000612 # Check to see if folder already exists in gs
613 ls_out_set = set(ls_out.strip().splitlines())
614 if (dest_prefix + '/' in ls_out_set and
615 dest_prefix + '.ready' in ls_out_set):
616 print('Cache %s already exists.' % dest_prefix)
Karen Qiandcad7492019-04-26 03:11:16 +0000617 return
618
Andrii Shyshkalov199182f2019-04-26 16:01:20 +0000619 # Run Garbage Collect to compress packfile.
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000620 gc_args = ['gc', '--prune=all']
621 if gc_aggressive:
622 gc_args.append('--aggressive')
623 self.RunGit(gc_args)
Andrii Shyshkalov199182f2019-04-26 16:01:20 +0000624
Karen Qianccd2b4d2019-05-03 22:25:59 +0000625 gsutil.call('-m', 'cp', '-r', src_name, dest_prefix)
Karen Qiandcad7492019-04-26 03:11:16 +0000626
Karen Qianccd2b4d2019-05-03 22:25:59 +0000627 # Create .ready file and upload
Karen Qiandcad7492019-04-26 03:11:16 +0000628 _, ready_file_name = tempfile.mkstemp(suffix='.ready')
629 try:
Karen Qianccd2b4d2019-05-03 22:25:59 +0000630 gsutil.call('cp', ready_file_name, '%s.ready' % (dest_prefix))
Karen Qiandcad7492019-04-26 03:11:16 +0000631 finally:
632 os.remove(ready_file_name)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000633
Karen Qianccd2b4d2019-05-03 22:25:59 +0000634 # remove all other directory/.ready files in the same gs_path
635 # except for the directory/.ready file previously created
636 # which can be used for bootstrapping while the current one is
637 # being uploaded
638 if not prune:
639 return
640 prev_dest_prefix = self._GetMostRecentCacheDirectory(ls_out_set)
641 if not prev_dest_prefix:
642 return
643 for path in ls_out_set:
644 if (path == prev_dest_prefix + '/' or
645 path == prev_dest_prefix + '.ready'):
646 continue
647 if path.endswith('.ready'):
648 gsutil.call('rm', path)
649 continue
650 gsutil.call('-m', 'rm', '-r', path)
651
652
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000653 @staticmethod
654 def DeleteTmpPackFiles(path):
655 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000656 if not os.path.isdir(pack_dir):
657 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000658 pack_files = [f for f in os.listdir(pack_dir) if
659 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
660 for f in pack_files:
661 f = os.path.join(pack_dir, f)
662 try:
663 os.remove(f)
664 logging.warn('Deleted stale temporary pack file %s' % f)
665 except OSError:
666 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000667
Vadim Shtayura08049e22017-10-11 00:14:52 +0000668 @classmethod
669 def BreakLocks(cls, path):
670 did_unlock = False
671 lf = Lockfile(path)
672 if lf.break_lock():
673 did_unlock = True
674 # Look for lock files that might have been left behind by an interrupted
675 # git process.
676 lf = os.path.join(path, 'config.lock')
677 if os.path.exists(lf):
678 os.remove(lf)
679 did_unlock = True
680 cls.DeleteTmpPackFiles(path)
681 return did_unlock
682
683 def unlock(self):
684 return self.BreakLocks(self.mirror_path)
685
686 @classmethod
687 def UnlockAll(cls):
688 cachepath = cls.GetCachePath()
689 if not cachepath:
690 return
691 dirlist = os.listdir(cachepath)
692 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
693 if os.path.isdir(os.path.join(cachepath, path))])
694 for dirent in dirlist:
695 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
696 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
697 elif (dirent.endswith('.lock') and
698 os.path.isfile(os.path.join(cachepath, dirent))):
699 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
700
701 unlocked_repos = []
702 for repo_dir in repo_dirs:
703 if cls.BreakLocks(repo_dir):
704 unlocked_repos.append(repo_dir)
705
706 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000707
agable@chromium.org5a306a22014-02-24 22:13:59 +0000708@subcommand.usage('[url of repo to check for caching]')
709def CMDexists(parser, args):
710 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000711 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000712 if not len(args) == 1:
713 parser.error('git cache exists only takes exactly one repo url.')
714 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000715 mirror = Mirror(url)
716 if mirror.exists():
717 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000718 return 0
719 return 1
720
721
hinoka@google.com563559c2014-04-02 00:36:24 +0000722@subcommand.usage('[url of repo to create a bootstrap zip file]')
723def CMDupdate_bootstrap(parser, args):
724 """Create and uploads a bootstrap tarball."""
725 # Lets just assert we can't do this on Windows.
726 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000727 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000728 return 1
729
Robert Iannucci0081c0f2019-09-29 08:30:54 +0000730 parser.add_option('--skip-populate', action='store_true',
731 help='Skips "populate" step if mirror already exists.')
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000732 parser.add_option('--gc-aggressive', action='store_true',
733 help='Run aggressive repacking of the repo.')
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000734 parser.add_option('--prune', action='store_true',
Andrii Shyshkalov7a2205c2019-04-26 05:14:36 +0000735 help='Prune all other cached bundles of the same repo.')
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000736
hinoka@google.com563559c2014-04-02 00:36:24 +0000737 populate_args = args[:]
Robert Iannucci0081c0f2019-09-29 08:30:54 +0000738 options, args = parser.parse_args(args)
739 url = args[0]
740 mirror = Mirror(url)
741 if not options.skip_populate or not mirror.exists():
742 CMDpopulate(parser, populate_args)
743 else:
744 print('Skipped populate step.')
hinoka@google.com563559c2014-04-02 00:36:24 +0000745
746 # Get the repo directory.
Andrii Shyshkalovc50b0962019-11-21 23:03:18 +0000747 _, args2 = parser.parse_args(args)
748 url = args2[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000749 mirror = Mirror(url)
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000750 mirror.update_bootstrap(options.prune, options.gc_aggressive)
szager@chromium.org848fd492014-04-09 19:06:44 +0000751 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000752
753
agable@chromium.org5a306a22014-02-24 22:13:59 +0000754@subcommand.usage('[url of repo to add to or update in cache]')
755def CMDpopulate(parser, args):
756 """Ensure that the cache has all up-to-date objects for the given repo."""
757 parser.add_option('--depth', type='int',
758 help='Only cache DEPTH commits of history')
danakjc41f72c2019-11-05 17:12:01 +0000759 parser.add_option(
760 '--no-fetch-tags',
761 action='store_true',
762 help=('Don\'t fetch tags from the server. This can speed up '
763 'fetch considerably when there are many tags.'))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000764 parser.add_option('--shallow', '-s', action='store_true',
765 help='Only cache 10000 commits of history')
766 parser.add_option('--ref', action='append',
767 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000768 parser.add_option('--no_bootstrap', '--no-bootstrap',
769 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000770 help='Don\'t bootstrap from Google Storage')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000771 parser.add_option('--ignore_locks', '--ignore-locks',
772 action='store_true',
773 help='Don\'t try to lock repository')
Robert Iannucci09315982019-10-05 08:12:03 +0000774 parser.add_option('--break-locks',
775 action='store_true',
776 help='Break any existing lock instead of just ignoring it')
Edward Lemur579c9862018-07-13 23:17:51 +0000777 parser.add_option('--reset-fetch-config', action='store_true', default=False,
778 help='Reset the fetch config before populating the cache.')
hinoka@google.com563559c2014-04-02 00:36:24 +0000779
agable@chromium.org5a306a22014-02-24 22:13:59 +0000780 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000781 if not len(args) == 1:
782 parser.error('git cache populate only takes exactly one repo url.')
783 url = args[0]
784
szager@chromium.org848fd492014-04-09 19:06:44 +0000785 mirror = Mirror(url, refs=options.ref)
Robert Iannucci09315982019-10-05 08:12:03 +0000786 if options.break_locks:
787 mirror.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000788 kwargs = {
danakjc41f72c2019-11-05 17:12:01 +0000789 'no_fetch_tags': options.no_fetch_tags,
szager@chromium.org848fd492014-04-09 19:06:44 +0000790 'verbose': options.verbose,
791 'shallow': options.shallow,
792 'bootstrap': not options.no_bootstrap,
Vadim Shtayura08049e22017-10-11 00:14:52 +0000793 'ignore_lock': options.ignore_locks,
794 'lock_timeout': options.timeout,
Edward Lemur579c9862018-07-13 23:17:51 +0000795 'reset_fetch_config': options.reset_fetch_config,
szager@chromium.org848fd492014-04-09 19:06:44 +0000796 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000797 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000798 kwargs['depth'] = options.depth
799 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000800
801
szager@chromium.orgf3145112014-08-07 21:02:36 +0000802@subcommand.usage('Fetch new commits into cache and current checkout')
803def CMDfetch(parser, args):
804 """Update mirror, and fetch in cwd."""
805 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000806 parser.add_option('--no_bootstrap', '--no-bootstrap',
807 action='store_true',
808 help='Don\'t (re)bootstrap from Google Storage')
danakjc41f72c2019-11-05 17:12:01 +0000809 parser.add_option(
810 '--no-fetch-tags',
811 action='store_true',
812 help=('Don\'t fetch tags from the server. This can speed up '
813 'fetch considerably when there are many tags.'))
szager@chromium.orgf3145112014-08-07 21:02:36 +0000814 options, args = parser.parse_args(args)
815
816 # Figure out which remotes to fetch. This mimics the behavior of regular
817 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
818 # this will NOT try to traverse up the branching structure to find the
819 # ultimate remote to update.
820 remotes = []
821 if options.all:
822 assert not args, 'fatal: fetch --all does not take a repository argument'
Edward Lesmes4c3eb702020-03-25 21:09:30 +0000823 remotes = subprocess.check_output([Mirror.git_exe, 'remote'])
824 remotes = remotes.decode('utf-8', 'ignore').splitlines()
szager@chromium.orgf3145112014-08-07 21:02:36 +0000825 elif args:
826 remotes = args
827 else:
828 current_branch = subprocess.check_output(
Edward Lesmes4c3eb702020-03-25 21:09:30 +0000829 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD'])
830 current_branch = current_branch.decode('utf-8', 'ignore').strip()
szager@chromium.orgf3145112014-08-07 21:02:36 +0000831 if current_branch != 'HEAD':
832 upstream = subprocess.check_output(
Edward Lesmes4c3eb702020-03-25 21:09:30 +0000833 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch])
834 upstream = upstream.decode('utf-8', 'ignore').strip()
szager@chromium.orgf3145112014-08-07 21:02:36 +0000835 if upstream and upstream != '.':
836 remotes = [upstream]
837 if not remotes:
838 remotes = ['origin']
839
840 cachepath = Mirror.GetCachePath()
841 git_dir = os.path.abspath(subprocess.check_output(
Edward Lesmes4c3eb702020-03-25 21:09:30 +0000842 [Mirror.git_exe, 'rev-parse', '--git-dir']).decode('utf-8', 'ignore'))
szager@chromium.orgf3145112014-08-07 21:02:36 +0000843 git_dir = os.path.abspath(git_dir)
844 if git_dir.startswith(cachepath):
845 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000846 mirror.populate(
danakjc41f72c2019-11-05 17:12:01 +0000847 bootstrap=not options.no_bootstrap,
848 no_fetch_tags=options.no_fetch_tags,
849 lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000850 return 0
851 for remote in remotes:
852 remote_url = subprocess.check_output(
Edward Lesmes4c3eb702020-03-25 21:09:30 +0000853 [Mirror.git_exe, 'config', 'remote.%s.url' % remote])
854 remote_url = remote_url.decode('utf-8', 'ignore').strip()
szager@chromium.orgf3145112014-08-07 21:02:36 +0000855 if remote_url.startswith(cachepath):
856 mirror = Mirror.FromPath(remote_url)
857 mirror.print = lambda *args: None
858 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000859 mirror.populate(
danakjc41f72c2019-11-05 17:12:01 +0000860 bootstrap=not options.no_bootstrap,
861 no_fetch_tags=options.no_fetch_tags,
862 lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000863 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
864 return 0
865
866
Vadim Shtayura08049e22017-10-11 00:14:52 +0000867@subcommand.usage('[url of repo to unlock, or -a|--all]')
868def CMDunlock(parser, args):
869 """Unlock one or all repos if their lock files are still around."""
870 parser.add_option('--force', '-f', action='store_true',
871 help='Actually perform the action')
872 parser.add_option('--all', '-a', action='store_true',
873 help='Unlock all repository caches')
874 options, args = parser.parse_args(args)
875 if len(args) > 1 or (len(args) == 0 and not options.all):
876 parser.error('git cache unlock takes exactly one repo url, or --all')
877
878 if not options.force:
879 cachepath = Mirror.GetCachePath()
880 lockfiles = [os.path.join(cachepath, path)
881 for path in os.listdir(cachepath)
882 if path.endswith('.lock') and os.path.isfile(path)]
883 parser.error('git cache unlock requires -f|--force to do anything. '
884 'Refusing to unlock the following repo caches: '
885 ', '.join(lockfiles))
886
887 unlocked_repos = []
888 if options.all:
889 unlocked_repos.extend(Mirror.UnlockAll())
890 else:
891 m = Mirror(args[0])
892 if m.unlock():
893 unlocked_repos.append(m.mirror_path)
894
895 if unlocked_repos:
896 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
897 unlocked_repos))
898
899
agable@chromium.org5a306a22014-02-24 22:13:59 +0000900class OptionParser(optparse.OptionParser):
901 """Wrapper class for OptionParser to handle global options."""
902
903 def __init__(self, *args, **kwargs):
904 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
905 self.add_option('-c', '--cache-dir',
Robert Iannuccia19649b2018-06-29 16:31:45 +0000906 help=(
907 'Path to the directory containing the caches. Normally '
908 'deduced from git config cache.cachepath or '
909 '$GIT_CACHE_PATH.'))
szager@chromium.org2c391af2014-05-23 09:07:15 +0000910 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000911 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000912 self.add_option('-q', '--quiet', action='store_true',
913 help='Suppress all extraneous output')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000914 self.add_option('--timeout', type='int', default=0,
915 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000916
917 def parse_args(self, args=None, values=None):
918 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000919 if options.quiet:
920 options.verbose = 0
921
922 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
923 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000924
925 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000926 global_cache_dir = Mirror.GetCachePath()
927 except RuntimeError:
928 global_cache_dir = None
929 if options.cache_dir:
930 if global_cache_dir and (
931 os.path.abspath(options.cache_dir) !=
932 os.path.abspath(global_cache_dir)):
933 logging.warn('Overriding globally-configured cache directory.')
934 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000935
agable@chromium.org5a306a22014-02-24 22:13:59 +0000936 return options, args
937
938
939def main(argv):
940 dispatcher = subcommand.CommandDispatcher(__name__)
941 return dispatcher.execute(OptionParser(), argv)
942
943
944if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000945 try:
946 sys.exit(main(sys.argv[1:]))
947 except KeyboardInterrupt:
948 sys.stderr.write('interrupted\n')
Edward Lemurdf746d02019-07-27 00:42:46 +0000949 sys.exit(1)