blob: bfdef80d797431d12a3c922a0236ff6931e4875c [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
szager@chromium.org1132f5f2014-08-23 01:57:59 +000015import threading
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000016import time
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000017import shutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000018import subprocess
19import sys
20import urlparse
hinoka@google.com776a2c32014-04-25 07:54:25 +000021import zipfile
agable@chromium.org5a306a22014-02-24 22:13:59 +000022
hinoka@google.com563559c2014-04-02 00:36:24 +000023from download_from_google_storage import Gsutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000024import gclient_utils
25import subcommand
26
szager@chromium.org301a7c32014-06-16 17:13:50 +000027# Analogous to gc.autopacklimit git config.
28GC_AUTOPACKLIMIT = 50
29
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000030GIT_CACHE_CORRUPT_MESSAGE = 'WARNING: The Git cache is corrupt.'
31
szager@chromium.org848fd492014-04-09 19:06:44 +000032try:
33 # pylint: disable=E0602
34 WinErr = WindowsError
35except NameError:
36 class WinErr(Exception):
37 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000038
39class LockError(Exception):
40 pass
41
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000042class RefsHeadsFailedToFetch(Exception):
43 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000044
45class Lockfile(object):
46 """Class to represent a cross-platform process-specific lockfile."""
47
48 def __init__(self, path):
49 self.path = os.path.abspath(path)
50 self.lockfile = self.path + ".lock"
51 self.pid = os.getpid()
52
53 def _read_pid(self):
54 """Read the pid stored in the lockfile.
55
56 Note: This method is potentially racy. By the time it returns the lockfile
57 may have been unlocked, removed, or stolen by some other process.
58 """
59 try:
60 with open(self.lockfile, 'r') as f:
61 pid = int(f.readline().strip())
62 except (IOError, ValueError):
63 pid = None
64 return pid
65
66 def _make_lockfile(self):
67 """Safely creates a lockfile containing the current pid."""
68 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
69 fd = os.open(self.lockfile, open_flags, 0o644)
70 f = os.fdopen(fd, 'w')
szager@chromium.org848fd492014-04-09 19:06:44 +000071 print(self.pid, file=f)
agable@chromium.org5a306a22014-02-24 22:13:59 +000072 f.close()
73
74 def _remove_lockfile(self):
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000075 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
76
77 See gclient_utils.py:rmtree docstring for more explanation on the
78 windows case.
79 """
80 if sys.platform == 'win32':
81 lockfile = os.path.normcase(self.lockfile)
82 for _ in xrange(3):
83 exitcode = subprocess.call(['cmd.exe', '/c',
84 'del', '/f', '/q', lockfile])
85 if exitcode == 0:
86 return
87 time.sleep(3)
88 raise LockError('Failed to remove lock: %s' % lockfile)
89 else:
90 os.remove(self.lockfile)
agable@chromium.org5a306a22014-02-24 22:13:59 +000091
92 def lock(self):
93 """Acquire the lock.
94
95 Note: This is a NON-BLOCKING FAIL-FAST operation.
96 Do. Or do not. There is no try.
97 """
98 try:
99 self._make_lockfile()
100 except OSError as e:
101 if e.errno == errno.EEXIST:
102 raise LockError("%s is already locked" % self.path)
103 else:
104 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
105
106 def unlock(self):
107 """Release the lock."""
szager@chromium.org108eced2014-06-19 21:22:43 +0000108 try:
109 if not self.is_locked():
110 raise LockError("%s is not locked" % self.path)
111 if not self.i_am_locking():
112 raise LockError("%s is locked, but not by me" % self.path)
113 self._remove_lockfile()
114 except WinErr:
115 # Windows is unreliable when it comes to file locking. YMMV.
116 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +0000117
118 def break_lock(self):
119 """Remove the lock, even if it was created by someone else."""
120 try:
121 self._remove_lockfile()
122 return True
123 except OSError as exc:
124 if exc.errno == errno.ENOENT:
125 return False
126 else:
127 raise
128
129 def is_locked(self):
130 """Test if the file is locked by anyone.
131
132 Note: This method is potentially racy. By the time it returns the lockfile
133 may have been unlocked, removed, or stolen by some other process.
134 """
135 return os.path.exists(self.lockfile)
136
137 def i_am_locking(self):
138 """Test if the file is locked by this process."""
139 return self.is_locked() and self.pid == self._read_pid()
140
agable@chromium.org5a306a22014-02-24 22:13:59 +0000141
szager@chromium.org848fd492014-04-09 19:06:44 +0000142class Mirror(object):
143
144 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
145 gsutil_exe = os.path.join(
146 os.path.dirname(os.path.abspath(__file__)),
147 'third_party', 'gsutil', 'gsutil')
szager@chromium.org1132f5f2014-08-23 01:57:59 +0000148 cachepath_lock = threading.Lock()
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):
szager@chromium.org84c56002014-08-23 03:33:28 +0000203 with cls.cachepath_lock:
204 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000205
206 @classmethod
207 def GetCachePath(cls):
szager@chromium.org84c56002014-08-23 03:33:28 +0000208 with cls.cachepath_lock:
209 if not hasattr(cls, 'cachepath'):
210 try:
211 cachepath = subprocess.check_output(
212 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
213 except subprocess.CalledProcessError:
214 cachepath = None
215 if not cachepath:
216 raise RuntimeError(
217 'No global cache.cachepath git configuration found.')
218 setattr(cls, 'cachepath', cachepath)
219 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000220
221 def RunGit(self, cmd, **kwargs):
222 """Run git in a subprocess."""
223 cwd = kwargs.setdefault('cwd', self.mirror_path)
224 kwargs.setdefault('print_stdout', False)
225 kwargs.setdefault('filter_fn', self.print)
226 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
227 env.setdefault('GIT_ASKPASS', 'true')
228 env.setdefault('SSH_ASKPASS', 'true')
229 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
230 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
231
232 def config(self, cwd=None):
233 if cwd is None:
234 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000235
236 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
237 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
238
239 # Don't combine pack files into one big pack file. It's really slow for
240 # repositories, and there's no way to track progress and make sure it's
241 # not stuck.
242 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
243
244 # Allocate more RAM for cache-ing delta chains, for better performance
245 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000246 self.RunGit(['config', 'core.deltaBaseCacheLimit',
247 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000248
szager@chromium.org848fd492014-04-09 19:06:44 +0000249 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
250 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
szager@chromium.org965c44f2014-08-19 21:19:19 +0000251 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000252 for ref in self.refs:
253 ref = ref.lstrip('+').rstrip('/')
254 if ref.startswith('refs/'):
255 refspec = '+%s:%s' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000256 regex = r'\+%s:.*' % ref.replace('*', r'\*')
szager@chromium.org848fd492014-04-09 19:06:44 +0000257 else:
258 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000259 regex = r'\+refs/heads/%s:.*' % ref.replace('*', r'\*')
260 self.RunGit(
261 ['config', '--replace-all', 'remote.origin.fetch', refspec, regex],
262 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000263
264 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000265 """Bootstrap the repo from Google Stroage if possible.
266
267 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
268 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000269
hinoka@google.com776a2c32014-04-25 07:54:25 +0000270 python_fallback = False
271 if sys.platform.startswith('win') and not self.FindExecutable('7z'):
272 python_fallback = True
273 elif sys.platform.startswith('darwin'):
274 # The OSX version of unzip doesn't support zip64.
275 python_fallback = True
276 elif not self.FindExecutable('unzip'):
277 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000278
279 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.orgc2bc22d2014-06-05 21:19:38 +0000280 gsutil = Gsutil(self.gsutil_exe, boto_path=None, bypass_prodaccess=True)
szager@chromium.org848fd492014-04-09 19:06:44 +0000281 # Get the most recent version of the zipfile.
282 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
283 ls_out_sorted = sorted(ls_out.splitlines())
284 if not ls_out_sorted:
285 # This repo is not on Google Storage.
286 return False
287 latest_checkout = ls_out_sorted[-1]
288
289 # Download zip file to a temporary directory.
290 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000291 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000292 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000293 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000294 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000295 return False
296 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
297
hinoka@google.com776a2c32014-04-25 07:54:25 +0000298 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
299 if not python_fallback:
300 if sys.platform.startswith('win'):
301 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
302 else:
303 cmd = ['unzip', filename, '-d', directory]
304 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000305 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000306 try:
307 with zipfile.ZipFile(filename, 'r') as f:
308 f.printdir()
309 f.extractall(directory)
310 except Exception as e:
311 self.print('Encountered error: %s' % str(e), file=sys.stderr)
312 retcode = 1
313 else:
314 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000315 finally:
316 # Clean up the downloaded zipfile.
317 gclient_utils.rmtree(tempdir)
318
319 if retcode:
320 self.print(
321 'Extracting bootstrap zipfile %s failed.\n'
322 'Resuming normal operations.' % filename)
323 return False
324 return True
325
326 def exists(self):
327 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
328
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000329 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
330 tempdir = None
331 config_file = os.path.join(self.mirror_path, 'config')
332 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
333 pack_files = []
334
335 if os.path.isdir(pack_dir):
336 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
337
338 should_bootstrap = (force or
339 not os.path.exists(config_file) or
340 len(pack_files) > GC_AUTOPACKLIMIT)
341 if should_bootstrap:
342 tempdir = tempfile.mkdtemp(
343 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
344 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
345 if bootstrapped:
346 # Bootstrap succeeded; delete previous cache, if any.
347 try:
348 # Try to move folder to tempdir if possible.
349 defunct_dir = tempfile.mkdtemp()
350 shutil.move(self.mirror_path, defunct_dir)
351 self.print('Moved defunct directory for repository %s from %s to %s'
352 % (self.url, self.mirror_path, defunct_dir))
353 except Exception:
354 gclient_utils.rmtree(self.mirror_path)
355 elif not os.path.exists(config_file):
356 # Bootstrap failed, no previous cache; start with a bare git dir.
357 self.RunGit(['init', '--bare'], cwd=tempdir)
358 else:
359 # Bootstrap failed, previous cache exists; warn and continue.
360 logging.warn(
361 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
362 'but failed. Continuing with non-optimized repository.'
363 % len(pack_files))
364 gclient_utils.rmtree(tempdir)
365 tempdir = None
366 else:
367 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
368 logging.warn(
369 'Shallow fetch requested, but repo cache already exists.')
370 return tempdir
371
372 def _fetch(self, rundir, verbose, depth):
373 self.config(rundir)
374 v = []
375 d = []
376 if verbose:
377 v = ['-v', '--progress']
378 if depth:
379 d = ['--depth', str(depth)]
380 fetch_cmd = ['fetch'] + v + d + ['origin']
381 fetch_specs = subprocess.check_output(
382 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
383 cwd=rundir).strip().splitlines()
384 for spec in fetch_specs:
385 try:
386 self.print('Fetching %s' % spec)
387 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
388 except subprocess.CalledProcessError:
389 if spec == '+refs/heads/*:refs/heads/*':
390 raise RefsHeadsFailedToFetch
391 logging.warn('Fetch of %s failed' % spec)
392
szager@chromium.org848fd492014-04-09 19:06:44 +0000393 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.org108eced2014-06-19 21:22:43 +0000394 verbose=False, ignore_lock=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000395 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000396 if shallow and not depth:
397 depth = 10000
398 gclient_utils.safe_makedirs(self.GetCachePath())
399
szager@chromium.org108eced2014-06-19 21:22:43 +0000400 lockfile = Lockfile(self.mirror_path)
401 if not ignore_lock:
402 lockfile.lock()
403
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000404 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000405 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000406 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000407 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000408 self._fetch(rundir, verbose, depth)
409 except RefsHeadsFailedToFetch:
410 # This is a major failure, we need to clean and force a bootstrap.
411 gclient_utils.rmtree(rundir)
412 self.print(GIT_CACHE_CORRUPT_MESSAGE)
413 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
414 assert tempdir
415 self._fetch(tempdir or self.mirror_path, verbose, depth)
416 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000417 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000418 try:
419 os.rename(tempdir, self.mirror_path)
420 except OSError as e:
421 # This is somehow racy on Windows.
422 # Catching OSError because WindowsError isn't portable and
423 # pylint complains.
424 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
425 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000426 if not ignore_lock:
427 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000428
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000429 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000430 # The files are named <git number>.zip
431 gen_number = subprocess.check_output(
432 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
433 self.RunGit(['gc']) # Run Garbage Collect to compress packfile.
434 # Creating a temp file and then deleting it ensures we can use this name.
435 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
436 os.remove(tmp_zipfile)
437 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
438 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000439 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
440 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000441 gsutil.call('cp', tmp_zipfile, dest_name)
442 os.remove(tmp_zipfile)
443
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000444 # Remove all other files in the same directory.
445 if prune:
446 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
447 for filename in ls_out.splitlines():
448 if filename == dest_name:
449 continue
450 gsutil.call('rm', filename)
451
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000452 @staticmethod
453 def DeleteTmpPackFiles(path):
454 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000455 if not os.path.isdir(pack_dir):
456 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000457 pack_files = [f for f in os.listdir(pack_dir) if
458 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
459 for f in pack_files:
460 f = os.path.join(pack_dir, f)
461 try:
462 os.remove(f)
463 logging.warn('Deleted stale temporary pack file %s' % f)
464 except OSError:
465 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000466
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000467 @classmethod
468 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000469 did_unlock = False
470 lf = Lockfile(path)
471 if lf.break_lock():
472 did_unlock = True
473 # Look for lock files that might have been left behind by an interrupted
474 # git process.
475 lf = os.path.join(path, 'config.lock')
476 if os.path.exists(lf):
477 os.remove(lf)
478 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000479 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000480 return did_unlock
481
szager@chromium.org848fd492014-04-09 19:06:44 +0000482 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000483 return self.BreakLocks(self.mirror_path)
484
485 @classmethod
486 def UnlockAll(cls):
487 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000488 if not cachepath:
489 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000490 dirlist = os.listdir(cachepath)
491 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
492 if os.path.isdir(os.path.join(cachepath, path))])
493 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000494 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
495 gclient_utils.rmtree(os.path.join(cachepath, dirent))
496 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000497 os.path.isfile(os.path.join(cachepath, dirent))):
498 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
499
500 unlocked_repos = []
501 for repo_dir in repo_dirs:
502 if cls.BreakLocks(repo_dir):
503 unlocked_repos.append(repo_dir)
504
505 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000506
agable@chromium.org5a306a22014-02-24 22:13:59 +0000507@subcommand.usage('[url of repo to check for caching]')
508def CMDexists(parser, args):
509 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000510 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000511 if not len(args) == 1:
512 parser.error('git cache exists only takes exactly one repo url.')
513 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000514 mirror = Mirror(url)
515 if mirror.exists():
516 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000517 return 0
518 return 1
519
520
hinoka@google.com563559c2014-04-02 00:36:24 +0000521@subcommand.usage('[url of repo to create a bootstrap zip file]')
522def CMDupdate_bootstrap(parser, args):
523 """Create and uploads a bootstrap tarball."""
524 # Lets just assert we can't do this on Windows.
525 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000526 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000527 return 1
528
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000529 parser.add_option('--prune', action='store_true',
530 help='Prune all other cached zipballs of the same repo.')
531
hinoka@google.com563559c2014-04-02 00:36:24 +0000532 # First, we need to ensure the cache is populated.
533 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000534 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000535 CMDpopulate(parser, populate_args)
536
537 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000538 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000539 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000540 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000541 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000542 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000543
544
agable@chromium.org5a306a22014-02-24 22:13:59 +0000545@subcommand.usage('[url of repo to add to or update in cache]')
546def CMDpopulate(parser, args):
547 """Ensure that the cache has all up-to-date objects for the given repo."""
548 parser.add_option('--depth', type='int',
549 help='Only cache DEPTH commits of history')
550 parser.add_option('--shallow', '-s', action='store_true',
551 help='Only cache 10000 commits of history')
552 parser.add_option('--ref', action='append',
553 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000554 parser.add_option('--no_bootstrap', '--no-bootstrap',
555 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000556 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000557 parser.add_option('--ignore_locks', '--ignore-locks',
558 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000559 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000560
agable@chromium.org5a306a22014-02-24 22:13:59 +0000561 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000562 if not len(args) == 1:
563 parser.error('git cache populate only takes exactly one repo url.')
564 url = args[0]
565
szager@chromium.org848fd492014-04-09 19:06:44 +0000566 mirror = Mirror(url, refs=options.ref)
567 kwargs = {
568 'verbose': options.verbose,
569 'shallow': options.shallow,
570 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000571 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000572 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000573 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000574 kwargs['depth'] = options.depth
575 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000576
577
szager@chromium.orgf3145112014-08-07 21:02:36 +0000578@subcommand.usage('Fetch new commits into cache and current checkout')
579def CMDfetch(parser, args):
580 """Update mirror, and fetch in cwd."""
581 parser.add_option('--all', action='store_true', help='Fetch all remotes')
582 options, args = parser.parse_args(args)
583
584 # Figure out which remotes to fetch. This mimics the behavior of regular
585 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
586 # this will NOT try to traverse up the branching structure to find the
587 # ultimate remote to update.
588 remotes = []
589 if options.all:
590 assert not args, 'fatal: fetch --all does not take a repository argument'
591 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
592 elif args:
593 remotes = args
594 else:
595 current_branch = subprocess.check_output(
596 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
597 if current_branch != 'HEAD':
598 upstream = subprocess.check_output(
599 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
600 ).strip()
601 if upstream and upstream != '.':
602 remotes = [upstream]
603 if not remotes:
604 remotes = ['origin']
605
606 cachepath = Mirror.GetCachePath()
607 git_dir = os.path.abspath(subprocess.check_output(
608 [Mirror.git_exe, 'rev-parse', '--git-dir']))
609 git_dir = os.path.abspath(git_dir)
610 if git_dir.startswith(cachepath):
611 mirror = Mirror.FromPath(git_dir)
612 mirror.populate()
613 return 0
614 for remote in remotes:
615 remote_url = subprocess.check_output(
616 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
617 if remote_url.startswith(cachepath):
618 mirror = Mirror.FromPath(remote_url)
619 mirror.print = lambda *args: None
620 print('Updating git cache...')
621 mirror.populate()
622 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
623 return 0
624
625
agable@chromium.org5a306a22014-02-24 22:13:59 +0000626@subcommand.usage('[url of repo to unlock, or -a|--all]')
627def CMDunlock(parser, args):
628 """Unlock one or all repos if their lock files are still around."""
629 parser.add_option('--force', '-f', action='store_true',
630 help='Actually perform the action')
631 parser.add_option('--all', '-a', action='store_true',
632 help='Unlock all repository caches')
633 options, args = parser.parse_args(args)
634 if len(args) > 1 or (len(args) == 0 and not options.all):
635 parser.error('git cache unlock takes exactly one repo url, or --all')
636
agable@chromium.org5a306a22014-02-24 22:13:59 +0000637 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000638 cachepath = Mirror.GetCachePath()
639 lockfiles = [os.path.join(cachepath, path)
640 for path in os.listdir(cachepath)
641 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000642 parser.error('git cache unlock requires -f|--force to do anything. '
643 'Refusing to unlock the following repo caches: '
644 ', '.join(lockfiles))
645
szager@chromium.org848fd492014-04-09 19:06:44 +0000646 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000647 if options.all:
648 unlocked_repos.extend(Mirror.UnlockAll())
649 else:
650 m = Mirror(args[0])
651 if m.unlock():
652 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000653
szager@chromium.org848fd492014-04-09 19:06:44 +0000654 if unlocked_repos:
655 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
656 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000657
658
659class OptionParser(optparse.OptionParser):
660 """Wrapper class for OptionParser to handle global options."""
661
662 def __init__(self, *args, **kwargs):
663 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
664 self.add_option('-c', '--cache-dir',
665 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000666 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000667 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000668 self.add_option('-q', '--quiet', action='store_true',
669 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000670
671 def parse_args(self, args=None, values=None):
672 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000673 if options.quiet:
674 options.verbose = 0
675
676 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
677 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000678
679 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000680 global_cache_dir = Mirror.GetCachePath()
681 except RuntimeError:
682 global_cache_dir = None
683 if options.cache_dir:
684 if global_cache_dir and (
685 os.path.abspath(options.cache_dir) !=
686 os.path.abspath(global_cache_dir)):
687 logging.warn('Overriding globally-configured cache directory.')
688 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000689
agable@chromium.org5a306a22014-02-24 22:13:59 +0000690 return options, args
691
692
693def main(argv):
694 dispatcher = subcommand.CommandDispatcher(__name__)
695 return dispatcher.execute(OptionParser(), argv)
696
697
698if __name__ == '__main__':
699 sys.exit(main(sys.argv[1:]))