blob: 92332808fe0758d46cd4a8170667280a16051926 [file] [log] [blame]
agable@chromium.org5a306a22014-02-24 22:13:59 +00001#!/usr/bin/env python
2# Copyright 2014 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""A git command for managing a local cache of git repositories."""
7
szager@chromium.org848fd492014-04-09 19:06:44 +00008from __future__ import print_function
agable@chromium.org5a306a22014-02-24 22:13:59 +00009import errno
10import logging
11import optparse
12import os
szager@chromium.org174766f2014-05-13 21:27:46 +000013import re
agable@chromium.org5a306a22014-02-24 22:13:59 +000014import tempfile
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000015import time
agable@chromium.org5a306a22014-02-24 22:13:59 +000016import subprocess
17import sys
18import urlparse
hinoka@google.com776a2c32014-04-25 07:54:25 +000019import zipfile
agable@chromium.org5a306a22014-02-24 22:13:59 +000020
hinoka@google.com563559c2014-04-02 00:36:24 +000021from download_from_google_storage import Gsutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000022import gclient_utils
23import subcommand
24
szager@chromium.org301a7c32014-06-16 17:13:50 +000025# Analogous to gc.autopacklimit git config.
26GC_AUTOPACKLIMIT = 50
27
szager@chromium.org848fd492014-04-09 19:06:44 +000028try:
29 # pylint: disable=E0602
30 WinErr = WindowsError
31except NameError:
32 class WinErr(Exception):
33 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000034
35class LockError(Exception):
36 pass
37
38
39class Lockfile(object):
40 """Class to represent a cross-platform process-specific lockfile."""
41
42 def __init__(self, path):
43 self.path = os.path.abspath(path)
44 self.lockfile = self.path + ".lock"
45 self.pid = os.getpid()
46
47 def _read_pid(self):
48 """Read the pid stored in the lockfile.
49
50 Note: This method is potentially racy. By the time it returns the lockfile
51 may have been unlocked, removed, or stolen by some other process.
52 """
53 try:
54 with open(self.lockfile, 'r') as f:
55 pid = int(f.readline().strip())
56 except (IOError, ValueError):
57 pid = None
58 return pid
59
60 def _make_lockfile(self):
61 """Safely creates a lockfile containing the current pid."""
62 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
63 fd = os.open(self.lockfile, open_flags, 0o644)
64 f = os.fdopen(fd, 'w')
szager@chromium.org848fd492014-04-09 19:06:44 +000065 print(self.pid, file=f)
agable@chromium.org5a306a22014-02-24 22:13:59 +000066 f.close()
67
68 def _remove_lockfile(self):
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000069 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
70
71 See gclient_utils.py:rmtree docstring for more explanation on the
72 windows case.
73 """
74 if sys.platform == 'win32':
75 lockfile = os.path.normcase(self.lockfile)
76 for _ in xrange(3):
77 exitcode = subprocess.call(['cmd.exe', '/c',
78 'del', '/f', '/q', lockfile])
79 if exitcode == 0:
80 return
81 time.sleep(3)
82 raise LockError('Failed to remove lock: %s' % lockfile)
83 else:
84 os.remove(self.lockfile)
agable@chromium.org5a306a22014-02-24 22:13:59 +000085
86 def lock(self):
87 """Acquire the lock.
88
89 Note: This is a NON-BLOCKING FAIL-FAST operation.
90 Do. Or do not. There is no try.
91 """
92 try:
93 self._make_lockfile()
94 except OSError as e:
95 if e.errno == errno.EEXIST:
96 raise LockError("%s is already locked" % self.path)
97 else:
98 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
99
100 def unlock(self):
101 """Release the lock."""
102 if not self.is_locked():
103 raise LockError("%s is not locked" % self.path)
104 if not self.i_am_locking():
105 raise LockError("%s is locked, but not by me" % self.path)
106 self._remove_lockfile()
107
108 def break_lock(self):
109 """Remove the lock, even if it was created by someone else."""
110 try:
111 self._remove_lockfile()
112 return True
113 except OSError as exc:
114 if exc.errno == errno.ENOENT:
115 return False
116 else:
117 raise
118
119 def is_locked(self):
120 """Test if the file is locked by anyone.
121
122 Note: This method is potentially racy. By the time it returns the lockfile
123 may have been unlocked, removed, or stolen by some other process.
124 """
125 return os.path.exists(self.lockfile)
126
127 def i_am_locking(self):
128 """Test if the file is locked by this process."""
129 return self.is_locked() and self.pid == self._read_pid()
130
131 def __enter__(self):
132 self.lock()
133 return self
134
135 def __exit__(self, *_exc):
szager@chromium.org848fd492014-04-09 19:06:44 +0000136 # Windows is unreliable when it comes to file locking. YMMV.
137 try:
138 self.unlock()
139 except WinErr:
140 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +0000141
142
szager@chromium.org848fd492014-04-09 19:06:44 +0000143class Mirror(object):
144
145 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
146 gsutil_exe = os.path.join(
147 os.path.dirname(os.path.abspath(__file__)),
148 'third_party', 'gsutil', 'gsutil')
szager@chromium.org848fd492014-04-09 19:06:44 +0000149
150 def __init__(self, url, refs=None, print_func=None):
151 self.url = url
152 self.refs = refs or []
153 self.basedir = self.UrlToCacheDir(url)
154 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
155 self.print = print_func or print
156
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000157 @property
158 def bootstrap_bucket(self):
159 if 'chrome-internal' in self.url:
160 return 'chrome-git-cache'
161 else:
162 return 'chromium-git-cache'
163
szager@chromium.org174766f2014-05-13 21:27:46 +0000164 @classmethod
165 def FromPath(cls, path):
166 return cls(cls.CacheDirToUrl(path))
167
szager@chromium.org848fd492014-04-09 19:06:44 +0000168 @staticmethod
169 def UrlToCacheDir(url):
170 """Convert a git url to a normalized form for the cache dir path."""
171 parsed = urlparse.urlparse(url)
172 norm_url = parsed.netloc + parsed.path
173 if norm_url.endswith('.git'):
174 norm_url = norm_url[:-len('.git')]
175 return norm_url.replace('-', '--').replace('/', '-').lower()
176
177 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000178 def CacheDirToUrl(path):
179 """Convert a cache dir path to its corresponding url."""
180 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
181 return 'https://%s' % netpath
182
183 @staticmethod
szager@chromium.org848fd492014-04-09 19:06:44 +0000184 def FindExecutable(executable):
185 """This mimics the "which" utility."""
186 path_folders = os.environ.get('PATH').split(os.pathsep)
187
188 for path_folder in path_folders:
189 target = os.path.join(path_folder, executable)
190 # Just incase we have some ~/blah paths.
191 target = os.path.abspath(os.path.expanduser(target))
192 if os.path.isfile(target) and os.access(target, os.X_OK):
193 return target
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000194 if sys.platform.startswith('win'):
195 for suffix in ('.bat', '.cmd', '.exe'):
196 alt_target = target + suffix
szager@chromium.org4039b312014-04-09 21:56:46 +0000197 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000198 return alt_target
szager@chromium.org848fd492014-04-09 19:06:44 +0000199 return None
200
201 @classmethod
202 def SetCachePath(cls, cachepath):
203 setattr(cls, 'cachepath', cachepath)
204
205 @classmethod
206 def GetCachePath(cls):
207 if not hasattr(cls, 'cachepath'):
208 try:
209 cachepath = subprocess.check_output(
210 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
211 except subprocess.CalledProcessError:
212 cachepath = None
213 if not cachepath:
214 raise RuntimeError('No global cache.cachepath git configuration found.')
215 setattr(cls, 'cachepath', cachepath)
216 return getattr(cls, 'cachepath')
217
218 def RunGit(self, cmd, **kwargs):
219 """Run git in a subprocess."""
220 cwd = kwargs.setdefault('cwd', self.mirror_path)
221 kwargs.setdefault('print_stdout', False)
222 kwargs.setdefault('filter_fn', self.print)
223 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
224 env.setdefault('GIT_ASKPASS', 'true')
225 env.setdefault('SSH_ASKPASS', 'true')
226 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
227 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
228
229 def config(self, cwd=None):
230 if cwd is None:
231 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000232
233 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
234 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
235
236 # Don't combine pack files into one big pack file. It's really slow for
237 # repositories, and there's no way to track progress and make sure it's
238 # not stuck.
239 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
240
241 # Allocate more RAM for cache-ing delta chains, for better performance
242 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000243 self.RunGit(['config', 'core.deltaBaseCacheLimit',
244 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000245
szager@chromium.org848fd492014-04-09 19:06:44 +0000246 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
247 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
248 '+refs/heads/*:refs/heads/*'], cwd=cwd)
249 for ref in self.refs:
250 ref = ref.lstrip('+').rstrip('/')
251 if ref.startswith('refs/'):
252 refspec = '+%s:%s' % (ref, ref)
253 else:
254 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
255 self.RunGit(['config', '--add', 'remote.origin.fetch', refspec], cwd=cwd)
256
257 def bootstrap_repo(self, directory):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000258 """Bootstrap the repo from Google Stroage if possible."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000259
hinoka@google.com776a2c32014-04-25 07:54:25 +0000260 python_fallback = False
261 if sys.platform.startswith('win') and not self.FindExecutable('7z'):
262 python_fallback = True
263 elif sys.platform.startswith('darwin'):
264 # The OSX version of unzip doesn't support zip64.
265 python_fallback = True
266 elif not self.FindExecutable('unzip'):
267 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000268
269 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.orgc2bc22d2014-06-05 21:19:38 +0000270 gsutil = Gsutil(self.gsutil_exe, boto_path=None, bypass_prodaccess=True)
szager@chromium.org848fd492014-04-09 19:06:44 +0000271 # Get the most recent version of the zipfile.
272 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
273 ls_out_sorted = sorted(ls_out.splitlines())
274 if not ls_out_sorted:
275 # This repo is not on Google Storage.
276 return False
277 latest_checkout = ls_out_sorted[-1]
278
279 # Download zip file to a temporary directory.
280 try:
281 tempdir = tempfile.mkdtemp()
282 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000283 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000284 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000285 return False
286 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
287
hinoka@google.com776a2c32014-04-25 07:54:25 +0000288 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
289 if not python_fallback:
290 if sys.platform.startswith('win'):
291 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
292 else:
293 cmd = ['unzip', filename, '-d', directory]
294 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000295 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000296 try:
297 with zipfile.ZipFile(filename, 'r') as f:
298 f.printdir()
299 f.extractall(directory)
300 except Exception as e:
301 self.print('Encountered error: %s' % str(e), file=sys.stderr)
302 retcode = 1
303 else:
304 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000305 finally:
306 # Clean up the downloaded zipfile.
307 gclient_utils.rmtree(tempdir)
308
309 if retcode:
310 self.print(
311 'Extracting bootstrap zipfile %s failed.\n'
312 'Resuming normal operations.' % filename)
313 return False
314 return True
315
316 def exists(self):
317 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
318
319 def populate(self, depth=None, shallow=False, bootstrap=False,
320 verbose=False):
321 if shallow and not depth:
322 depth = 10000
323 gclient_utils.safe_makedirs(self.GetCachePath())
324
325 v = []
326 if verbose:
327 v = ['-v', '--progress']
328
329 d = []
330 if depth:
331 d = ['--depth', str(depth)]
332
333
334 with Lockfile(self.mirror_path):
335 # Setup from scratch if the repo is new or is in a bad state.
336 tempdir = None
szager@chromium.org301a7c32014-06-16 17:13:50 +0000337 config_file = os.path.join(self.mirror_path, 'config')
338 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
339 pack_files = []
340 if os.path.isdir(pack_dir):
341 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
342
343 should_bootstrap = (not os.path.exists(config_file) or
344 len(pack_files) > GC_AUTOPACKLIMIT)
345 if should_bootstrap:
szager@chromium.org848fd492014-04-09 19:06:44 +0000346 tempdir = tempfile.mkdtemp(
347 suffix=self.basedir, dir=self.GetCachePath())
348 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000349 if bootstrapped:
350 # Bootstrap succeeded; delete previous cache, if any.
351 gclient_utils.rmtree(self.mirror_path)
352 elif not os.path.exists(config_file):
353 # Bootstrap failed, no previous cache; start with a bare git dir.
szager@chromium.org848fd492014-04-09 19:06:44 +0000354 self.RunGit(['init', '--bare'], cwd=tempdir)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000355 else:
356 # Bootstrap failed, previous cache exists; warn and continue.
357 logging.warn(
358 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
359 'but failed. Continuing with non-optimized repository.'
360 % len(pack_files))
361 gclient_utils.rmtree(tempdir)
362 tempdir = None
szager@chromium.org848fd492014-04-09 19:06:44 +0000363 else:
364 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
365 logging.warn(
366 'Shallow fetch requested, but repo cache already exists.')
367 d = []
368
369 rundir = tempdir or self.mirror_path
370 self.config(rundir)
371 fetch_cmd = ['fetch'] + v + d + ['origin']
372 fetch_specs = subprocess.check_output(
373 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
374 cwd=rundir).strip().splitlines()
375 for spec in fetch_specs:
376 try:
377 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
378 except subprocess.CalledProcessError:
379 logging.warn('Fetch of %s failed' % spec)
380 if tempdir:
381 os.rename(tempdir, self.mirror_path)
382
383 def update_bootstrap(self):
384 # The files are named <git number>.zip
385 gen_number = subprocess.check_output(
386 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
387 self.RunGit(['gc']) # Run Garbage Collect to compress packfile.
388 # Creating a temp file and then deleting it ensures we can use this name.
389 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
390 os.remove(tmp_zipfile)
391 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
392 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
393 dest_name = 'gs://%s/%s/%s.zip' % (
394 self.bootstrap_bucket, self.basedir, gen_number)
395 gsutil.call('cp', tmp_zipfile, dest_name)
396 os.remove(tmp_zipfile)
397
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000398 @staticmethod
399 def DeleteTmpPackFiles(path):
400 pack_dir = os.path.join(path, 'objects', 'pack')
401 pack_files = [f for f in os.listdir(pack_dir) if
402 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
403 for f in pack_files:
404 f = os.path.join(pack_dir, f)
405 try:
406 os.remove(f)
407 logging.warn('Deleted stale temporary pack file %s' % f)
408 except OSError:
409 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000410
411 @staticmethod
412 def BreakLocks(path):
413 did_unlock = False
414 lf = Lockfile(path)
415 if lf.break_lock():
416 did_unlock = True
417 # Look for lock files that might have been left behind by an interrupted
418 # git process.
419 lf = os.path.join(path, 'config.lock')
420 if os.path.exists(lf):
421 os.remove(lf)
422 did_unlock = True
423 return did_unlock
424
szager@chromium.org848fd492014-04-09 19:06:44 +0000425 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000426 return self.BreakLocks(self.mirror_path)
427
428 @classmethod
429 def UnlockAll(cls):
430 cachepath = cls.GetCachePath()
431 dirlist = os.listdir(cachepath)
432 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
433 if os.path.isdir(os.path.join(cachepath, path))])
434 for dirent in dirlist:
435 if (dirent.endswith('.lock') and
436 os.path.isfile(os.path.join(cachepath, dirent))):
437 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
438
439 unlocked_repos = []
440 for repo_dir in repo_dirs:
441 if cls.BreakLocks(repo_dir):
442 unlocked_repos.append(repo_dir)
443
444 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000445
agable@chromium.org5a306a22014-02-24 22:13:59 +0000446@subcommand.usage('[url of repo to check for caching]')
447def CMDexists(parser, args):
448 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000449 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000450 if not len(args) == 1:
451 parser.error('git cache exists only takes exactly one repo url.')
452 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000453 mirror = Mirror(url)
454 if mirror.exists():
455 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000456 return 0
457 return 1
458
459
hinoka@google.com563559c2014-04-02 00:36:24 +0000460@subcommand.usage('[url of repo to create a bootstrap zip file]')
461def CMDupdate_bootstrap(parser, args):
462 """Create and uploads a bootstrap tarball."""
463 # Lets just assert we can't do this on Windows.
464 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000465 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000466 return 1
467
468 # First, we need to ensure the cache is populated.
469 populate_args = args[:]
470 populate_args.append('--no_bootstrap')
471 CMDpopulate(parser, populate_args)
472
473 # Get the repo directory.
szager@chromium.org848fd492014-04-09 19:06:44 +0000474 _, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000475 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000476 mirror = Mirror(url)
477 mirror.update_bootstrap()
478 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000479
480
agable@chromium.org5a306a22014-02-24 22:13:59 +0000481@subcommand.usage('[url of repo to add to or update in cache]')
482def CMDpopulate(parser, args):
483 """Ensure that the cache has all up-to-date objects for the given repo."""
484 parser.add_option('--depth', type='int',
485 help='Only cache DEPTH commits of history')
486 parser.add_option('--shallow', '-s', action='store_true',
487 help='Only cache 10000 commits of history')
488 parser.add_option('--ref', action='append',
489 help='Specify additional refs to be fetched')
hinoka@google.com563559c2014-04-02 00:36:24 +0000490 parser.add_option('--no_bootstrap', action='store_true',
491 help='Don\'t bootstrap from Google Storage')
492
agable@chromium.org5a306a22014-02-24 22:13:59 +0000493 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000494 if not len(args) == 1:
495 parser.error('git cache populate only takes exactly one repo url.')
496 url = args[0]
497
szager@chromium.org848fd492014-04-09 19:06:44 +0000498 mirror = Mirror(url, refs=options.ref)
499 kwargs = {
500 'verbose': options.verbose,
501 'shallow': options.shallow,
502 'bootstrap': not options.no_bootstrap,
503 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000504 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000505 kwargs['depth'] = options.depth
506 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000507
508
509@subcommand.usage('[url of repo to unlock, or -a|--all]')
510def CMDunlock(parser, args):
511 """Unlock one or all repos if their lock files are still around."""
512 parser.add_option('--force', '-f', action='store_true',
513 help='Actually perform the action')
514 parser.add_option('--all', '-a', action='store_true',
515 help='Unlock all repository caches')
516 options, args = parser.parse_args(args)
517 if len(args) > 1 or (len(args) == 0 and not options.all):
518 parser.error('git cache unlock takes exactly one repo url, or --all')
519
agable@chromium.org5a306a22014-02-24 22:13:59 +0000520 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000521 cachepath = Mirror.GetCachePath()
522 lockfiles = [os.path.join(cachepath, path)
523 for path in os.listdir(cachepath)
524 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000525 parser.error('git cache unlock requires -f|--force to do anything. '
526 'Refusing to unlock the following repo caches: '
527 ', '.join(lockfiles))
528
szager@chromium.org848fd492014-04-09 19:06:44 +0000529 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000530 if options.all:
531 unlocked_repos.extend(Mirror.UnlockAll())
532 else:
533 m = Mirror(args[0])
534 if m.unlock():
535 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000536
szager@chromium.org848fd492014-04-09 19:06:44 +0000537 if unlocked_repos:
538 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
539 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000540
541
542class OptionParser(optparse.OptionParser):
543 """Wrapper class for OptionParser to handle global options."""
544
545 def __init__(self, *args, **kwargs):
546 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
547 self.add_option('-c', '--cache-dir',
548 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000549 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000550 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000551 self.add_option('-q', '--quiet', action='store_true',
552 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000553
554 def parse_args(self, args=None, values=None):
555 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000556 if options.quiet:
557 options.verbose = 0
558
559 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
560 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000561
562 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000563 global_cache_dir = Mirror.GetCachePath()
564 except RuntimeError:
565 global_cache_dir = None
566 if options.cache_dir:
567 if global_cache_dir and (
568 os.path.abspath(options.cache_dir) !=
569 os.path.abspath(global_cache_dir)):
570 logging.warn('Overriding globally-configured cache directory.')
571 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000572
agable@chromium.org5a306a22014-02-24 22:13:59 +0000573 return options, args
574
575
576def main(argv):
577 dispatcher = subcommand.CommandDispatcher(__name__)
578 return dispatcher.execute(OptionParser(), argv)
579
580
581if __name__ == '__main__':
582 sys.exit(main(sys.argv[1:]))