blob: 75fb29ac4a638cc87353e04b957a53f383bc7dc9 [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."""
szager@chromium.org108eced2014-06-19 21:22:43 +0000102 try:
103 if not self.is_locked():
104 raise LockError("%s is not locked" % self.path)
105 if not self.i_am_locking():
106 raise LockError("%s is locked, but not by me" % self.path)
107 self._remove_lockfile()
108 except WinErr:
109 # Windows is unreliable when it comes to file locking. YMMV.
110 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +0000111
112 def break_lock(self):
113 """Remove the lock, even if it was created by someone else."""
114 try:
115 self._remove_lockfile()
116 return True
117 except OSError as exc:
118 if exc.errno == errno.ENOENT:
119 return False
120 else:
121 raise
122
123 def is_locked(self):
124 """Test if the file is locked by anyone.
125
126 Note: This method is potentially racy. By the time it returns the lockfile
127 may have been unlocked, removed, or stolen by some other process.
128 """
129 return os.path.exists(self.lockfile)
130
131 def i_am_locking(self):
132 """Test if the file is locked by this process."""
133 return self.is_locked() and self.pid == self._read_pid()
134
agable@chromium.org5a306a22014-02-24 22:13:59 +0000135
szager@chromium.org848fd492014-04-09 19:06:44 +0000136class Mirror(object):
137
138 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
139 gsutil_exe = os.path.join(
140 os.path.dirname(os.path.abspath(__file__)),
141 'third_party', 'gsutil', 'gsutil')
szager@chromium.org848fd492014-04-09 19:06:44 +0000142
143 def __init__(self, url, refs=None, print_func=None):
144 self.url = url
145 self.refs = refs or []
146 self.basedir = self.UrlToCacheDir(url)
147 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
148 self.print = print_func or print
149
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000150 @property
151 def bootstrap_bucket(self):
152 if 'chrome-internal' in self.url:
153 return 'chrome-git-cache'
154 else:
155 return 'chromium-git-cache'
156
szager@chromium.org174766f2014-05-13 21:27:46 +0000157 @classmethod
158 def FromPath(cls, path):
159 return cls(cls.CacheDirToUrl(path))
160
szager@chromium.org848fd492014-04-09 19:06:44 +0000161 @staticmethod
162 def UrlToCacheDir(url):
163 """Convert a git url to a normalized form for the cache dir path."""
164 parsed = urlparse.urlparse(url)
165 norm_url = parsed.netloc + parsed.path
166 if norm_url.endswith('.git'):
167 norm_url = norm_url[:-len('.git')]
168 return norm_url.replace('-', '--').replace('/', '-').lower()
169
170 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000171 def CacheDirToUrl(path):
172 """Convert a cache dir path to its corresponding url."""
173 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
174 return 'https://%s' % netpath
175
176 @staticmethod
szager@chromium.org848fd492014-04-09 19:06:44 +0000177 def FindExecutable(executable):
178 """This mimics the "which" utility."""
179 path_folders = os.environ.get('PATH').split(os.pathsep)
180
181 for path_folder in path_folders:
182 target = os.path.join(path_folder, executable)
183 # Just incase we have some ~/blah paths.
184 target = os.path.abspath(os.path.expanduser(target))
185 if os.path.isfile(target) and os.access(target, os.X_OK):
186 return target
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000187 if sys.platform.startswith('win'):
188 for suffix in ('.bat', '.cmd', '.exe'):
189 alt_target = target + suffix
szager@chromium.org4039b312014-04-09 21:56:46 +0000190 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000191 return alt_target
szager@chromium.org848fd492014-04-09 19:06:44 +0000192 return None
193
194 @classmethod
195 def SetCachePath(cls, cachepath):
196 setattr(cls, 'cachepath', cachepath)
197
198 @classmethod
199 def GetCachePath(cls):
200 if not hasattr(cls, 'cachepath'):
201 try:
202 cachepath = subprocess.check_output(
203 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
204 except subprocess.CalledProcessError:
205 cachepath = None
206 if not cachepath:
207 raise RuntimeError('No global cache.cachepath git configuration found.')
208 setattr(cls, 'cachepath', cachepath)
209 return getattr(cls, 'cachepath')
210
211 def RunGit(self, cmd, **kwargs):
212 """Run git in a subprocess."""
213 cwd = kwargs.setdefault('cwd', self.mirror_path)
214 kwargs.setdefault('print_stdout', False)
215 kwargs.setdefault('filter_fn', self.print)
216 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
217 env.setdefault('GIT_ASKPASS', 'true')
218 env.setdefault('SSH_ASKPASS', 'true')
219 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
220 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
221
222 def config(self, cwd=None):
223 if cwd is None:
224 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000225
226 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
227 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
228
229 # Don't combine pack files into one big pack file. It's really slow for
230 # repositories, and there's no way to track progress and make sure it's
231 # not stuck.
232 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
233
234 # Allocate more RAM for cache-ing delta chains, for better performance
235 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000236 self.RunGit(['config', 'core.deltaBaseCacheLimit',
237 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000238
szager@chromium.org848fd492014-04-09 19:06:44 +0000239 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
240 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
241 '+refs/heads/*:refs/heads/*'], cwd=cwd)
242 for ref in self.refs:
243 ref = ref.lstrip('+').rstrip('/')
244 if ref.startswith('refs/'):
245 refspec = '+%s:%s' % (ref, ref)
246 else:
247 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
248 self.RunGit(['config', '--add', 'remote.origin.fetch', refspec], cwd=cwd)
249
250 def bootstrap_repo(self, directory):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000251 """Bootstrap the repo from Google Stroage if possible."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000252
hinoka@google.com776a2c32014-04-25 07:54:25 +0000253 python_fallback = False
254 if sys.platform.startswith('win') and not self.FindExecutable('7z'):
255 python_fallback = True
256 elif sys.platform.startswith('darwin'):
257 # The OSX version of unzip doesn't support zip64.
258 python_fallback = True
259 elif not self.FindExecutable('unzip'):
260 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000261
262 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.orgc2bc22d2014-06-05 21:19:38 +0000263 gsutil = Gsutil(self.gsutil_exe, boto_path=None, bypass_prodaccess=True)
szager@chromium.org848fd492014-04-09 19:06:44 +0000264 # Get the most recent version of the zipfile.
265 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
266 ls_out_sorted = sorted(ls_out.splitlines())
267 if not ls_out_sorted:
268 # This repo is not on Google Storage.
269 return False
270 latest_checkout = ls_out_sorted[-1]
271
272 # Download zip file to a temporary directory.
273 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000274 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000275 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000276 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000277 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000278 return False
279 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
280
hinoka@google.com776a2c32014-04-25 07:54:25 +0000281 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
282 if not python_fallback:
283 if sys.platform.startswith('win'):
284 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
285 else:
286 cmd = ['unzip', filename, '-d', directory]
287 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000288 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000289 try:
290 with zipfile.ZipFile(filename, 'r') as f:
291 f.printdir()
292 f.extractall(directory)
293 except Exception as e:
294 self.print('Encountered error: %s' % str(e), file=sys.stderr)
295 retcode = 1
296 else:
297 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000298 finally:
299 # Clean up the downloaded zipfile.
300 gclient_utils.rmtree(tempdir)
301
302 if retcode:
303 self.print(
304 'Extracting bootstrap zipfile %s failed.\n'
305 'Resuming normal operations.' % filename)
306 return False
307 return True
308
309 def exists(self):
310 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
311
312 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.org108eced2014-06-19 21:22:43 +0000313 verbose=False, ignore_lock=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000314 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000315 if shallow and not depth:
316 depth = 10000
317 gclient_utils.safe_makedirs(self.GetCachePath())
318
319 v = []
320 if verbose:
321 v = ['-v', '--progress']
322
323 d = []
324 if depth:
325 d = ['--depth', str(depth)]
326
327
szager@chromium.org108eced2014-06-19 21:22:43 +0000328 lockfile = Lockfile(self.mirror_path)
329 if not ignore_lock:
330 lockfile.lock()
331
332 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000333 # Setup from scratch if the repo is new or is in a bad state.
334 tempdir = None
szager@chromium.org301a7c32014-06-16 17:13:50 +0000335 config_file = os.path.join(self.mirror_path, 'config')
336 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
337 pack_files = []
338 if os.path.isdir(pack_dir):
339 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
340
341 should_bootstrap = (not os.path.exists(config_file) or
342 len(pack_files) > GC_AUTOPACKLIMIT)
343 if should_bootstrap:
szager@chromium.org848fd492014-04-09 19:06:44 +0000344 tempdir = tempfile.mkdtemp(
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000345 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000346 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000347 if bootstrapped:
348 # Bootstrap succeeded; delete previous cache, if any.
349 gclient_utils.rmtree(self.mirror_path)
350 elif not os.path.exists(config_file):
351 # Bootstrap failed, no previous cache; start with a bare git dir.
szager@chromium.org848fd492014-04-09 19:06:44 +0000352 self.RunGit(['init', '--bare'], cwd=tempdir)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000353 else:
354 # Bootstrap failed, previous cache exists; warn and continue.
355 logging.warn(
356 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
357 'but failed. Continuing with non-optimized repository.'
358 % len(pack_files))
359 gclient_utils.rmtree(tempdir)
360 tempdir = None
szager@chromium.org848fd492014-04-09 19:06:44 +0000361 else:
362 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
363 logging.warn(
364 'Shallow fetch requested, but repo cache already exists.')
365 d = []
366
367 rundir = tempdir or self.mirror_path
368 self.config(rundir)
369 fetch_cmd = ['fetch'] + v + d + ['origin']
370 fetch_specs = subprocess.check_output(
371 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
372 cwd=rundir).strip().splitlines()
373 for spec in fetch_specs:
374 try:
375 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
376 except subprocess.CalledProcessError:
377 logging.warn('Fetch of %s failed' % spec)
378 if tempdir:
379 os.rename(tempdir, self.mirror_path)
szager@chromium.org108eced2014-06-19 21:22:43 +0000380 finally:
381 if not ignore_lock:
382 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000383
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')
szager@chromium.org108eced2014-06-19 21:22:43 +0000511 parser.add_option('--ignore_locks', action='store_true',
512 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000513
agable@chromium.org5a306a22014-02-24 22:13:59 +0000514 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000515 if not len(args) == 1:
516 parser.error('git cache populate only takes exactly one repo url.')
517 url = args[0]
518
szager@chromium.org848fd492014-04-09 19:06:44 +0000519 mirror = Mirror(url, refs=options.ref)
520 kwargs = {
521 'verbose': options.verbose,
522 'shallow': options.shallow,
523 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000524 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000525 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000526 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000527 kwargs['depth'] = options.depth
528 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000529
530
531@subcommand.usage('[url of repo to unlock, or -a|--all]')
532def CMDunlock(parser, args):
533 """Unlock one or all repos if their lock files are still around."""
534 parser.add_option('--force', '-f', action='store_true',
535 help='Actually perform the action')
536 parser.add_option('--all', '-a', action='store_true',
537 help='Unlock all repository caches')
538 options, args = parser.parse_args(args)
539 if len(args) > 1 or (len(args) == 0 and not options.all):
540 parser.error('git cache unlock takes exactly one repo url, or --all')
541
agable@chromium.org5a306a22014-02-24 22:13:59 +0000542 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000543 cachepath = Mirror.GetCachePath()
544 lockfiles = [os.path.join(cachepath, path)
545 for path in os.listdir(cachepath)
546 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000547 parser.error('git cache unlock requires -f|--force to do anything. '
548 'Refusing to unlock the following repo caches: '
549 ', '.join(lockfiles))
550
szager@chromium.org848fd492014-04-09 19:06:44 +0000551 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000552 if options.all:
553 unlocked_repos.extend(Mirror.UnlockAll())
554 else:
555 m = Mirror(args[0])
556 if m.unlock():
557 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000558
szager@chromium.org848fd492014-04-09 19:06:44 +0000559 if unlocked_repos:
560 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
561 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000562
563
564class OptionParser(optparse.OptionParser):
565 """Wrapper class for OptionParser to handle global options."""
566
567 def __init__(self, *args, **kwargs):
568 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
569 self.add_option('-c', '--cache-dir',
570 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000571 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000572 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000573 self.add_option('-q', '--quiet', action='store_true',
574 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000575
576 def parse_args(self, args=None, values=None):
577 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000578 if options.quiet:
579 options.verbose = 0
580
581 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
582 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000583
584 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000585 global_cache_dir = Mirror.GetCachePath()
586 except RuntimeError:
587 global_cache_dir = None
588 if options.cache_dir:
589 if global_cache_dir and (
590 os.path.abspath(options.cache_dir) !=
591 os.path.abspath(global_cache_dir)):
592 logging.warn('Overriding globally-configured cache directory.')
593 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000594
agable@chromium.org5a306a22014-02-24 22:13:59 +0000595 return options, args
596
597
598def main(argv):
599 dispatcher = subcommand.CommandDispatcher(__name__)
600 return dispatcher.execute(OptionParser(), argv)
601
602
603if __name__ == '__main__':
604 sys.exit(main(sys.argv[1:]))