blob: 8d050c36c15c5f8941940c8c0b5ff9cfbee1700c [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
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
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)
271 norm_url = parsed.netloc + parsed.path
272 if norm_url.endswith('.git'):
273 norm_url = norm_url[:-len('.git')]
Dirk Prankedb589542019-04-12 21:07:01 +0000274
275 # Use the same dir for authenticated URLs and unauthenticated URLs.
276 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
277
szager@chromium.org848fd492014-04-09 19:06:44 +0000278 return norm_url.replace('-', '--').replace('/', '-').lower()
279
280 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000281 def CacheDirToUrl(path):
282 """Convert a cache dir path to its corresponding url."""
283 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
284 return 'https://%s' % netpath
285
szager@chromium.org848fd492014-04-09 19:06:44 +0000286 @classmethod
287 def SetCachePath(cls, cachepath):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000288 with cls.cachepath_lock:
289 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000290
291 @classmethod
292 def GetCachePath(cls):
Vadim Shtayura08049e22017-10-11 00:14:52 +0000293 with cls.cachepath_lock:
294 if not hasattr(cls, 'cachepath'):
295 try:
296 cachepath = subprocess.check_output(
Robert Iannuccia19649b2018-06-29 16:31:45 +0000297 [cls.git_exe, 'config'] +
298 cls._GIT_CONFIG_LOCATION +
299 ['cache.cachepath']).strip()
Vadim Shtayura08049e22017-10-11 00:14:52 +0000300 except subprocess.CalledProcessError:
Robert Iannuccia19649b2018-06-29 16:31:45 +0000301 cachepath = os.environ.get('GIT_CACHE_PATH', cls.UNSET_CACHEPATH)
Vadim Shtayura08049e22017-10-11 00:14:52 +0000302 setattr(cls, 'cachepath', cachepath)
Robert Iannuccia19649b2018-06-29 16:31:45 +0000303
304 ret = getattr(cls, 'cachepath')
305 if ret is cls.UNSET_CACHEPATH:
306 raise RuntimeError('No cache.cachepath git configuration or '
307 '$GIT_CACHE_PATH is set.')
308 return ret
szager@chromium.org848fd492014-04-09 19:06:44 +0000309
Karen Qianccd2b4d2019-05-03 22:25:59 +0000310 @staticmethod
311 def _GetMostRecentCacheDirectory(ls_out_set):
312 ready_file_pattern = re.compile(r'.*/(\d+).ready$')
313 ready_dirs = []
314
315 for name in ls_out_set:
316 m = ready_file_pattern.match(name)
317 # Given <path>/<number>.ready,
318 # we are interested in <path>/<number> directory
319 if m and (name[:-len('.ready')] + '/') in ls_out_set:
320 ready_dirs.append((int(m.group(1)), name[:-len('.ready')]))
321
322 if not ready_dirs:
323 return None
324
325 return max(ready_dirs)[1]
326
dnj4625b5a2016-11-10 18:23:26 -0800327 def Rename(self, src, dst):
328 # This is somehow racy on Windows.
329 # Catching OSError because WindowsError isn't portable and
330 # pylint complains.
331 exponential_backoff_retry(
332 lambda: os.rename(src, dst),
333 excs=(OSError,),
334 name='rename [%s] => [%s]' % (src, dst),
335 printerr=self.print)
336
szager@chromium.org848fd492014-04-09 19:06:44 +0000337 def RunGit(self, cmd, **kwargs):
338 """Run git in a subprocess."""
339 cwd = kwargs.setdefault('cwd', self.mirror_path)
340 kwargs.setdefault('print_stdout', False)
341 kwargs.setdefault('filter_fn', self.print)
342 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
343 env.setdefault('GIT_ASKPASS', 'true')
344 env.setdefault('SSH_ASKPASS', 'true')
345 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
346 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
347
Edward Lemur579c9862018-07-13 23:17:51 +0000348 def config(self, cwd=None, reset_fetch_config=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000349 if cwd is None:
350 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000351
Edward Lemur579c9862018-07-13 23:17:51 +0000352 if reset_fetch_config:
Edward Lemur2f38df62018-07-14 02:13:21 +0000353 try:
354 self.RunGit(['config', '--unset-all', 'remote.origin.fetch'], cwd=cwd)
355 except subprocess.CalledProcessError as e:
356 # If exit code was 5, it means we attempted to unset a config that
357 # didn't exist. Ignore it.
358 if e.returncode != 5:
359 raise
Edward Lemur579c9862018-07-13 23:17:51 +0000360
szager@chromium.org301a7c32014-06-16 17:13:50 +0000361 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinokadcd84042016-06-09 14:26:17 -0700362 try:
363 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
364 except subprocess.CalledProcessError:
365 # Hard error, need to clobber.
366 raise ClobberNeeded()
szager@chromium.org301a7c32014-06-16 17:13:50 +0000367
368 # Don't combine pack files into one big pack file. It's really slow for
369 # repositories, and there's no way to track progress and make sure it's
370 # not stuck.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700371 if self.supported_project():
372 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000373
374 # Allocate more RAM for cache-ing delta chains, for better performance
375 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000376 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000377 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000378
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000379 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000380 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000381 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000382 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000383 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000384 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000385 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000386
387 def bootstrap_repo(self, directory):
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800388 """Bootstrap the repo from Google Storage if possible.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000389
390 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
391 """
Ryan Tseng3beabd02017-03-15 13:57:58 -0700392 if not self.bootstrap_bucket:
393 return False
szager@chromium.org848fd492014-04-09 19:06:44 +0000394
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000395 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
Yuwei Huanga1fbdff2019-02-01 21:51:15 +0000396
Karen Qian0cbd5a52019-04-29 20:14:50 +0000397 # Get the most recent version of the directory.
398 # This is determined from the most recent version of a .ready file.
399 # The .ready file is only uploaded when an entire directory has been
400 # uploaded to GS.
401 _, ls_out, ls_err = gsutil.check_call('ls', self._gs_path)
Karen Qianccd2b4d2019-05-03 22:25:59 +0000402 ls_out_set = set(ls_out.strip().splitlines())
403 latest_dir = self._GetMostRecentCacheDirectory(ls_out_set)
Yuwei Huanga1fbdff2019-02-01 21:51:15 +0000404
Karen Qianccd2b4d2019-05-03 22:25:59 +0000405 if not latest_dir:
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800406 self.print('No bootstrap file for %s found in %s, stderr:\n %s' %
407 (self.mirror_path, self.bootstrap_bucket,
Karen Qian0cbd5a52019-04-29 20:14:50 +0000408 ' '.join((ls_err or '').splitlines(True))))
szager@chromium.org848fd492014-04-09 19:06:44 +0000409 return False
szager@chromium.org848fd492014-04-09 19:06:44 +0000410
szager@chromium.org848fd492014-04-09 19:06:44 +0000411 try:
Karen Qian0cbd5a52019-04-29 20:14:50 +0000412 # create new temporary directory locally
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000413 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
Karen Qian0cbd5a52019-04-29 20:14:50 +0000414 self.RunGit(['init', '--bare'], cwd=tempdir)
415 self.print('Downloading files in %s/* into %s.' %
416 (latest_dir, tempdir))
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800417 with self.print_duration_of('download'):
Karen Qian0cbd5a52019-04-29 20:14:50 +0000418 code = gsutil.call('-m', 'cp', '-r', latest_dir + "/*",
419 tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000420 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000421 return False
Karen Qian0cbd5a52019-04-29 20:14:50 +0000422 except Exception as e:
423 self.print('Encountered error: %s' % str(e), file=sys.stderr)
424 gclient_utils.rmtree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000425 return False
Karen Qian0cbd5a52019-04-29 20:14:50 +0000426 # delete the old directory
427 if os.path.exists(directory):
428 gclient_utils.rmtree(directory)
429 self.Rename(tempdir, directory)
szager@chromium.org848fd492014-04-09 19:06:44 +0000430 return True
431
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800432 def contains_revision(self, revision):
433 if not self.exists():
434 return False
435
436 if sys.platform.startswith('win'):
437 # Windows .bat scripts use ^ as escape sequence, which means we have to
438 # escape it with itself for every .bat invocation.
439 needle = '%s^^^^{commit}' % revision
440 else:
441 needle = '%s^{commit}' % revision
442 try:
443 # cat-file exits with 0 on success, that is git object of given hash was
444 # found.
445 self.RunGit(['cat-file', '-e', needle])
446 return True
447 except subprocess.CalledProcessError:
448 return False
449
szager@chromium.org848fd492014-04-09 19:06:44 +0000450 def exists(self):
451 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
452
Ryan Tseng3beabd02017-03-15 13:57:58 -0700453 def supported_project(self):
454 """Returns true if this repo is known to have a bootstrap zip file."""
455 u = urlparse.urlparse(self.url)
456 return u.netloc in [
457 'chromium.googlesource.com',
458 'chrome-internal.googlesource.com']
459
szager@chromium.org66c8b852015-09-22 23:19:07 +0000460 def _preserve_fetchspec(self):
461 """Read and preserve remote.origin.fetch from an existing mirror.
462
463 This modifies self.fetch_specs.
464 """
465 if not self.exists():
466 return
467 try:
468 config_fetchspecs = subprocess.check_output(
469 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
470 cwd=self.mirror_path)
471 for fetchspec in config_fetchspecs.splitlines():
472 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
473 except subprocess.CalledProcessError:
474 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
475 'existing cache directory. You may need to manually edit '
476 '%s and "git cache fetch" again.'
477 % os.path.join(self.mirror_path, 'config'))
478
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000479 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000480 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
481 pack_files = []
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000482 if os.path.isdir(pack_dir):
483 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800484 self.print('%s has %d .pack files, re-bootstrapping if >%d' %
Karen Qian0cbd5a52019-04-29 20:14:50 +0000485 (self.mirror_path, len(pack_files), GC_AUTOPACKLIMIT))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000486
487 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000488 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000489 len(pack_files) > GC_AUTOPACKLIMIT)
Karen Qian0cbd5a52019-04-29 20:14:50 +0000490
491 if not should_bootstrap:
492 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
493 logging.warn(
494 'Shallow fetch requested, but repo cache already exists.')
495 return
496
497 if self.exists():
498 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
499 self._preserve_fetchspec()
500 else:
John Budorick47ec0692019-05-01 15:04:28 +0000501 if os.path.exists(self.mirror_path):
502 # If the mirror path exists but self.exists() returns false, we're
503 # in an unexpected state. Nuke the previous mirror directory and
504 # start fresh.
505 gclient_utils.rmtree(self.mirror_path)
Karen Qian0cbd5a52019-04-29 20:14:50 +0000506 os.mkdir(self.mirror_path)
507
508 bootstrapped = (not depth and bootstrap and
509 self.bootstrap_repo(self.mirror_path))
510
511 if not bootstrapped:
512 if not self.exists() or not self.supported_project():
513 # Bootstrap failed due to:
514 # 1. No previous cache.
515 # 2. Project doesn't have a bootstrap folder.
Ryan Tseng3beabd02017-03-15 13:57:58 -0700516 # Start with a bare git dir.
Karen Qian0cbd5a52019-04-29 20:14:50 +0000517 self.RunGit(['init', '--bare'], cwd=self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000518 else:
519 # Bootstrap failed, previous cache exists; warn and continue.
520 logging.warn(
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800521 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
522 'but failed. Continuing with non-optimized repository.'
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000523 % len(pack_files))
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000524
Edward Lemur579c9862018-07-13 23:17:51 +0000525 def _fetch(self, rundir, verbose, depth, reset_fetch_config):
526 self.config(rundir, reset_fetch_config)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000527 v = []
528 d = []
529 if verbose:
530 v = ['-v', '--progress']
531 if depth:
532 d = ['--depth', str(depth)]
533 fetch_cmd = ['fetch'] + v + d + ['origin']
534 fetch_specs = subprocess.check_output(
535 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
536 cwd=rundir).strip().splitlines()
537 for spec in fetch_specs:
Edward Lemurdf746d02019-07-27 00:42:46 +0000538 spec = spec.decode()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000539 try:
540 self.print('Fetching %s' % spec)
Andrii Shyshkalov4f56f232017-11-23 02:19:25 -0800541 with self.print_duration_of('fetch %s' % spec):
542 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000543 except subprocess.CalledProcessError:
544 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700545 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000546 logging.warn('Fetch of %s failed' % spec)
547
Vadim Shtayura08049e22017-10-11 00:14:52 +0000548 def populate(self, depth=None, shallow=False, bootstrap=False,
Edward Lemur579c9862018-07-13 23:17:51 +0000549 verbose=False, ignore_lock=False, lock_timeout=0,
550 reset_fetch_config=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000551 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000552 if shallow and not depth:
553 depth = 10000
554 gclient_utils.safe_makedirs(self.GetCachePath())
555
Vadim Shtayura08049e22017-10-11 00:14:52 +0000556 lockfile = Lockfile(self.mirror_path, lock_timeout)
557 if not ignore_lock:
558 lockfile.lock()
559
szager@chromium.org108eced2014-06-19 21:22:43 +0000560 try:
Karen Qian0cbd5a52019-04-29 20:14:50 +0000561 self._ensure_bootstrapped(depth, bootstrap)
562 self._fetch(self.mirror_path, verbose, depth, reset_fetch_config)
hinokadcd84042016-06-09 14:26:17 -0700563 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000564 # This is a major failure, we need to clean and force a bootstrap.
Karen Qian0cbd5a52019-04-29 20:14:50 +0000565 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000566 self.print(GIT_CACHE_CORRUPT_MESSAGE)
Karen Qian0cbd5a52019-04-29 20:14:50 +0000567 self._ensure_bootstrapped(depth, bootstrap, force=True)
568 self._fetch(self.mirror_path, verbose, depth, reset_fetch_config)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000569 finally:
Vadim Shtayura08049e22017-10-11 00:14:52 +0000570 if not ignore_lock:
571 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000572
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000573 def update_bootstrap(self, prune=False, gc_aggressive=False):
Karen Qiandcad7492019-04-26 03:11:16 +0000574 # The folder is <git number>
szager@chromium.org848fd492014-04-09 19:06:44 +0000575 gen_number = subprocess.check_output(
576 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
Karen Qiandcad7492019-04-26 03:11:16 +0000577 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
578
579 src_name = self.mirror_path
Karen Qianccd2b4d2019-05-03 22:25:59 +0000580 dest_prefix = '%s/%s' % (self._gs_path, gen_number)
Karen Qiandcad7492019-04-26 03:11:16 +0000581
Karen Qianccd2b4d2019-05-03 22:25:59 +0000582 # ls_out lists contents in the format: gs://blah/blah/123...
583 _, ls_out, _ = gsutil.check_call('ls', self._gs_path)
Karen Qiandcad7492019-04-26 03:11:16 +0000584
Karen Qianccd2b4d2019-05-03 22:25:59 +0000585 # Check to see if folder already exists in gs
586 ls_out_set = set(ls_out.strip().splitlines())
587 if (dest_prefix + '/' in ls_out_set and
588 dest_prefix + '.ready' in ls_out_set):
589 print('Cache %s already exists.' % dest_prefix)
Karen Qiandcad7492019-04-26 03:11:16 +0000590 return
591
Andrii Shyshkalov199182f2019-04-26 16:01:20 +0000592 # Run Garbage Collect to compress packfile.
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000593 gc_args = ['gc', '--prune=all']
594 if gc_aggressive:
595 gc_args.append('--aggressive')
596 self.RunGit(gc_args)
Andrii Shyshkalov199182f2019-04-26 16:01:20 +0000597
Karen Qianccd2b4d2019-05-03 22:25:59 +0000598 gsutil.call('-m', 'cp', '-r', src_name, dest_prefix)
Karen Qiandcad7492019-04-26 03:11:16 +0000599
Karen Qianccd2b4d2019-05-03 22:25:59 +0000600 # Create .ready file and upload
Karen Qiandcad7492019-04-26 03:11:16 +0000601 _, ready_file_name = tempfile.mkstemp(suffix='.ready')
602 try:
Karen Qianccd2b4d2019-05-03 22:25:59 +0000603 gsutil.call('cp', ready_file_name, '%s.ready' % (dest_prefix))
Karen Qiandcad7492019-04-26 03:11:16 +0000604 finally:
605 os.remove(ready_file_name)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000606
Karen Qianccd2b4d2019-05-03 22:25:59 +0000607 # remove all other directory/.ready files in the same gs_path
608 # except for the directory/.ready file previously created
609 # which can be used for bootstrapping while the current one is
610 # being uploaded
611 if not prune:
612 return
613 prev_dest_prefix = self._GetMostRecentCacheDirectory(ls_out_set)
614 if not prev_dest_prefix:
615 return
616 for path in ls_out_set:
617 if (path == prev_dest_prefix + '/' or
618 path == prev_dest_prefix + '.ready'):
619 continue
620 if path.endswith('.ready'):
621 gsutil.call('rm', path)
622 continue
623 gsutil.call('-m', 'rm', '-r', path)
624
625
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000626 @staticmethod
627 def DeleteTmpPackFiles(path):
628 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000629 if not os.path.isdir(pack_dir):
630 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000631 pack_files = [f for f in os.listdir(pack_dir) if
632 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
633 for f in pack_files:
634 f = os.path.join(pack_dir, f)
635 try:
636 os.remove(f)
637 logging.warn('Deleted stale temporary pack file %s' % f)
638 except OSError:
639 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000640
Vadim Shtayura08049e22017-10-11 00:14:52 +0000641 @classmethod
642 def BreakLocks(cls, path):
643 did_unlock = False
644 lf = Lockfile(path)
645 if lf.break_lock():
646 did_unlock = True
647 # Look for lock files that might have been left behind by an interrupted
648 # git process.
649 lf = os.path.join(path, 'config.lock')
650 if os.path.exists(lf):
651 os.remove(lf)
652 did_unlock = True
653 cls.DeleteTmpPackFiles(path)
654 return did_unlock
655
656 def unlock(self):
657 return self.BreakLocks(self.mirror_path)
658
659 @classmethod
660 def UnlockAll(cls):
661 cachepath = cls.GetCachePath()
662 if not cachepath:
663 return
664 dirlist = os.listdir(cachepath)
665 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
666 if os.path.isdir(os.path.join(cachepath, path))])
667 for dirent in dirlist:
668 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
669 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
670 elif (dirent.endswith('.lock') and
671 os.path.isfile(os.path.join(cachepath, dirent))):
672 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
673
674 unlocked_repos = []
675 for repo_dir in repo_dirs:
676 if cls.BreakLocks(repo_dir):
677 unlocked_repos.append(repo_dir)
678
679 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000680
agable@chromium.org5a306a22014-02-24 22:13:59 +0000681@subcommand.usage('[url of repo to check for caching]')
682def CMDexists(parser, args):
683 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000684 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000685 if not len(args) == 1:
686 parser.error('git cache exists only takes exactly one repo url.')
687 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000688 mirror = Mirror(url)
689 if mirror.exists():
690 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000691 return 0
692 return 1
693
694
hinoka@google.com563559c2014-04-02 00:36:24 +0000695@subcommand.usage('[url of repo to create a bootstrap zip file]')
696def CMDupdate_bootstrap(parser, args):
697 """Create and uploads a bootstrap tarball."""
698 # Lets just assert we can't do this on Windows.
699 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000700 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000701 return 1
702
Robert Iannucci0081c0f2019-09-29 08:30:54 +0000703 parser.add_option('--skip-populate', action='store_true',
704 help='Skips "populate" step if mirror already exists.')
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000705 parser.add_option('--gc-aggressive', action='store_true',
706 help='Run aggressive repacking of the repo.')
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000707 parser.add_option('--prune', action='store_true',
Andrii Shyshkalov7a2205c2019-04-26 05:14:36 +0000708 help='Prune all other cached bundles of the same repo.')
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000709
hinoka@google.com563559c2014-04-02 00:36:24 +0000710 populate_args = args[:]
Robert Iannucci0081c0f2019-09-29 08:30:54 +0000711 options, args = parser.parse_args(args)
712 url = args[0]
713 mirror = Mirror(url)
714 if not options.skip_populate or not mirror.exists():
715 CMDpopulate(parser, populate_args)
716 else:
717 print('Skipped populate step.')
hinoka@google.com563559c2014-04-02 00:36:24 +0000718
719 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000720 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000721 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000722 mirror = Mirror(url)
Andrii Shyshkalovdcfe55f2019-09-21 03:35:39 +0000723 mirror.update_bootstrap(options.prune, options.gc_aggressive)
szager@chromium.org848fd492014-04-09 19:06:44 +0000724 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000725
726
agable@chromium.org5a306a22014-02-24 22:13:59 +0000727@subcommand.usage('[url of repo to add to or update in cache]')
728def CMDpopulate(parser, args):
729 """Ensure that the cache has all up-to-date objects for the given repo."""
730 parser.add_option('--depth', type='int',
731 help='Only cache DEPTH commits of history')
732 parser.add_option('--shallow', '-s', action='store_true',
733 help='Only cache 10000 commits of history')
734 parser.add_option('--ref', action='append',
735 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000736 parser.add_option('--no_bootstrap', '--no-bootstrap',
737 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000738 help='Don\'t bootstrap from Google Storage')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000739 parser.add_option('--ignore_locks', '--ignore-locks',
740 action='store_true',
741 help='Don\'t try to lock repository')
Edward Lemur579c9862018-07-13 23:17:51 +0000742 parser.add_option('--reset-fetch-config', action='store_true', default=False,
743 help='Reset the fetch config before populating the cache.')
hinoka@google.com563559c2014-04-02 00:36:24 +0000744
agable@chromium.org5a306a22014-02-24 22:13:59 +0000745 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000746 if not len(args) == 1:
747 parser.error('git cache populate only takes exactly one repo url.')
748 url = args[0]
749
szager@chromium.org848fd492014-04-09 19:06:44 +0000750 mirror = Mirror(url, refs=options.ref)
751 kwargs = {
752 'verbose': options.verbose,
753 'shallow': options.shallow,
754 'bootstrap': not options.no_bootstrap,
Vadim Shtayura08049e22017-10-11 00:14:52 +0000755 'ignore_lock': options.ignore_locks,
756 'lock_timeout': options.timeout,
Edward Lemur579c9862018-07-13 23:17:51 +0000757 'reset_fetch_config': options.reset_fetch_config,
szager@chromium.org848fd492014-04-09 19:06:44 +0000758 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000759 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000760 kwargs['depth'] = options.depth
761 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000762
763
szager@chromium.orgf3145112014-08-07 21:02:36 +0000764@subcommand.usage('Fetch new commits into cache and current checkout')
765def CMDfetch(parser, args):
766 """Update mirror, and fetch in cwd."""
767 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000768 parser.add_option('--no_bootstrap', '--no-bootstrap',
769 action='store_true',
770 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000771 options, args = parser.parse_args(args)
772
773 # Figure out which remotes to fetch. This mimics the behavior of regular
774 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
775 # this will NOT try to traverse up the branching structure to find the
776 # ultimate remote to update.
777 remotes = []
778 if options.all:
779 assert not args, 'fatal: fetch --all does not take a repository argument'
780 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
781 elif args:
782 remotes = args
783 else:
784 current_branch = subprocess.check_output(
785 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
786 if current_branch != 'HEAD':
787 upstream = subprocess.check_output(
788 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
789 ).strip()
790 if upstream and upstream != '.':
791 remotes = [upstream]
792 if not remotes:
793 remotes = ['origin']
794
795 cachepath = Mirror.GetCachePath()
796 git_dir = os.path.abspath(subprocess.check_output(
797 [Mirror.git_exe, 'rev-parse', '--git-dir']))
798 git_dir = os.path.abspath(git_dir)
799 if git_dir.startswith(cachepath):
800 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000801 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000802 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000803 return 0
804 for remote in remotes:
805 remote_url = subprocess.check_output(
806 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
807 if remote_url.startswith(cachepath):
808 mirror = Mirror.FromPath(remote_url)
809 mirror.print = lambda *args: None
810 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000811 mirror.populate(
Vadim Shtayura08049e22017-10-11 00:14:52 +0000812 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000813 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
814 return 0
815
816
Vadim Shtayura08049e22017-10-11 00:14:52 +0000817@subcommand.usage('[url of repo to unlock, or -a|--all]')
818def CMDunlock(parser, args):
819 """Unlock one or all repos if their lock files are still around."""
820 parser.add_option('--force', '-f', action='store_true',
821 help='Actually perform the action')
822 parser.add_option('--all', '-a', action='store_true',
823 help='Unlock all repository caches')
824 options, args = parser.parse_args(args)
825 if len(args) > 1 or (len(args) == 0 and not options.all):
826 parser.error('git cache unlock takes exactly one repo url, or --all')
827
828 if not options.force:
829 cachepath = Mirror.GetCachePath()
830 lockfiles = [os.path.join(cachepath, path)
831 for path in os.listdir(cachepath)
832 if path.endswith('.lock') and os.path.isfile(path)]
833 parser.error('git cache unlock requires -f|--force to do anything. '
834 'Refusing to unlock the following repo caches: '
835 ', '.join(lockfiles))
836
837 unlocked_repos = []
838 if options.all:
839 unlocked_repos.extend(Mirror.UnlockAll())
840 else:
841 m = Mirror(args[0])
842 if m.unlock():
843 unlocked_repos.append(m.mirror_path)
844
845 if unlocked_repos:
846 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
847 unlocked_repos))
848
849
agable@chromium.org5a306a22014-02-24 22:13:59 +0000850class OptionParser(optparse.OptionParser):
851 """Wrapper class for OptionParser to handle global options."""
852
853 def __init__(self, *args, **kwargs):
854 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
855 self.add_option('-c', '--cache-dir',
Robert Iannuccia19649b2018-06-29 16:31:45 +0000856 help=(
857 'Path to the directory containing the caches. Normally '
858 'deduced from git config cache.cachepath or '
859 '$GIT_CACHE_PATH.'))
szager@chromium.org2c391af2014-05-23 09:07:15 +0000860 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000861 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000862 self.add_option('-q', '--quiet', action='store_true',
863 help='Suppress all extraneous output')
Vadim Shtayura08049e22017-10-11 00:14:52 +0000864 self.add_option('--timeout', type='int', default=0,
865 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000866
867 def parse_args(self, args=None, values=None):
868 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000869 if options.quiet:
870 options.verbose = 0
871
872 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
873 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000874
875 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000876 global_cache_dir = Mirror.GetCachePath()
877 except RuntimeError:
878 global_cache_dir = None
879 if options.cache_dir:
880 if global_cache_dir and (
881 os.path.abspath(options.cache_dir) !=
882 os.path.abspath(global_cache_dir)):
883 logging.warn('Overriding globally-configured cache directory.')
884 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000885
agable@chromium.org5a306a22014-02-24 22:13:59 +0000886 return options, args
887
888
889def main(argv):
890 dispatcher = subcommand.CommandDispatcher(__name__)
891 return dispatcher.execute(OptionParser(), argv)
892
893
894if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000895 try:
896 sys.exit(main(sys.argv[1:]))
897 except KeyboardInterrupt:
898 sys.stderr.write('interrupted\n')
Edward Lemurdf746d02019-07-27 00:42:46 +0000899 sys.exit(1)