blob: f472a1ae6f274fb4b7911353cf87d098c9417c26 [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
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000384 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000385 # 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)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000394 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
395 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000396 gsutil.call('cp', tmp_zipfile, dest_name)
397 os.remove(tmp_zipfile)
398
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000399 # Remove all other files in the same directory.
400 if prune:
401 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
402 for filename in ls_out.splitlines():
403 if filename == dest_name:
404 continue
405 gsutil.call('rm', filename)
406
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000407 @staticmethod
408 def DeleteTmpPackFiles(path):
409 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000410 if not os.path.isdir(pack_dir):
411 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000412 pack_files = [f for f in os.listdir(pack_dir) if
413 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
414 for f in pack_files:
415 f = os.path.join(pack_dir, f)
416 try:
417 os.remove(f)
418 logging.warn('Deleted stale temporary pack file %s' % f)
419 except OSError:
420 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000421
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000422 @classmethod
423 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000424 did_unlock = False
425 lf = Lockfile(path)
426 if lf.break_lock():
427 did_unlock = True
428 # Look for lock files that might have been left behind by an interrupted
429 # git process.
430 lf = os.path.join(path, 'config.lock')
431 if os.path.exists(lf):
432 os.remove(lf)
433 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000434 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000435 return did_unlock
436
szager@chromium.org848fd492014-04-09 19:06:44 +0000437 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000438 return self.BreakLocks(self.mirror_path)
439
440 @classmethod
441 def UnlockAll(cls):
442 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000443 if not cachepath:
444 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000445 dirlist = os.listdir(cachepath)
446 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
447 if os.path.isdir(os.path.join(cachepath, path))])
448 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000449 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
450 gclient_utils.rmtree(os.path.join(cachepath, dirent))
451 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000452 os.path.isfile(os.path.join(cachepath, dirent))):
453 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
454
455 unlocked_repos = []
456 for repo_dir in repo_dirs:
457 if cls.BreakLocks(repo_dir):
458 unlocked_repos.append(repo_dir)
459
460 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000461
agable@chromium.org5a306a22014-02-24 22:13:59 +0000462@subcommand.usage('[url of repo to check for caching]')
463def CMDexists(parser, args):
464 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000465 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000466 if not len(args) == 1:
467 parser.error('git cache exists only takes exactly one repo url.')
468 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000469 mirror = Mirror(url)
470 if mirror.exists():
471 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000472 return 0
473 return 1
474
475
hinoka@google.com563559c2014-04-02 00:36:24 +0000476@subcommand.usage('[url of repo to create a bootstrap zip file]')
477def CMDupdate_bootstrap(parser, args):
478 """Create and uploads a bootstrap tarball."""
479 # Lets just assert we can't do this on Windows.
480 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000481 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000482 return 1
483
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000484 parser.add_option('--prune', action='store_true',
485 help='Prune all other cached zipballs of the same repo.')
486
hinoka@google.com563559c2014-04-02 00:36:24 +0000487 # First, we need to ensure the cache is populated.
488 populate_args = args[:]
489 populate_args.append('--no_bootstrap')
490 CMDpopulate(parser, populate_args)
491
492 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000493 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000494 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000495 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000496 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000497 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000498
499
agable@chromium.org5a306a22014-02-24 22:13:59 +0000500@subcommand.usage('[url of repo to add to or update in cache]')
501def CMDpopulate(parser, args):
502 """Ensure that the cache has all up-to-date objects for the given repo."""
503 parser.add_option('--depth', type='int',
504 help='Only cache DEPTH commits of history')
505 parser.add_option('--shallow', '-s', action='store_true',
506 help='Only cache 10000 commits of history')
507 parser.add_option('--ref', action='append',
508 help='Specify additional refs to be fetched')
hinoka@google.com563559c2014-04-02 00:36:24 +0000509 parser.add_option('--no_bootstrap', action='store_true',
510 help='Don\'t bootstrap from Google Storage')
511
agable@chromium.org5a306a22014-02-24 22:13:59 +0000512 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000513 if not len(args) == 1:
514 parser.error('git cache populate only takes exactly one repo url.')
515 url = args[0]
516
szager@chromium.org848fd492014-04-09 19:06:44 +0000517 mirror = Mirror(url, refs=options.ref)
518 kwargs = {
519 'verbose': options.verbose,
520 'shallow': options.shallow,
521 'bootstrap': not options.no_bootstrap,
522 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000523 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000524 kwargs['depth'] = options.depth
525 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000526
527
528@subcommand.usage('[url of repo to unlock, or -a|--all]')
529def CMDunlock(parser, args):
530 """Unlock one or all repos if their lock files are still around."""
531 parser.add_option('--force', '-f', action='store_true',
532 help='Actually perform the action')
533 parser.add_option('--all', '-a', action='store_true',
534 help='Unlock all repository caches')
535 options, args = parser.parse_args(args)
536 if len(args) > 1 or (len(args) == 0 and not options.all):
537 parser.error('git cache unlock takes exactly one repo url, or --all')
538
agable@chromium.org5a306a22014-02-24 22:13:59 +0000539 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000540 cachepath = Mirror.GetCachePath()
541 lockfiles = [os.path.join(cachepath, path)
542 for path in os.listdir(cachepath)
543 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000544 parser.error('git cache unlock requires -f|--force to do anything. '
545 'Refusing to unlock the following repo caches: '
546 ', '.join(lockfiles))
547
szager@chromium.org848fd492014-04-09 19:06:44 +0000548 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000549 if options.all:
550 unlocked_repos.extend(Mirror.UnlockAll())
551 else:
552 m = Mirror(args[0])
553 if m.unlock():
554 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000555
szager@chromium.org848fd492014-04-09 19:06:44 +0000556 if unlocked_repos:
557 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
558 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000559
560
561class OptionParser(optparse.OptionParser):
562 """Wrapper class for OptionParser to handle global options."""
563
564 def __init__(self, *args, **kwargs):
565 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
566 self.add_option('-c', '--cache-dir',
567 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000568 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000569 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000570 self.add_option('-q', '--quiet', action='store_true',
571 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000572
573 def parse_args(self, args=None, values=None):
574 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000575 if options.quiet:
576 options.verbose = 0
577
578 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
579 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000580
581 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000582 global_cache_dir = Mirror.GetCachePath()
583 except RuntimeError:
584 global_cache_dir = None
585 if options.cache_dir:
586 if global_cache_dir and (
587 os.path.abspath(options.cache_dir) !=
588 os.path.abspath(global_cache_dir)):
589 logging.warn('Overriding globally-configured cache directory.')
590 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000591
agable@chromium.org5a306a22014-02-24 22:13:59 +0000592 return options, args
593
594
595def main(argv):
596 dispatcher = subcommand.CommandDispatcher(__name__)
597 return dispatcher.execute(OptionParser(), argv)
598
599
600if __name__ == '__main__':
601 sys.exit(main(sys.argv[1:]))