blob: 1b135d50b425ae9bdab8201a140ece0311390b2a [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):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000321 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000322 if shallow and not depth:
323 depth = 10000
324 gclient_utils.safe_makedirs(self.GetCachePath())
325
326 v = []
327 if verbose:
328 v = ['-v', '--progress']
329
330 d = []
331 if depth:
332 d = ['--depth', str(depth)]
333
334
335 with Lockfile(self.mirror_path):
336 # Setup from scratch if the repo is new or is in a bad state.
337 tempdir = None
szager@chromium.org301a7c32014-06-16 17:13:50 +0000338 config_file = os.path.join(self.mirror_path, 'config')
339 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
340 pack_files = []
341 if os.path.isdir(pack_dir):
342 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
343
344 should_bootstrap = (not os.path.exists(config_file) or
345 len(pack_files) > GC_AUTOPACKLIMIT)
346 if should_bootstrap:
szager@chromium.org848fd492014-04-09 19:06:44 +0000347 tempdir = tempfile.mkdtemp(
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000348 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000349 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000350 if bootstrapped:
351 # Bootstrap succeeded; delete previous cache, if any.
352 gclient_utils.rmtree(self.mirror_path)
353 elif not os.path.exists(config_file):
354 # Bootstrap failed, no previous cache; start with a bare git dir.
szager@chromium.org848fd492014-04-09 19:06:44 +0000355 self.RunGit(['init', '--bare'], cwd=tempdir)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000356 else:
357 # Bootstrap failed, previous cache exists; warn and continue.
358 logging.warn(
359 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
360 'but failed. Continuing with non-optimized repository.'
361 % len(pack_files))
362 gclient_utils.rmtree(tempdir)
363 tempdir = None
szager@chromium.org848fd492014-04-09 19:06:44 +0000364 else:
365 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
366 logging.warn(
367 'Shallow fetch requested, but repo cache already exists.')
368 d = []
369
370 rundir = tempdir or self.mirror_path
371 self.config(rundir)
372 fetch_cmd = ['fetch'] + v + d + ['origin']
373 fetch_specs = subprocess.check_output(
374 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
375 cwd=rundir).strip().splitlines()
376 for spec in fetch_specs:
377 try:
378 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
379 except subprocess.CalledProcessError:
380 logging.warn('Fetch of %s failed' % spec)
381 if tempdir:
382 os.rename(tempdir, self.mirror_path)
383
384 def update_bootstrap(self):
385 # The files are named <git number>.zip
386 gen_number = subprocess.check_output(
387 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
388 self.RunGit(['gc']) # Run Garbage Collect to compress packfile.
389 # Creating a temp file and then deleting it ensures we can use this name.
390 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
391 os.remove(tmp_zipfile)
392 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
393 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
394 dest_name = 'gs://%s/%s/%s.zip' % (
395 self.bootstrap_bucket, self.basedir, gen_number)
396 gsutil.call('cp', tmp_zipfile, dest_name)
397 os.remove(tmp_zipfile)
398
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000399 @staticmethod
400 def DeleteTmpPackFiles(path):
401 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000402 if not os.path.isdir(pack_dir):
403 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000404 pack_files = [f for f in os.listdir(pack_dir) if
405 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
406 for f in pack_files:
407 f = os.path.join(pack_dir, f)
408 try:
409 os.remove(f)
410 logging.warn('Deleted stale temporary pack file %s' % f)
411 except OSError:
412 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000413
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000414 @classmethod
415 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000416 did_unlock = False
417 lf = Lockfile(path)
418 if lf.break_lock():
419 did_unlock = True
420 # Look for lock files that might have been left behind by an interrupted
421 # git process.
422 lf = os.path.join(path, 'config.lock')
423 if os.path.exists(lf):
424 os.remove(lf)
425 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000426 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000427 return did_unlock
428
szager@chromium.org848fd492014-04-09 19:06:44 +0000429 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000430 return self.BreakLocks(self.mirror_path)
431
432 @classmethod
433 def UnlockAll(cls):
434 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000435 if not cachepath:
436 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000437 dirlist = os.listdir(cachepath)
438 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
439 if os.path.isdir(os.path.join(cachepath, path))])
440 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000441 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
442 gclient_utils.rmtree(os.path.join(cachepath, dirent))
443 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000444 os.path.isfile(os.path.join(cachepath, dirent))):
445 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
446
447 unlocked_repos = []
448 for repo_dir in repo_dirs:
449 if cls.BreakLocks(repo_dir):
450 unlocked_repos.append(repo_dir)
451
452 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000453
agable@chromium.org5a306a22014-02-24 22:13:59 +0000454@subcommand.usage('[url of repo to check for caching]')
455def CMDexists(parser, args):
456 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000457 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000458 if not len(args) == 1:
459 parser.error('git cache exists only takes exactly one repo url.')
460 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000461 mirror = Mirror(url)
462 if mirror.exists():
463 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000464 return 0
465 return 1
466
467
hinoka@google.com563559c2014-04-02 00:36:24 +0000468@subcommand.usage('[url of repo to create a bootstrap zip file]')
469def CMDupdate_bootstrap(parser, args):
470 """Create and uploads a bootstrap tarball."""
471 # Lets just assert we can't do this on Windows.
472 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000473 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000474 return 1
475
476 # First, we need to ensure the cache is populated.
477 populate_args = args[:]
478 populate_args.append('--no_bootstrap')
479 CMDpopulate(parser, populate_args)
480
481 # Get the repo directory.
szager@chromium.org848fd492014-04-09 19:06:44 +0000482 _, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000483 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000484 mirror = Mirror(url)
485 mirror.update_bootstrap()
486 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000487
488
agable@chromium.org5a306a22014-02-24 22:13:59 +0000489@subcommand.usage('[url of repo to add to or update in cache]')
490def CMDpopulate(parser, args):
491 """Ensure that the cache has all up-to-date objects for the given repo."""
492 parser.add_option('--depth', type='int',
493 help='Only cache DEPTH commits of history')
494 parser.add_option('--shallow', '-s', action='store_true',
495 help='Only cache 10000 commits of history')
496 parser.add_option('--ref', action='append',
497 help='Specify additional refs to be fetched')
hinoka@google.com563559c2014-04-02 00:36:24 +0000498 parser.add_option('--no_bootstrap', action='store_true',
499 help='Don\'t bootstrap from Google Storage')
500
agable@chromium.org5a306a22014-02-24 22:13:59 +0000501 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000502 if not len(args) == 1:
503 parser.error('git cache populate only takes exactly one repo url.')
504 url = args[0]
505
szager@chromium.org848fd492014-04-09 19:06:44 +0000506 mirror = Mirror(url, refs=options.ref)
507 kwargs = {
508 'verbose': options.verbose,
509 'shallow': options.shallow,
510 'bootstrap': not options.no_bootstrap,
511 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000512 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000513 kwargs['depth'] = options.depth
514 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000515
516
517@subcommand.usage('[url of repo to unlock, or -a|--all]')
518def CMDunlock(parser, args):
519 """Unlock one or all repos if their lock files are still around."""
520 parser.add_option('--force', '-f', action='store_true',
521 help='Actually perform the action')
522 parser.add_option('--all', '-a', action='store_true',
523 help='Unlock all repository caches')
524 options, args = parser.parse_args(args)
525 if len(args) > 1 or (len(args) == 0 and not options.all):
526 parser.error('git cache unlock takes exactly one repo url, or --all')
527
agable@chromium.org5a306a22014-02-24 22:13:59 +0000528 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000529 cachepath = Mirror.GetCachePath()
530 lockfiles = [os.path.join(cachepath, path)
531 for path in os.listdir(cachepath)
532 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000533 parser.error('git cache unlock requires -f|--force to do anything. '
534 'Refusing to unlock the following repo caches: '
535 ', '.join(lockfiles))
536
szager@chromium.org848fd492014-04-09 19:06:44 +0000537 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000538 if options.all:
539 unlocked_repos.extend(Mirror.UnlockAll())
540 else:
541 m = Mirror(args[0])
542 if m.unlock():
543 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000544
szager@chromium.org848fd492014-04-09 19:06:44 +0000545 if unlocked_repos:
546 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
547 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000548
549
550class OptionParser(optparse.OptionParser):
551 """Wrapper class for OptionParser to handle global options."""
552
553 def __init__(self, *args, **kwargs):
554 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
555 self.add_option('-c', '--cache-dir',
556 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000557 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000558 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000559 self.add_option('-q', '--quiet', action='store_true',
560 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000561
562 def parse_args(self, args=None, values=None):
563 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000564 if options.quiet:
565 options.verbose = 0
566
567 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
568 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000569
570 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000571 global_cache_dir = Mirror.GetCachePath()
572 except RuntimeError:
573 global_cache_dir = None
574 if options.cache_dir:
575 if global_cache_dir and (
576 os.path.abspath(options.cache_dir) !=
577 os.path.abspath(global_cache_dir)):
578 logging.warn('Overriding globally-configured cache directory.')
579 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000580
agable@chromium.org5a306a22014-02-24 22:13:59 +0000581 return options, args
582
583
584def main(argv):
585 dispatcher = subcommand.CommandDispatcher(__name__)
586 return dispatcher.execute(OptionParser(), argv)
587
588
589if __name__ == '__main__':
590 sys.exit(main(sys.argv[1:]))