blob: 21252535f491146eea20efa32179144b8d469827 [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:
hinoka@chromium.orga5cda1e2014-08-24 12:09:13 +0000419 if os.path.exists(self.mirror_path):
420 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000421 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:]))