blob: 1fa7e4556871a82e9de7090146ccf7035523a140 [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:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000409 try:
410 os.rename(tempdir, self.mirror_path)
411 except OSError as e:
412 # This is somehow racy on Windows.
413 # Catching OSError because WindowsError isn't portable and
414 # pylint complains.
415 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
416 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000417 if not ignore_lock:
418 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000419
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000420 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000421 # The files are named <git number>.zip
422 gen_number = subprocess.check_output(
423 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
424 self.RunGit(['gc']) # Run Garbage Collect to compress packfile.
425 # Creating a temp file and then deleting it ensures we can use this name.
426 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
427 os.remove(tmp_zipfile)
428 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
429 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000430 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
431 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000432 gsutil.call('cp', tmp_zipfile, dest_name)
433 os.remove(tmp_zipfile)
434
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000435 # Remove all other files in the same directory.
436 if prune:
437 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
438 for filename in ls_out.splitlines():
439 if filename == dest_name:
440 continue
441 gsutil.call('rm', filename)
442
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000443 @staticmethod
444 def DeleteTmpPackFiles(path):
445 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000446 if not os.path.isdir(pack_dir):
447 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000448 pack_files = [f for f in os.listdir(pack_dir) if
449 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
450 for f in pack_files:
451 f = os.path.join(pack_dir, f)
452 try:
453 os.remove(f)
454 logging.warn('Deleted stale temporary pack file %s' % f)
455 except OSError:
456 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000457
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000458 @classmethod
459 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000460 did_unlock = False
461 lf = Lockfile(path)
462 if lf.break_lock():
463 did_unlock = True
464 # Look for lock files that might have been left behind by an interrupted
465 # git process.
466 lf = os.path.join(path, 'config.lock')
467 if os.path.exists(lf):
468 os.remove(lf)
469 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000470 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000471 return did_unlock
472
szager@chromium.org848fd492014-04-09 19:06:44 +0000473 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000474 return self.BreakLocks(self.mirror_path)
475
476 @classmethod
477 def UnlockAll(cls):
478 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000479 if not cachepath:
480 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000481 dirlist = os.listdir(cachepath)
482 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
483 if os.path.isdir(os.path.join(cachepath, path))])
484 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000485 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
486 gclient_utils.rmtree(os.path.join(cachepath, dirent))
487 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000488 os.path.isfile(os.path.join(cachepath, dirent))):
489 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
490
491 unlocked_repos = []
492 for repo_dir in repo_dirs:
493 if cls.BreakLocks(repo_dir):
494 unlocked_repos.append(repo_dir)
495
496 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000497
agable@chromium.org5a306a22014-02-24 22:13:59 +0000498@subcommand.usage('[url of repo to check for caching]')
499def CMDexists(parser, args):
500 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000501 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000502 if not len(args) == 1:
503 parser.error('git cache exists only takes exactly one repo url.')
504 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000505 mirror = Mirror(url)
506 if mirror.exists():
507 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000508 return 0
509 return 1
510
511
hinoka@google.com563559c2014-04-02 00:36:24 +0000512@subcommand.usage('[url of repo to create a bootstrap zip file]')
513def CMDupdate_bootstrap(parser, args):
514 """Create and uploads a bootstrap tarball."""
515 # Lets just assert we can't do this on Windows.
516 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000517 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000518 return 1
519
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000520 parser.add_option('--prune', action='store_true',
521 help='Prune all other cached zipballs of the same repo.')
522
hinoka@google.com563559c2014-04-02 00:36:24 +0000523 # First, we need to ensure the cache is populated.
524 populate_args = args[:]
525 populate_args.append('--no_bootstrap')
526 CMDpopulate(parser, populate_args)
527
528 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000529 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000530 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000531 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000532 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000533 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000534
535
agable@chromium.org5a306a22014-02-24 22:13:59 +0000536@subcommand.usage('[url of repo to add to or update in cache]')
537def CMDpopulate(parser, args):
538 """Ensure that the cache has all up-to-date objects for the given repo."""
539 parser.add_option('--depth', type='int',
540 help='Only cache DEPTH commits of history')
541 parser.add_option('--shallow', '-s', action='store_true',
542 help='Only cache 10000 commits of history')
543 parser.add_option('--ref', action='append',
544 help='Specify additional refs to be fetched')
hinoka@google.com563559c2014-04-02 00:36:24 +0000545 parser.add_option('--no_bootstrap', action='store_true',
546 help='Don\'t bootstrap from Google Storage')
szager@chromium.org108eced2014-06-19 21:22:43 +0000547 parser.add_option('--ignore_locks', action='store_true',
548 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000549
agable@chromium.org5a306a22014-02-24 22:13:59 +0000550 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000551 if not len(args) == 1:
552 parser.error('git cache populate only takes exactly one repo url.')
553 url = args[0]
554
szager@chromium.org848fd492014-04-09 19:06:44 +0000555 mirror = Mirror(url, refs=options.ref)
556 kwargs = {
557 'verbose': options.verbose,
558 'shallow': options.shallow,
559 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000560 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000561 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000562 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000563 kwargs['depth'] = options.depth
564 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000565
566
szager@chromium.orgf3145112014-08-07 21:02:36 +0000567@subcommand.usage('Fetch new commits into cache and current checkout')
568def CMDfetch(parser, args):
569 """Update mirror, and fetch in cwd."""
570 parser.add_option('--all', action='store_true', help='Fetch all remotes')
571 options, args = parser.parse_args(args)
572
573 # Figure out which remotes to fetch. This mimics the behavior of regular
574 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
575 # this will NOT try to traverse up the branching structure to find the
576 # ultimate remote to update.
577 remotes = []
578 if options.all:
579 assert not args, 'fatal: fetch --all does not take a repository argument'
580 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
581 elif args:
582 remotes = args
583 else:
584 current_branch = subprocess.check_output(
585 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
586 if current_branch != 'HEAD':
587 upstream = subprocess.check_output(
588 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
589 ).strip()
590 if upstream and upstream != '.':
591 remotes = [upstream]
592 if not remotes:
593 remotes = ['origin']
594
595 cachepath = Mirror.GetCachePath()
596 git_dir = os.path.abspath(subprocess.check_output(
597 [Mirror.git_exe, 'rev-parse', '--git-dir']))
598 git_dir = os.path.abspath(git_dir)
599 if git_dir.startswith(cachepath):
600 mirror = Mirror.FromPath(git_dir)
601 mirror.populate()
602 return 0
603 for remote in remotes:
604 remote_url = subprocess.check_output(
605 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
606 if remote_url.startswith(cachepath):
607 mirror = Mirror.FromPath(remote_url)
608 mirror.print = lambda *args: None
609 print('Updating git cache...')
610 mirror.populate()
611 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
612 return 0
613
614
agable@chromium.org5a306a22014-02-24 22:13:59 +0000615@subcommand.usage('[url of repo to unlock, or -a|--all]')
616def CMDunlock(parser, args):
617 """Unlock one or all repos if their lock files are still around."""
618 parser.add_option('--force', '-f', action='store_true',
619 help='Actually perform the action')
620 parser.add_option('--all', '-a', action='store_true',
621 help='Unlock all repository caches')
622 options, args = parser.parse_args(args)
623 if len(args) > 1 or (len(args) == 0 and not options.all):
624 parser.error('git cache unlock takes exactly one repo url, or --all')
625
agable@chromium.org5a306a22014-02-24 22:13:59 +0000626 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000627 cachepath = Mirror.GetCachePath()
628 lockfiles = [os.path.join(cachepath, path)
629 for path in os.listdir(cachepath)
630 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000631 parser.error('git cache unlock requires -f|--force to do anything. '
632 'Refusing to unlock the following repo caches: '
633 ', '.join(lockfiles))
634
szager@chromium.org848fd492014-04-09 19:06:44 +0000635 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000636 if options.all:
637 unlocked_repos.extend(Mirror.UnlockAll())
638 else:
639 m = Mirror(args[0])
640 if m.unlock():
641 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000642
szager@chromium.org848fd492014-04-09 19:06:44 +0000643 if unlocked_repos:
644 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
645 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000646
647
648class OptionParser(optparse.OptionParser):
649 """Wrapper class for OptionParser to handle global options."""
650
651 def __init__(self, *args, **kwargs):
652 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
653 self.add_option('-c', '--cache-dir',
654 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000655 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000656 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000657 self.add_option('-q', '--quiet', action='store_true',
658 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000659
660 def parse_args(self, args=None, values=None):
661 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000662 if options.quiet:
663 options.verbose = 0
664
665 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
666 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000667
668 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000669 global_cache_dir = Mirror.GetCachePath()
670 except RuntimeError:
671 global_cache_dir = None
672 if options.cache_dir:
673 if global_cache_dir and (
674 os.path.abspath(options.cache_dir) !=
675 os.path.abspath(global_cache_dir)):
676 logging.warn('Overriding globally-configured cache directory.')
677 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000678
agable@chromium.org5a306a22014-02-24 22:13:59 +0000679 return options, args
680
681
682def main(argv):
683 dispatcher = subcommand.CommandDispatcher(__name__)
684 return dispatcher.execute(OptionParser(), argv)
685
686
687if __name__ == '__main__':
688 sys.exit(main(sys.argv[1:]))