blob: d6ed2a82d313980da46e1f0d5bd936fce043c0f5 [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.orgc5325042014-08-23 02:35:23 +0000203 cls.cachepath_lock.acquire()
szager@chromium.org848fd492014-04-09 19:06:44 +0000204 setattr(cls, 'cachepath', cachepath)
szager@chromium.orgc5325042014-08-23 02:35:23 +0000205 cls.cachepath_lock.release()
szager@chromium.org848fd492014-04-09 19:06:44 +0000206
207 @classmethod
208 def GetCachePath(cls):
szager@chromium.org1132f5f2014-08-23 01:57:59 +0000209 cls.cachepath_lock.acquire()
szager@chromium.org848fd492014-04-09 19:06:44 +0000210 if not hasattr(cls, 'cachepath'):
211 try:
212 cachepath = subprocess.check_output(
213 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
214 except subprocess.CalledProcessError:
215 cachepath = None
216 if not cachepath:
szager@chromium.org1132f5f2014-08-23 01:57:59 +0000217 cls.cachepath_lock.release()
szager@chromium.org848fd492014-04-09 19:06:44 +0000218 raise RuntimeError('No global cache.cachepath git configuration found.')
219 setattr(cls, 'cachepath', cachepath)
szager@chromium.orgc5325042014-08-23 02:35:23 +0000220 cls.cachepath_lock.release()
szager@chromium.org848fd492014-04-09 19:06:44 +0000221 return getattr(cls, 'cachepath')
222
223 def RunGit(self, cmd, **kwargs):
224 """Run git in a subprocess."""
225 cwd = kwargs.setdefault('cwd', self.mirror_path)
226 kwargs.setdefault('print_stdout', False)
227 kwargs.setdefault('filter_fn', self.print)
228 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
229 env.setdefault('GIT_ASKPASS', 'true')
230 env.setdefault('SSH_ASKPASS', 'true')
231 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
232 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
233
234 def config(self, cwd=None):
235 if cwd is None:
236 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000237
238 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
239 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
240
241 # Don't combine pack files into one big pack file. It's really slow for
242 # repositories, and there's no way to track progress and make sure it's
243 # not stuck.
244 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
245
246 # Allocate more RAM for cache-ing delta chains, for better performance
247 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000248 self.RunGit(['config', 'core.deltaBaseCacheLimit',
249 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000250
szager@chromium.org848fd492014-04-09 19:06:44 +0000251 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
252 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
szager@chromium.org965c44f2014-08-19 21:19:19 +0000253 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000254 for ref in self.refs:
255 ref = ref.lstrip('+').rstrip('/')
256 if ref.startswith('refs/'):
257 refspec = '+%s:%s' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000258 regex = r'\+%s:.*' % ref.replace('*', r'\*')
szager@chromium.org848fd492014-04-09 19:06:44 +0000259 else:
260 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000261 regex = r'\+refs/heads/%s:.*' % ref.replace('*', r'\*')
262 self.RunGit(
263 ['config', '--replace-all', 'remote.origin.fetch', refspec, regex],
264 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000265
266 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000267 """Bootstrap the repo from Google Stroage if possible.
268
269 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
270 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000271
hinoka@google.com776a2c32014-04-25 07:54:25 +0000272 python_fallback = False
273 if sys.platform.startswith('win') and not self.FindExecutable('7z'):
274 python_fallback = True
275 elif sys.platform.startswith('darwin'):
276 # The OSX version of unzip doesn't support zip64.
277 python_fallback = True
278 elif not self.FindExecutable('unzip'):
279 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000280
281 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.orgc2bc22d2014-06-05 21:19:38 +0000282 gsutil = Gsutil(self.gsutil_exe, boto_path=None, bypass_prodaccess=True)
szager@chromium.org848fd492014-04-09 19:06:44 +0000283 # Get the most recent version of the zipfile.
284 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
285 ls_out_sorted = sorted(ls_out.splitlines())
286 if not ls_out_sorted:
287 # This repo is not on Google Storage.
288 return False
289 latest_checkout = ls_out_sorted[-1]
290
291 # Download zip file to a temporary directory.
292 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000293 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000294 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000295 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000296 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000297 return False
298 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
299
hinoka@google.com776a2c32014-04-25 07:54:25 +0000300 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
301 if not python_fallback:
302 if sys.platform.startswith('win'):
303 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
304 else:
305 cmd = ['unzip', filename, '-d', directory]
306 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000307 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000308 try:
309 with zipfile.ZipFile(filename, 'r') as f:
310 f.printdir()
311 f.extractall(directory)
312 except Exception as e:
313 self.print('Encountered error: %s' % str(e), file=sys.stderr)
314 retcode = 1
315 else:
316 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000317 finally:
318 # Clean up the downloaded zipfile.
319 gclient_utils.rmtree(tempdir)
320
321 if retcode:
322 self.print(
323 'Extracting bootstrap zipfile %s failed.\n'
324 'Resuming normal operations.' % filename)
325 return False
326 return True
327
328 def exists(self):
329 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
330
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000331 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
332 tempdir = None
333 config_file = os.path.join(self.mirror_path, 'config')
334 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
335 pack_files = []
336
337 if os.path.isdir(pack_dir):
338 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
339
340 should_bootstrap = (force or
341 not os.path.exists(config_file) or
342 len(pack_files) > GC_AUTOPACKLIMIT)
343 if should_bootstrap:
344 tempdir = tempfile.mkdtemp(
345 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
346 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
347 if bootstrapped:
348 # Bootstrap succeeded; delete previous cache, if any.
349 try:
350 # Try to move folder to tempdir if possible.
351 defunct_dir = tempfile.mkdtemp()
352 shutil.move(self.mirror_path, defunct_dir)
353 self.print('Moved defunct directory for repository %s from %s to %s'
354 % (self.url, self.mirror_path, defunct_dir))
355 except Exception:
356 gclient_utils.rmtree(self.mirror_path)
357 elif not os.path.exists(config_file):
358 # Bootstrap failed, no previous cache; start with a bare git dir.
359 self.RunGit(['init', '--bare'], cwd=tempdir)
360 else:
361 # Bootstrap failed, previous cache exists; warn and continue.
362 logging.warn(
363 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
364 'but failed. Continuing with non-optimized repository.'
365 % len(pack_files))
366 gclient_utils.rmtree(tempdir)
367 tempdir = None
368 else:
369 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
370 logging.warn(
371 'Shallow fetch requested, but repo cache already exists.')
372 return tempdir
373
374 def _fetch(self, rundir, verbose, depth):
375 self.config(rundir)
376 v = []
377 d = []
378 if verbose:
379 v = ['-v', '--progress']
380 if depth:
381 d = ['--depth', str(depth)]
382 fetch_cmd = ['fetch'] + v + d + ['origin']
383 fetch_specs = subprocess.check_output(
384 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
385 cwd=rundir).strip().splitlines()
386 for spec in fetch_specs:
387 try:
388 self.print('Fetching %s' % spec)
389 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
390 except subprocess.CalledProcessError:
391 if spec == '+refs/heads/*:refs/heads/*':
392 raise RefsHeadsFailedToFetch
393 logging.warn('Fetch of %s failed' % spec)
394
szager@chromium.org848fd492014-04-09 19:06:44 +0000395 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.org108eced2014-06-19 21:22:43 +0000396 verbose=False, ignore_lock=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000397 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000398 if shallow and not depth:
399 depth = 10000
400 gclient_utils.safe_makedirs(self.GetCachePath())
401
szager@chromium.org108eced2014-06-19 21:22:43 +0000402 lockfile = Lockfile(self.mirror_path)
403 if not ignore_lock:
404 lockfile.lock()
405
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000406 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000407 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000408 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000409 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000410 self._fetch(rundir, verbose, depth)
411 except RefsHeadsFailedToFetch:
412 # This is a major failure, we need to clean and force a bootstrap.
413 gclient_utils.rmtree(rundir)
414 self.print(GIT_CACHE_CORRUPT_MESSAGE)
415 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
416 assert tempdir
417 self._fetch(tempdir or self.mirror_path, verbose, depth)
418 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000419 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000420 try:
421 os.rename(tempdir, self.mirror_path)
422 except OSError as e:
423 # This is somehow racy on Windows.
424 # Catching OSError because WindowsError isn't portable and
425 # pylint complains.
426 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
427 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000428 if not ignore_lock:
429 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000430
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000431 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000432 # The files are named <git number>.zip
433 gen_number = subprocess.check_output(
434 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
435 self.RunGit(['gc']) # Run Garbage Collect to compress packfile.
436 # Creating a temp file and then deleting it ensures we can use this name.
437 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
438 os.remove(tmp_zipfile)
439 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
440 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000441 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
442 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000443 gsutil.call('cp', tmp_zipfile, dest_name)
444 os.remove(tmp_zipfile)
445
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000446 # Remove all other files in the same directory.
447 if prune:
448 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
449 for filename in ls_out.splitlines():
450 if filename == dest_name:
451 continue
452 gsutil.call('rm', filename)
453
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000454 @staticmethod
455 def DeleteTmpPackFiles(path):
456 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000457 if not os.path.isdir(pack_dir):
458 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000459 pack_files = [f for f in os.listdir(pack_dir) if
460 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
461 for f in pack_files:
462 f = os.path.join(pack_dir, f)
463 try:
464 os.remove(f)
465 logging.warn('Deleted stale temporary pack file %s' % f)
466 except OSError:
467 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000468
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000469 @classmethod
470 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000471 did_unlock = False
472 lf = Lockfile(path)
473 if lf.break_lock():
474 did_unlock = True
475 # Look for lock files that might have been left behind by an interrupted
476 # git process.
477 lf = os.path.join(path, 'config.lock')
478 if os.path.exists(lf):
479 os.remove(lf)
480 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000481 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000482 return did_unlock
483
szager@chromium.org848fd492014-04-09 19:06:44 +0000484 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000485 return self.BreakLocks(self.mirror_path)
486
487 @classmethod
488 def UnlockAll(cls):
489 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000490 if not cachepath:
491 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000492 dirlist = os.listdir(cachepath)
493 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
494 if os.path.isdir(os.path.join(cachepath, path))])
495 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000496 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
497 gclient_utils.rmtree(os.path.join(cachepath, dirent))
498 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000499 os.path.isfile(os.path.join(cachepath, dirent))):
500 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
501
502 unlocked_repos = []
503 for repo_dir in repo_dirs:
504 if cls.BreakLocks(repo_dir):
505 unlocked_repos.append(repo_dir)
506
507 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000508
agable@chromium.org5a306a22014-02-24 22:13:59 +0000509@subcommand.usage('[url of repo to check for caching]')
510def CMDexists(parser, args):
511 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000512 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000513 if not len(args) == 1:
514 parser.error('git cache exists only takes exactly one repo url.')
515 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000516 mirror = Mirror(url)
517 if mirror.exists():
518 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000519 return 0
520 return 1
521
522
hinoka@google.com563559c2014-04-02 00:36:24 +0000523@subcommand.usage('[url of repo to create a bootstrap zip file]')
524def CMDupdate_bootstrap(parser, args):
525 """Create and uploads a bootstrap tarball."""
526 # Lets just assert we can't do this on Windows.
527 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000528 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000529 return 1
530
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000531 parser.add_option('--prune', action='store_true',
532 help='Prune all other cached zipballs of the same repo.')
533
hinoka@google.com563559c2014-04-02 00:36:24 +0000534 # First, we need to ensure the cache is populated.
535 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000536 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000537 CMDpopulate(parser, populate_args)
538
539 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000540 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000541 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000542 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000543 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000544 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000545
546
agable@chromium.org5a306a22014-02-24 22:13:59 +0000547@subcommand.usage('[url of repo to add to or update in cache]')
548def CMDpopulate(parser, args):
549 """Ensure that the cache has all up-to-date objects for the given repo."""
550 parser.add_option('--depth', type='int',
551 help='Only cache DEPTH commits of history')
552 parser.add_option('--shallow', '-s', action='store_true',
553 help='Only cache 10000 commits of history')
554 parser.add_option('--ref', action='append',
555 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000556 parser.add_option('--no_bootstrap', '--no-bootstrap',
557 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000558 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000559 parser.add_option('--ignore_locks', '--ignore-locks',
560 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000561 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000562
agable@chromium.org5a306a22014-02-24 22:13:59 +0000563 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000564 if not len(args) == 1:
565 parser.error('git cache populate only takes exactly one repo url.')
566 url = args[0]
567
szager@chromium.org848fd492014-04-09 19:06:44 +0000568 mirror = Mirror(url, refs=options.ref)
569 kwargs = {
570 'verbose': options.verbose,
571 'shallow': options.shallow,
572 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000573 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000574 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000575 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000576 kwargs['depth'] = options.depth
577 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000578
579
szager@chromium.orgf3145112014-08-07 21:02:36 +0000580@subcommand.usage('Fetch new commits into cache and current checkout')
581def CMDfetch(parser, args):
582 """Update mirror, and fetch in cwd."""
583 parser.add_option('--all', action='store_true', help='Fetch all remotes')
584 options, args = parser.parse_args(args)
585
586 # Figure out which remotes to fetch. This mimics the behavior of regular
587 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
588 # this will NOT try to traverse up the branching structure to find the
589 # ultimate remote to update.
590 remotes = []
591 if options.all:
592 assert not args, 'fatal: fetch --all does not take a repository argument'
593 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
594 elif args:
595 remotes = args
596 else:
597 current_branch = subprocess.check_output(
598 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
599 if current_branch != 'HEAD':
600 upstream = subprocess.check_output(
601 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
602 ).strip()
603 if upstream and upstream != '.':
604 remotes = [upstream]
605 if not remotes:
606 remotes = ['origin']
607
608 cachepath = Mirror.GetCachePath()
609 git_dir = os.path.abspath(subprocess.check_output(
610 [Mirror.git_exe, 'rev-parse', '--git-dir']))
611 git_dir = os.path.abspath(git_dir)
612 if git_dir.startswith(cachepath):
613 mirror = Mirror.FromPath(git_dir)
614 mirror.populate()
615 return 0
616 for remote in remotes:
617 remote_url = subprocess.check_output(
618 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
619 if remote_url.startswith(cachepath):
620 mirror = Mirror.FromPath(remote_url)
621 mirror.print = lambda *args: None
622 print('Updating git cache...')
623 mirror.populate()
624 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
625 return 0
626
627
agable@chromium.org5a306a22014-02-24 22:13:59 +0000628@subcommand.usage('[url of repo to unlock, or -a|--all]')
629def CMDunlock(parser, args):
630 """Unlock one or all repos if their lock files are still around."""
631 parser.add_option('--force', '-f', action='store_true',
632 help='Actually perform the action')
633 parser.add_option('--all', '-a', action='store_true',
634 help='Unlock all repository caches')
635 options, args = parser.parse_args(args)
636 if len(args) > 1 or (len(args) == 0 and not options.all):
637 parser.error('git cache unlock takes exactly one repo url, or --all')
638
agable@chromium.org5a306a22014-02-24 22:13:59 +0000639 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000640 cachepath = Mirror.GetCachePath()
641 lockfiles = [os.path.join(cachepath, path)
642 for path in os.listdir(cachepath)
643 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000644 parser.error('git cache unlock requires -f|--force to do anything. '
645 'Refusing to unlock the following repo caches: '
646 ', '.join(lockfiles))
647
szager@chromium.org848fd492014-04-09 19:06:44 +0000648 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000649 if options.all:
650 unlocked_repos.extend(Mirror.UnlockAll())
651 else:
652 m = Mirror(args[0])
653 if m.unlock():
654 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000655
szager@chromium.org848fd492014-04-09 19:06:44 +0000656 if unlocked_repos:
657 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
658 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000659
660
661class OptionParser(optparse.OptionParser):
662 """Wrapper class for OptionParser to handle global options."""
663
664 def __init__(self, *args, **kwargs):
665 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
666 self.add_option('-c', '--cache-dir',
667 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000668 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000669 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000670 self.add_option('-q', '--quiet', action='store_true',
671 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000672
673 def parse_args(self, args=None, values=None):
674 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000675 if options.quiet:
676 options.verbose = 0
677
678 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
679 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000680
681 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000682 global_cache_dir = Mirror.GetCachePath()
683 except RuntimeError:
684 global_cache_dir = None
685 if options.cache_dir:
686 if global_cache_dir and (
687 os.path.abspath(options.cache_dir) !=
688 os.path.abspath(global_cache_dir)):
689 logging.warn('Overriding globally-configured cache directory.')
690 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000691
agable@chromium.org5a306a22014-02-24 22:13:59 +0000692 return options, args
693
694
695def main(argv):
696 dispatcher = subcommand.CommandDispatcher(__name__)
697 return dispatcher.execute(OptionParser(), argv)
698
699
700if __name__ == '__main__':
701 sys.exit(main(sys.argv[1:]))