blob: ae8a5194878ac98f69bf0fcee1558fcec147837d [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
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000016import shutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000017import subprocess
18import sys
19import urlparse
hinoka@google.com776a2c32014-04-25 07:54:25 +000020import zipfile
agable@chromium.org5a306a22014-02-24 22:13:59 +000021
hinoka@google.com563559c2014-04-02 00:36:24 +000022from download_from_google_storage import Gsutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000023import gclient_utils
24import subcommand
25
szager@chromium.org301a7c32014-06-16 17:13:50 +000026# Analogous to gc.autopacklimit git config.
27GC_AUTOPACKLIMIT = 50
28
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000029GIT_CACHE_CORRUPT_MESSAGE = 'WARNING: The Git cache is corrupt.'
30
szager@chromium.org848fd492014-04-09 19:06:44 +000031try:
32 # pylint: disable=E0602
33 WinErr = WindowsError
34except NameError:
35 class WinErr(Exception):
36 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000037
38class LockError(Exception):
39 pass
40
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000041class RefsHeadsFailedToFetch(Exception):
42 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000043
44class Lockfile(object):
45 """Class to represent a cross-platform process-specific lockfile."""
46
47 def __init__(self, path):
48 self.path = os.path.abspath(path)
49 self.lockfile = self.path + ".lock"
50 self.pid = os.getpid()
51
52 def _read_pid(self):
53 """Read the pid stored in the lockfile.
54
55 Note: This method is potentially racy. By the time it returns the lockfile
56 may have been unlocked, removed, or stolen by some other process.
57 """
58 try:
59 with open(self.lockfile, 'r') as f:
60 pid = int(f.readline().strip())
61 except (IOError, ValueError):
62 pid = None
63 return pid
64
65 def _make_lockfile(self):
66 """Safely creates a lockfile containing the current pid."""
67 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
68 fd = os.open(self.lockfile, open_flags, 0o644)
69 f = os.fdopen(fd, 'w')
szager@chromium.org848fd492014-04-09 19:06:44 +000070 print(self.pid, file=f)
agable@chromium.org5a306a22014-02-24 22:13:59 +000071 f.close()
72
73 def _remove_lockfile(self):
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000074 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
75
76 See gclient_utils.py:rmtree docstring for more explanation on the
77 windows case.
78 """
79 if sys.platform == 'win32':
80 lockfile = os.path.normcase(self.lockfile)
81 for _ in xrange(3):
82 exitcode = subprocess.call(['cmd.exe', '/c',
83 'del', '/f', '/q', lockfile])
84 if exitcode == 0:
85 return
86 time.sleep(3)
87 raise LockError('Failed to remove lock: %s' % lockfile)
88 else:
89 os.remove(self.lockfile)
agable@chromium.org5a306a22014-02-24 22:13:59 +000090
91 def lock(self):
92 """Acquire the lock.
93
94 Note: This is a NON-BLOCKING FAIL-FAST operation.
95 Do. Or do not. There is no try.
96 """
97 try:
98 self._make_lockfile()
99 except OSError as e:
100 if e.errno == errno.EEXIST:
101 raise LockError("%s is already locked" % self.path)
102 else:
103 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
104
105 def unlock(self):
106 """Release the lock."""
szager@chromium.org108eced2014-06-19 21:22:43 +0000107 try:
108 if not self.is_locked():
109 raise LockError("%s is not locked" % self.path)
110 if not self.i_am_locking():
111 raise LockError("%s is locked, but not by me" % self.path)
112 self._remove_lockfile()
113 except WinErr:
114 # Windows is unreliable when it comes to file locking. YMMV.
115 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +0000116
117 def break_lock(self):
118 """Remove the lock, even if it was created by someone else."""
119 try:
120 self._remove_lockfile()
121 return True
122 except OSError as exc:
123 if exc.errno == errno.ENOENT:
124 return False
125 else:
126 raise
127
128 def is_locked(self):
129 """Test if the file is locked by anyone.
130
131 Note: This method is potentially racy. By the time it returns the lockfile
132 may have been unlocked, removed, or stolen by some other process.
133 """
134 return os.path.exists(self.lockfile)
135
136 def i_am_locking(self):
137 """Test if the file is locked by this process."""
138 return self.is_locked() and self.pid == self._read_pid()
139
agable@chromium.org5a306a22014-02-24 22:13:59 +0000140
szager@chromium.org848fd492014-04-09 19:06:44 +0000141class Mirror(object):
142
143 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
144 gsutil_exe = os.path.join(
145 os.path.dirname(os.path.abspath(__file__)),
146 'third_party', 'gsutil', 'gsutil')
szager@chromium.org848fd492014-04-09 19:06:44 +0000147
148 def __init__(self, url, refs=None, print_func=None):
149 self.url = url
150 self.refs = refs or []
151 self.basedir = self.UrlToCacheDir(url)
152 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
153 self.print = print_func or print
154
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000155 @property
156 def bootstrap_bucket(self):
157 if 'chrome-internal' in self.url:
158 return 'chrome-git-cache'
159 else:
160 return 'chromium-git-cache'
161
szager@chromium.org174766f2014-05-13 21:27:46 +0000162 @classmethod
163 def FromPath(cls, path):
164 return cls(cls.CacheDirToUrl(path))
165
szager@chromium.org848fd492014-04-09 19:06:44 +0000166 @staticmethod
167 def UrlToCacheDir(url):
168 """Convert a git url to a normalized form for the cache dir path."""
169 parsed = urlparse.urlparse(url)
170 norm_url = parsed.netloc + parsed.path
171 if norm_url.endswith('.git'):
172 norm_url = norm_url[:-len('.git')]
173 return norm_url.replace('-', '--').replace('/', '-').lower()
174
175 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000176 def CacheDirToUrl(path):
177 """Convert a cache dir path to its corresponding url."""
178 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
179 return 'https://%s' % netpath
180
181 @staticmethod
szager@chromium.org848fd492014-04-09 19:06:44 +0000182 def FindExecutable(executable):
183 """This mimics the "which" utility."""
184 path_folders = os.environ.get('PATH').split(os.pathsep)
185
186 for path_folder in path_folders:
187 target = os.path.join(path_folder, executable)
188 # Just incase we have some ~/blah paths.
189 target = os.path.abspath(os.path.expanduser(target))
190 if os.path.isfile(target) and os.access(target, os.X_OK):
191 return target
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000192 if sys.platform.startswith('win'):
193 for suffix in ('.bat', '.cmd', '.exe'):
194 alt_target = target + suffix
szager@chromium.org4039b312014-04-09 21:56:46 +0000195 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000196 return alt_target
szager@chromium.org848fd492014-04-09 19:06:44 +0000197 return None
198
199 @classmethod
200 def SetCachePath(cls, cachepath):
201 setattr(cls, 'cachepath', cachepath)
202
203 @classmethod
204 def GetCachePath(cls):
205 if not hasattr(cls, 'cachepath'):
206 try:
207 cachepath = subprocess.check_output(
208 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
209 except subprocess.CalledProcessError:
210 cachepath = None
211 if not cachepath:
212 raise RuntimeError('No global cache.cachepath git configuration found.')
213 setattr(cls, 'cachepath', cachepath)
214 return getattr(cls, 'cachepath')
215
216 def RunGit(self, cmd, **kwargs):
217 """Run git in a subprocess."""
218 cwd = kwargs.setdefault('cwd', self.mirror_path)
219 kwargs.setdefault('print_stdout', False)
220 kwargs.setdefault('filter_fn', self.print)
221 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
222 env.setdefault('GIT_ASKPASS', 'true')
223 env.setdefault('SSH_ASKPASS', 'true')
224 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
225 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
226
227 def config(self, cwd=None):
228 if cwd is None:
229 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000230
231 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
232 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
233
234 # Don't combine pack files into one big pack file. It's really slow for
235 # repositories, and there's no way to track progress and make sure it's
236 # not stuck.
237 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
238
239 # Allocate more RAM for cache-ing delta chains, for better performance
240 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000241 self.RunGit(['config', 'core.deltaBaseCacheLimit',
242 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000243
szager@chromium.org848fd492014-04-09 19:06:44 +0000244 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
245 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
246 '+refs/heads/*:refs/heads/*'], cwd=cwd)
247 for ref in self.refs:
248 ref = ref.lstrip('+').rstrip('/')
249 if ref.startswith('refs/'):
250 refspec = '+%s:%s' % (ref, ref)
251 else:
252 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
253 self.RunGit(['config', '--add', 'remote.origin.fetch', refspec], cwd=cwd)
254
255 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000256 """Bootstrap the repo from Google Stroage if possible.
257
258 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
259 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000260
hinoka@google.com776a2c32014-04-25 07:54:25 +0000261 python_fallback = False
262 if sys.platform.startswith('win') and not self.FindExecutable('7z'):
263 python_fallback = True
264 elif sys.platform.startswith('darwin'):
265 # The OSX version of unzip doesn't support zip64.
266 python_fallback = True
267 elif not self.FindExecutable('unzip'):
268 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000269
270 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.orgc2bc22d2014-06-05 21:19:38 +0000271 gsutil = Gsutil(self.gsutil_exe, boto_path=None, bypass_prodaccess=True)
szager@chromium.org848fd492014-04-09 19:06:44 +0000272 # Get the most recent version of the zipfile.
273 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
274 ls_out_sorted = sorted(ls_out.splitlines())
275 if not ls_out_sorted:
276 # This repo is not on Google Storage.
277 return False
278 latest_checkout = ls_out_sorted[-1]
279
280 # Download zip file to a temporary directory.
281 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000282 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000283 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000284 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000285 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000286 return False
287 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
288
hinoka@google.com776a2c32014-04-25 07:54:25 +0000289 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
290 if not python_fallback:
291 if sys.platform.startswith('win'):
292 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
293 else:
294 cmd = ['unzip', filename, '-d', directory]
295 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000296 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000297 try:
298 with zipfile.ZipFile(filename, 'r') as f:
299 f.printdir()
300 f.extractall(directory)
301 except Exception as e:
302 self.print('Encountered error: %s' % str(e), file=sys.stderr)
303 retcode = 1
304 else:
305 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000306 finally:
307 # Clean up the downloaded zipfile.
308 gclient_utils.rmtree(tempdir)
309
310 if retcode:
311 self.print(
312 'Extracting bootstrap zipfile %s failed.\n'
313 'Resuming normal operations.' % filename)
314 return False
315 return True
316
317 def exists(self):
318 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
319
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000320 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
321 tempdir = None
322 config_file = os.path.join(self.mirror_path, 'config')
323 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
324 pack_files = []
325
326 if os.path.isdir(pack_dir):
327 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
328
329 should_bootstrap = (force or
330 not os.path.exists(config_file) or
331 len(pack_files) > GC_AUTOPACKLIMIT)
332 if should_bootstrap:
333 tempdir = tempfile.mkdtemp(
334 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
335 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
336 if bootstrapped:
337 # Bootstrap succeeded; delete previous cache, if any.
338 try:
339 # Try to move folder to tempdir if possible.
340 defunct_dir = tempfile.mkdtemp()
341 shutil.move(self.mirror_path, defunct_dir)
342 self.print('Moved defunct directory for repository %s from %s to %s'
343 % (self.url, self.mirror_path, defunct_dir))
344 except Exception:
345 gclient_utils.rmtree(self.mirror_path)
346 elif not os.path.exists(config_file):
347 # Bootstrap failed, no previous cache; start with a bare git dir.
348 self.RunGit(['init', '--bare'], cwd=tempdir)
349 else:
350 # Bootstrap failed, previous cache exists; warn and continue.
351 logging.warn(
352 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
353 'but failed. Continuing with non-optimized repository.'
354 % len(pack_files))
355 gclient_utils.rmtree(tempdir)
356 tempdir = None
357 else:
358 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
359 logging.warn(
360 'Shallow fetch requested, but repo cache already exists.')
361 return tempdir
362
363 def _fetch(self, rundir, verbose, depth):
364 self.config(rundir)
365 v = []
366 d = []
367 if verbose:
368 v = ['-v', '--progress']
369 if depth:
370 d = ['--depth', str(depth)]
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.print('Fetching %s' % spec)
378 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
379 except subprocess.CalledProcessError:
380 if spec == '+refs/heads/*:refs/heads/*':
381 raise RefsHeadsFailedToFetch
382 logging.warn('Fetch of %s failed' % spec)
383
szager@chromium.org848fd492014-04-09 19:06:44 +0000384 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.org108eced2014-06-19 21:22:43 +0000385 verbose=False, ignore_lock=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000386 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000387 if shallow and not depth:
388 depth = 10000
389 gclient_utils.safe_makedirs(self.GetCachePath())
390
szager@chromium.org108eced2014-06-19 21:22:43 +0000391 lockfile = Lockfile(self.mirror_path)
392 if not ignore_lock:
393 lockfile.lock()
394
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000395 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000396 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000397 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000398 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000399 self._fetch(rundir, verbose, depth)
400 except RefsHeadsFailedToFetch:
401 # This is a major failure, we need to clean and force a bootstrap.
402 gclient_utils.rmtree(rundir)
403 self.print(GIT_CACHE_CORRUPT_MESSAGE)
404 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
405 assert tempdir
406 self._fetch(tempdir or self.mirror_path, verbose, depth)
407 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000408 if tempdir:
409 os.rename(tempdir, self.mirror_path)
szager@chromium.org108eced2014-06-19 21:22:43 +0000410 if not ignore_lock:
411 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000412
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000413 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000414 # The files are named <git number>.zip
415 gen_number = subprocess.check_output(
416 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
417 self.RunGit(['gc']) # Run Garbage Collect to compress packfile.
418 # Creating a temp file and then deleting it ensures we can use this name.
419 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
420 os.remove(tmp_zipfile)
421 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
422 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000423 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
424 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000425 gsutil.call('cp', tmp_zipfile, dest_name)
426 os.remove(tmp_zipfile)
427
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000428 # Remove all other files in the same directory.
429 if prune:
430 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
431 for filename in ls_out.splitlines():
432 if filename == dest_name:
433 continue
434 gsutil.call('rm', filename)
435
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000436 @staticmethod
437 def DeleteTmpPackFiles(path):
438 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000439 if not os.path.isdir(pack_dir):
440 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000441 pack_files = [f for f in os.listdir(pack_dir) if
442 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
443 for f in pack_files:
444 f = os.path.join(pack_dir, f)
445 try:
446 os.remove(f)
447 logging.warn('Deleted stale temporary pack file %s' % f)
448 except OSError:
449 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000450
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000451 @classmethod
452 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000453 did_unlock = False
454 lf = Lockfile(path)
455 if lf.break_lock():
456 did_unlock = True
457 # Look for lock files that might have been left behind by an interrupted
458 # git process.
459 lf = os.path.join(path, 'config.lock')
460 if os.path.exists(lf):
461 os.remove(lf)
462 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000463 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000464 return did_unlock
465
szager@chromium.org848fd492014-04-09 19:06:44 +0000466 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000467 return self.BreakLocks(self.mirror_path)
468
469 @classmethod
470 def UnlockAll(cls):
471 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000472 if not cachepath:
473 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000474 dirlist = os.listdir(cachepath)
475 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
476 if os.path.isdir(os.path.join(cachepath, path))])
477 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000478 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
479 gclient_utils.rmtree(os.path.join(cachepath, dirent))
480 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000481 os.path.isfile(os.path.join(cachepath, dirent))):
482 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
483
484 unlocked_repos = []
485 for repo_dir in repo_dirs:
486 if cls.BreakLocks(repo_dir):
487 unlocked_repos.append(repo_dir)
488
489 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000490
agable@chromium.org5a306a22014-02-24 22:13:59 +0000491@subcommand.usage('[url of repo to check for caching]')
492def CMDexists(parser, args):
493 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000494 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000495 if not len(args) == 1:
496 parser.error('git cache exists only takes exactly one repo url.')
497 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000498 mirror = Mirror(url)
499 if mirror.exists():
500 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000501 return 0
502 return 1
503
504
hinoka@google.com563559c2014-04-02 00:36:24 +0000505@subcommand.usage('[url of repo to create a bootstrap zip file]')
506def CMDupdate_bootstrap(parser, args):
507 """Create and uploads a bootstrap tarball."""
508 # Lets just assert we can't do this on Windows.
509 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000510 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000511 return 1
512
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000513 parser.add_option('--prune', action='store_true',
514 help='Prune all other cached zipballs of the same repo.')
515
hinoka@google.com563559c2014-04-02 00:36:24 +0000516 # First, we need to ensure the cache is populated.
517 populate_args = args[:]
518 populate_args.append('--no_bootstrap')
519 CMDpopulate(parser, populate_args)
520
521 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000522 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000523 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000524 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000525 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000526 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000527
528
agable@chromium.org5a306a22014-02-24 22:13:59 +0000529@subcommand.usage('[url of repo to add to or update in cache]')
530def CMDpopulate(parser, args):
531 """Ensure that the cache has all up-to-date objects for the given repo."""
532 parser.add_option('--depth', type='int',
533 help='Only cache DEPTH commits of history')
534 parser.add_option('--shallow', '-s', action='store_true',
535 help='Only cache 10000 commits of history')
536 parser.add_option('--ref', action='append',
537 help='Specify additional refs to be fetched')
hinoka@google.com563559c2014-04-02 00:36:24 +0000538 parser.add_option('--no_bootstrap', action='store_true',
539 help='Don\'t bootstrap from Google Storage')
szager@chromium.org108eced2014-06-19 21:22:43 +0000540 parser.add_option('--ignore_locks', action='store_true',
541 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000542
agable@chromium.org5a306a22014-02-24 22:13:59 +0000543 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000544 if not len(args) == 1:
545 parser.error('git cache populate only takes exactly one repo url.')
546 url = args[0]
547
szager@chromium.org848fd492014-04-09 19:06:44 +0000548 mirror = Mirror(url, refs=options.ref)
549 kwargs = {
550 'verbose': options.verbose,
551 'shallow': options.shallow,
552 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000553 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000554 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000555 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000556 kwargs['depth'] = options.depth
557 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000558
559
560@subcommand.usage('[url of repo to unlock, or -a|--all]')
561def CMDunlock(parser, args):
562 """Unlock one or all repos if their lock files are still around."""
563 parser.add_option('--force', '-f', action='store_true',
564 help='Actually perform the action')
565 parser.add_option('--all', '-a', action='store_true',
566 help='Unlock all repository caches')
567 options, args = parser.parse_args(args)
568 if len(args) > 1 or (len(args) == 0 and not options.all):
569 parser.error('git cache unlock takes exactly one repo url, or --all')
570
agable@chromium.org5a306a22014-02-24 22:13:59 +0000571 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000572 cachepath = Mirror.GetCachePath()
573 lockfiles = [os.path.join(cachepath, path)
574 for path in os.listdir(cachepath)
575 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000576 parser.error('git cache unlock requires -f|--force to do anything. '
577 'Refusing to unlock the following repo caches: '
578 ', '.join(lockfiles))
579
szager@chromium.org848fd492014-04-09 19:06:44 +0000580 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000581 if options.all:
582 unlocked_repos.extend(Mirror.UnlockAll())
583 else:
584 m = Mirror(args[0])
585 if m.unlock():
586 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000587
szager@chromium.org848fd492014-04-09 19:06:44 +0000588 if unlocked_repos:
589 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
590 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000591
592
593class OptionParser(optparse.OptionParser):
594 """Wrapper class for OptionParser to handle global options."""
595
596 def __init__(self, *args, **kwargs):
597 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
598 self.add_option('-c', '--cache-dir',
599 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000600 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000601 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000602 self.add_option('-q', '--quiet', action='store_true',
603 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000604
605 def parse_args(self, args=None, values=None):
606 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000607 if options.quiet:
608 options.verbose = 0
609
610 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
611 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000612
613 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000614 global_cache_dir = Mirror.GetCachePath()
615 except RuntimeError:
616 global_cache_dir = None
617 if options.cache_dir:
618 if global_cache_dir and (
619 os.path.abspath(options.cache_dir) !=
620 os.path.abspath(global_cache_dir)):
621 logging.warn('Overriding globally-configured cache directory.')
622 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000623
agable@chromium.org5a306a22014-02-24 22:13:59 +0000624 return options, args
625
626
627def main(argv):
628 dispatcher = subcommand.CommandDispatcher(__name__)
629 return dispatcher.execute(OptionParser(), argv)
630
631
632if __name__ == '__main__':
633 sys.exit(main(sys.argv[1:]))