blob: 686d0e14813acb6bacb76badf63f1bdea67283f3 [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:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000281 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000282 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(
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000347 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000348 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
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000411 @classmethod
412 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000413 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
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000423 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000424 return did_unlock
425
szager@chromium.org848fd492014-04-09 19:06:44 +0000426 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000427 return self.BreakLocks(self.mirror_path)
428
429 @classmethod
430 def UnlockAll(cls):
431 cachepath = cls.GetCachePath()
432 dirlist = os.listdir(cachepath)
433 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
434 if os.path.isdir(os.path.join(cachepath, path))])
435 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000436 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
437 gclient_utils.rmtree(os.path.join(cachepath, dirent))
438 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000439 os.path.isfile(os.path.join(cachepath, dirent))):
440 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
441
442 unlocked_repos = []
443 for repo_dir in repo_dirs:
444 if cls.BreakLocks(repo_dir):
445 unlocked_repos.append(repo_dir)
446
447 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000448
agable@chromium.org5a306a22014-02-24 22:13:59 +0000449@subcommand.usage('[url of repo to check for caching]')
450def CMDexists(parser, args):
451 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000452 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000453 if not len(args) == 1:
454 parser.error('git cache exists only takes exactly one repo url.')
455 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000456 mirror = Mirror(url)
457 if mirror.exists():
458 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000459 return 0
460 return 1
461
462
hinoka@google.com563559c2014-04-02 00:36:24 +0000463@subcommand.usage('[url of repo to create a bootstrap zip file]')
464def CMDupdate_bootstrap(parser, args):
465 """Create and uploads a bootstrap tarball."""
466 # Lets just assert we can't do this on Windows.
467 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000468 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000469 return 1
470
471 # First, we need to ensure the cache is populated.
472 populate_args = args[:]
473 populate_args.append('--no_bootstrap')
474 CMDpopulate(parser, populate_args)
475
476 # Get the repo directory.
szager@chromium.org848fd492014-04-09 19:06:44 +0000477 _, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000478 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000479 mirror = Mirror(url)
480 mirror.update_bootstrap()
481 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000482
483
agable@chromium.org5a306a22014-02-24 22:13:59 +0000484@subcommand.usage('[url of repo to add to or update in cache]')
485def CMDpopulate(parser, args):
486 """Ensure that the cache has all up-to-date objects for the given repo."""
487 parser.add_option('--depth', type='int',
488 help='Only cache DEPTH commits of history')
489 parser.add_option('--shallow', '-s', action='store_true',
490 help='Only cache 10000 commits of history')
491 parser.add_option('--ref', action='append',
492 help='Specify additional refs to be fetched')
hinoka@google.com563559c2014-04-02 00:36:24 +0000493 parser.add_option('--no_bootstrap', action='store_true',
494 help='Don\'t bootstrap from Google Storage')
495
agable@chromium.org5a306a22014-02-24 22:13:59 +0000496 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000497 if not len(args) == 1:
498 parser.error('git cache populate only takes exactly one repo url.')
499 url = args[0]
500
szager@chromium.org848fd492014-04-09 19:06:44 +0000501 mirror = Mirror(url, refs=options.ref)
502 kwargs = {
503 'verbose': options.verbose,
504 'shallow': options.shallow,
505 'bootstrap': not options.no_bootstrap,
506 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000507 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000508 kwargs['depth'] = options.depth
509 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000510
511
512@subcommand.usage('[url of repo to unlock, or -a|--all]')
513def CMDunlock(parser, args):
514 """Unlock one or all repos if their lock files are still around."""
515 parser.add_option('--force', '-f', action='store_true',
516 help='Actually perform the action')
517 parser.add_option('--all', '-a', action='store_true',
518 help='Unlock all repository caches')
519 options, args = parser.parse_args(args)
520 if len(args) > 1 or (len(args) == 0 and not options.all):
521 parser.error('git cache unlock takes exactly one repo url, or --all')
522
agable@chromium.org5a306a22014-02-24 22:13:59 +0000523 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000524 cachepath = Mirror.GetCachePath()
525 lockfiles = [os.path.join(cachepath, path)
526 for path in os.listdir(cachepath)
527 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000528 parser.error('git cache unlock requires -f|--force to do anything. '
529 'Refusing to unlock the following repo caches: '
530 ', '.join(lockfiles))
531
szager@chromium.org848fd492014-04-09 19:06:44 +0000532 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000533 if options.all:
534 unlocked_repos.extend(Mirror.UnlockAll())
535 else:
536 m = Mirror(args[0])
537 if m.unlock():
538 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000539
szager@chromium.org848fd492014-04-09 19:06:44 +0000540 if unlocked_repos:
541 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
542 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000543
544
545class OptionParser(optparse.OptionParser):
546 """Wrapper class for OptionParser to handle global options."""
547
548 def __init__(self, *args, **kwargs):
549 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
550 self.add_option('-c', '--cache-dir',
551 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000552 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000553 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000554 self.add_option('-q', '--quiet', action='store_true',
555 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000556
557 def parse_args(self, args=None, values=None):
558 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000559 if options.quiet:
560 options.verbose = 0
561
562 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
563 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000564
565 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000566 global_cache_dir = Mirror.GetCachePath()
567 except RuntimeError:
568 global_cache_dir = None
569 if options.cache_dir:
570 if global_cache_dir and (
571 os.path.abspath(options.cache_dir) !=
572 os.path.abspath(global_cache_dir)):
573 logging.warn('Overriding globally-configured cache directory.')
574 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000575
agable@chromium.org5a306a22014-02-24 22:13:59 +0000576 return options, args
577
578
579def main(argv):
580 dispatcher = subcommand.CommandDispatcher(__name__)
581 return dispatcher.execute(OptionParser(), argv)
582
583
584if __name__ == '__main__':
585 sys.exit(main(sys.argv[1:]))