blob: 5d1af238ede978de79aeef8a8df52999a46e9e05 [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
agable@chromium.org5a306a22014-02-24 22:13:59 +000017import subprocess
18import sys
19import urlparse
hinoka@google.com776a2c32014-04-25 07:54:25 +000020import zipfile
agable@chromium.org5a306a22014-02-24 22:13:59 +000021
hinoka@google.com563559c2014-04-02 00:36:24 +000022from download_from_google_storage import Gsutil
agable@chromium.org5a306a22014-02-24 22:13:59 +000023import gclient_utils
24import subcommand
25
szager@chromium.org301a7c32014-06-16 17:13:50 +000026# Analogous to gc.autopacklimit git config.
27GC_AUTOPACKLIMIT = 50
28
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000029GIT_CACHE_CORRUPT_MESSAGE = 'WARNING: The Git cache is corrupt.'
30
szager@chromium.org848fd492014-04-09 19:06:44 +000031try:
32 # pylint: disable=E0602
33 WinErr = WindowsError
34except NameError:
35 class WinErr(Exception):
36 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000037
38class LockError(Exception):
39 pass
40
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000041class RefsHeadsFailedToFetch(Exception):
42 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +000043
44class Lockfile(object):
45 """Class to represent a cross-platform process-specific lockfile."""
46
47 def __init__(self, path):
48 self.path = os.path.abspath(path)
49 self.lockfile = self.path + ".lock"
50 self.pid = os.getpid()
51
52 def _read_pid(self):
53 """Read the pid stored in the lockfile.
54
55 Note: This method is potentially racy. By the time it returns the lockfile
56 may have been unlocked, removed, or stolen by some other process.
57 """
58 try:
59 with open(self.lockfile, 'r') as f:
60 pid = int(f.readline().strip())
61 except (IOError, ValueError):
62 pid = None
63 return pid
64
65 def _make_lockfile(self):
66 """Safely creates a lockfile containing the current pid."""
67 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
68 fd = os.open(self.lockfile, open_flags, 0o644)
69 f = os.fdopen(fd, 'w')
szager@chromium.org848fd492014-04-09 19:06:44 +000070 print(self.pid, file=f)
agable@chromium.org5a306a22014-02-24 22:13:59 +000071 f.close()
72
73 def _remove_lockfile(self):
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000074 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
75
76 See gclient_utils.py:rmtree docstring for more explanation on the
77 windows case.
78 """
79 if sys.platform == 'win32':
80 lockfile = os.path.normcase(self.lockfile)
81 for _ in xrange(3):
82 exitcode = subprocess.call(['cmd.exe', '/c',
83 'del', '/f', '/q', lockfile])
84 if exitcode == 0:
85 return
86 time.sleep(3)
87 raise LockError('Failed to remove lock: %s' % lockfile)
88 else:
89 os.remove(self.lockfile)
agable@chromium.org5a306a22014-02-24 22:13:59 +000090
91 def lock(self):
92 """Acquire the lock.
93
94 Note: This is a NON-BLOCKING FAIL-FAST operation.
95 Do. Or do not. There is no try.
96 """
97 try:
98 self._make_lockfile()
99 except OSError as e:
100 if e.errno == errno.EEXIST:
101 raise LockError("%s is already locked" % self.path)
102 else:
103 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
104
105 def unlock(self):
106 """Release the lock."""
szager@chromium.org108eced2014-06-19 21:22:43 +0000107 try:
108 if not self.is_locked():
109 raise LockError("%s is not locked" % self.path)
110 if not self.i_am_locking():
111 raise LockError("%s is locked, but not by me" % self.path)
112 self._remove_lockfile()
113 except WinErr:
114 # Windows is unreliable when it comes to file locking. YMMV.
115 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +0000116
117 def break_lock(self):
118 """Remove the lock, even if it was created by someone else."""
119 try:
120 self._remove_lockfile()
121 return True
122 except OSError as exc:
123 if exc.errno == errno.ENOENT:
124 return False
125 else:
126 raise
127
128 def is_locked(self):
129 """Test if the file is locked by anyone.
130
131 Note: This method is potentially racy. By the time it returns the lockfile
132 may have been unlocked, removed, or stolen by some other process.
133 """
134 return os.path.exists(self.lockfile)
135
136 def i_am_locking(self):
137 """Test if the file is locked by this process."""
138 return self.is_locked() and self.pid == self._read_pid()
139
agable@chromium.org5a306a22014-02-24 22:13:59 +0000140
szager@chromium.org848fd492014-04-09 19:06:44 +0000141class Mirror(object):
142
143 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
144 gsutil_exe = os.path.join(
145 os.path.dirname(os.path.abspath(__file__)),
146 'third_party', 'gsutil', 'gsutil')
szager@chromium.org1132f5f2014-08-23 01:57:59 +0000147 cachepath_lock = threading.Lock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000148
149 def __init__(self, url, refs=None, print_func=None):
150 self.url = url
151 self.refs = refs or []
152 self.basedir = self.UrlToCacheDir(url)
153 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
154 self.print = print_func or print
155
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000156 @property
157 def bootstrap_bucket(self):
158 if 'chrome-internal' in self.url:
159 return 'chrome-git-cache'
160 else:
161 return 'chromium-git-cache'
162
szager@chromium.org174766f2014-05-13 21:27:46 +0000163 @classmethod
164 def FromPath(cls, path):
165 return cls(cls.CacheDirToUrl(path))
166
szager@chromium.org848fd492014-04-09 19:06:44 +0000167 @staticmethod
168 def UrlToCacheDir(url):
169 """Convert a git url to a normalized form for the cache dir path."""
170 parsed = urlparse.urlparse(url)
171 norm_url = parsed.netloc + parsed.path
172 if norm_url.endswith('.git'):
173 norm_url = norm_url[:-len('.git')]
174 return norm_url.replace('-', '--').replace('/', '-').lower()
175
176 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000177 def CacheDirToUrl(path):
178 """Convert a cache dir path to its corresponding url."""
179 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
180 return 'https://%s' % netpath
181
182 @staticmethod
szager@chromium.org848fd492014-04-09 19:06:44 +0000183 def FindExecutable(executable):
184 """This mimics the "which" utility."""
185 path_folders = os.environ.get('PATH').split(os.pathsep)
186
187 for path_folder in path_folders:
188 target = os.path.join(path_folder, executable)
189 # Just incase we have some ~/blah paths.
190 target = os.path.abspath(os.path.expanduser(target))
191 if os.path.isfile(target) and os.access(target, os.X_OK):
192 return target
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000193 if sys.platform.startswith('win'):
194 for suffix in ('.bat', '.cmd', '.exe'):
195 alt_target = target + suffix
szager@chromium.org4039b312014-04-09 21:56:46 +0000196 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000197 return alt_target
szager@chromium.org848fd492014-04-09 19:06:44 +0000198 return None
199
200 @classmethod
201 def SetCachePath(cls, cachepath):
szager@chromium.org84c56002014-08-23 03:33:28 +0000202 with cls.cachepath_lock:
203 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000204
205 @classmethod
206 def GetCachePath(cls):
szager@chromium.org84c56002014-08-23 03:33:28 +0000207 with cls.cachepath_lock:
208 if not hasattr(cls, 'cachepath'):
209 try:
210 cachepath = subprocess.check_output(
211 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
212 except subprocess.CalledProcessError:
213 cachepath = None
214 if not cachepath:
215 raise RuntimeError(
216 'No global cache.cachepath git configuration found.')
217 setattr(cls, 'cachepath', cachepath)
218 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000219
220 def RunGit(self, cmd, **kwargs):
221 """Run git in a subprocess."""
222 cwd = kwargs.setdefault('cwd', self.mirror_path)
223 kwargs.setdefault('print_stdout', False)
224 kwargs.setdefault('filter_fn', self.print)
225 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
226 env.setdefault('GIT_ASKPASS', 'true')
227 env.setdefault('SSH_ASKPASS', 'true')
228 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
229 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
230
231 def config(self, cwd=None):
232 if cwd is None:
233 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000234
235 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
236 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
237
238 # Don't combine pack files into one big pack file. It's really slow for
239 # repositories, and there's no way to track progress and make sure it's
240 # not stuck.
241 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
242
243 # Allocate more RAM for cache-ing delta chains, for better performance
244 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000245 self.RunGit(['config', 'core.deltaBaseCacheLimit',
246 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000247
szager@chromium.org848fd492014-04-09 19:06:44 +0000248 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
249 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
szager@chromium.org965c44f2014-08-19 21:19:19 +0000250 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000251 for ref in self.refs:
252 ref = ref.lstrip('+').rstrip('/')
253 if ref.startswith('refs/'):
254 refspec = '+%s:%s' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000255 regex = r'\+%s:.*' % ref.replace('*', r'\*')
szager@chromium.org848fd492014-04-09 19:06:44 +0000256 else:
257 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000258 regex = r'\+refs/heads/%s:.*' % ref.replace('*', r'\*')
259 self.RunGit(
260 ['config', '--replace-all', 'remote.origin.fetch', refspec, regex],
261 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000262
263 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000264 """Bootstrap the repo from Google Stroage if possible.
265
266 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
267 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000268
hinoka@google.com776a2c32014-04-25 07:54:25 +0000269 python_fallback = False
270 if sys.platform.startswith('win') and not self.FindExecutable('7z'):
271 python_fallback = True
272 elif sys.platform.startswith('darwin'):
273 # The OSX version of unzip doesn't support zip64.
274 python_fallback = True
275 elif not self.FindExecutable('unzip'):
276 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000277
278 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.orgc2bc22d2014-06-05 21:19:38 +0000279 gsutil = Gsutil(self.gsutil_exe, boto_path=None, bypass_prodaccess=True)
szager@chromium.org848fd492014-04-09 19:06:44 +0000280 # Get the most recent version of the zipfile.
281 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
282 ls_out_sorted = sorted(ls_out.splitlines())
283 if not ls_out_sorted:
284 # This repo is not on Google Storage.
285 return False
286 latest_checkout = ls_out_sorted[-1]
287
288 # Download zip file to a temporary directory.
289 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000290 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000291 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000292 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000293 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000294 return False
295 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
296
hinoka@google.com776a2c32014-04-25 07:54:25 +0000297 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
298 if not python_fallback:
299 if sys.platform.startswith('win'):
300 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
301 else:
302 cmd = ['unzip', filename, '-d', directory]
303 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000304 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000305 try:
306 with zipfile.ZipFile(filename, 'r') as f:
307 f.printdir()
308 f.extractall(directory)
309 except Exception as e:
310 self.print('Encountered error: %s' % str(e), file=sys.stderr)
311 retcode = 1
312 else:
313 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000314 finally:
315 # Clean up the downloaded zipfile.
316 gclient_utils.rmtree(tempdir)
317
318 if retcode:
319 self.print(
320 'Extracting bootstrap zipfile %s failed.\n'
321 'Resuming normal operations.' % filename)
322 return False
323 return True
324
325 def exists(self):
326 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
327
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000328 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
329 tempdir = None
330 config_file = os.path.join(self.mirror_path, 'config')
331 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
332 pack_files = []
333
334 if os.path.isdir(pack_dir):
335 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
336
337 should_bootstrap = (force or
338 not os.path.exists(config_file) or
339 len(pack_files) > GC_AUTOPACKLIMIT)
340 if should_bootstrap:
341 tempdir = tempfile.mkdtemp(
342 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
343 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
344 if bootstrapped:
345 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000346 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000347 elif not os.path.exists(config_file):
348 # Bootstrap failed, no previous cache; start with a bare git dir.
349 self.RunGit(['init', '--bare'], cwd=tempdir)
350 else:
351 # Bootstrap failed, previous cache exists; warn and continue.
352 logging.warn(
353 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
354 'but failed. Continuing with non-optimized repository.'
355 % len(pack_files))
356 gclient_utils.rmtree(tempdir)
357 tempdir = None
358 else:
359 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
360 logging.warn(
361 'Shallow fetch requested, but repo cache already exists.')
362 return tempdir
363
364 def _fetch(self, rundir, verbose, depth):
365 self.config(rundir)
366 v = []
367 d = []
368 if verbose:
369 v = ['-v', '--progress']
370 if depth:
371 d = ['--depth', str(depth)]
372 fetch_cmd = ['fetch'] + v + d + ['origin']
373 fetch_specs = subprocess.check_output(
374 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
375 cwd=rundir).strip().splitlines()
376 for spec in fetch_specs:
377 try:
378 self.print('Fetching %s' % spec)
379 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
380 except subprocess.CalledProcessError:
381 if spec == '+refs/heads/*:refs/heads/*':
382 raise RefsHeadsFailedToFetch
383 logging.warn('Fetch of %s failed' % spec)
384
szager@chromium.org848fd492014-04-09 19:06:44 +0000385 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.org108eced2014-06-19 21:22:43 +0000386 verbose=False, ignore_lock=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000387 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000388 if shallow and not depth:
389 depth = 10000
390 gclient_utils.safe_makedirs(self.GetCachePath())
391
szager@chromium.org108eced2014-06-19 21:22:43 +0000392 lockfile = Lockfile(self.mirror_path)
393 if not ignore_lock:
394 lockfile.lock()
395
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000396 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000397 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000398 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000399 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000400 self._fetch(rundir, verbose, depth)
401 except RefsHeadsFailedToFetch:
402 # This is a major failure, we need to clean and force a bootstrap.
403 gclient_utils.rmtree(rundir)
404 self.print(GIT_CACHE_CORRUPT_MESSAGE)
405 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
406 assert tempdir
407 self._fetch(tempdir or self.mirror_path, verbose, depth)
408 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000409 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000410 try:
hinoka@chromium.orga5cda1e2014-08-24 12:09:13 +0000411 if os.path.exists(self.mirror_path):
412 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000413 os.rename(tempdir, self.mirror_path)
414 except OSError as e:
415 # This is somehow racy on Windows.
416 # Catching OSError because WindowsError isn't portable and
417 # pylint complains.
418 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
419 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000420 if not ignore_lock:
421 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000422
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000423 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000424 # The files are named <git number>.zip
425 gen_number = subprocess.check_output(
426 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
427 self.RunGit(['gc']) # Run Garbage Collect to compress packfile.
428 # Creating a temp file and then deleting it ensures we can use this name.
429 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
430 os.remove(tmp_zipfile)
431 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
432 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000433 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
434 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000435 gsutil.call('cp', tmp_zipfile, dest_name)
436 os.remove(tmp_zipfile)
437
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000438 # Remove all other files in the same directory.
439 if prune:
440 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
441 for filename in ls_out.splitlines():
442 if filename == dest_name:
443 continue
444 gsutil.call('rm', filename)
445
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000446 @staticmethod
447 def DeleteTmpPackFiles(path):
448 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000449 if not os.path.isdir(pack_dir):
450 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000451 pack_files = [f for f in os.listdir(pack_dir) if
452 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
453 for f in pack_files:
454 f = os.path.join(pack_dir, f)
455 try:
456 os.remove(f)
457 logging.warn('Deleted stale temporary pack file %s' % f)
458 except OSError:
459 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000460
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000461 @classmethod
462 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000463 did_unlock = False
464 lf = Lockfile(path)
465 if lf.break_lock():
466 did_unlock = True
467 # Look for lock files that might have been left behind by an interrupted
468 # git process.
469 lf = os.path.join(path, 'config.lock')
470 if os.path.exists(lf):
471 os.remove(lf)
472 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000473 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000474 return did_unlock
475
szager@chromium.org848fd492014-04-09 19:06:44 +0000476 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000477 return self.BreakLocks(self.mirror_path)
478
479 @classmethod
480 def UnlockAll(cls):
481 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000482 if not cachepath:
483 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000484 dirlist = os.listdir(cachepath)
485 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
486 if os.path.isdir(os.path.join(cachepath, path))])
487 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000488 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
489 gclient_utils.rmtree(os.path.join(cachepath, dirent))
490 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000491 os.path.isfile(os.path.join(cachepath, dirent))):
492 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
493
494 unlocked_repos = []
495 for repo_dir in repo_dirs:
496 if cls.BreakLocks(repo_dir):
497 unlocked_repos.append(repo_dir)
498
499 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000500
agable@chromium.org5a306a22014-02-24 22:13:59 +0000501@subcommand.usage('[url of repo to check for caching]')
502def CMDexists(parser, args):
503 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000504 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000505 if not len(args) == 1:
506 parser.error('git cache exists only takes exactly one repo url.')
507 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000508 mirror = Mirror(url)
509 if mirror.exists():
510 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000511 return 0
512 return 1
513
514
hinoka@google.com563559c2014-04-02 00:36:24 +0000515@subcommand.usage('[url of repo to create a bootstrap zip file]')
516def CMDupdate_bootstrap(parser, args):
517 """Create and uploads a bootstrap tarball."""
518 # Lets just assert we can't do this on Windows.
519 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000520 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000521 return 1
522
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000523 parser.add_option('--prune', action='store_true',
524 help='Prune all other cached zipballs of the same repo.')
525
hinoka@google.com563559c2014-04-02 00:36:24 +0000526 # First, we need to ensure the cache is populated.
527 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000528 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000529 CMDpopulate(parser, populate_args)
530
531 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000532 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000533 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000534 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000535 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000536 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000537
538
agable@chromium.org5a306a22014-02-24 22:13:59 +0000539@subcommand.usage('[url of repo to add to or update in cache]')
540def CMDpopulate(parser, args):
541 """Ensure that the cache has all up-to-date objects for the given repo."""
542 parser.add_option('--depth', type='int',
543 help='Only cache DEPTH commits of history')
544 parser.add_option('--shallow', '-s', action='store_true',
545 help='Only cache 10000 commits of history')
546 parser.add_option('--ref', action='append',
547 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000548 parser.add_option('--no_bootstrap', '--no-bootstrap',
549 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000550 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000551 parser.add_option('--ignore_locks', '--ignore-locks',
552 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000553 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000554
agable@chromium.org5a306a22014-02-24 22:13:59 +0000555 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000556 if not len(args) == 1:
557 parser.error('git cache populate only takes exactly one repo url.')
558 url = args[0]
559
szager@chromium.org848fd492014-04-09 19:06:44 +0000560 mirror = Mirror(url, refs=options.ref)
561 kwargs = {
562 'verbose': options.verbose,
563 'shallow': options.shallow,
564 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000565 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000566 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000567 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000568 kwargs['depth'] = options.depth
569 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000570
571
szager@chromium.orgf3145112014-08-07 21:02:36 +0000572@subcommand.usage('Fetch new commits into cache and current checkout')
573def CMDfetch(parser, args):
574 """Update mirror, and fetch in cwd."""
575 parser.add_option('--all', action='store_true', help='Fetch all remotes')
576 options, args = parser.parse_args(args)
577
578 # Figure out which remotes to fetch. This mimics the behavior of regular
579 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
580 # this will NOT try to traverse up the branching structure to find the
581 # ultimate remote to update.
582 remotes = []
583 if options.all:
584 assert not args, 'fatal: fetch --all does not take a repository argument'
585 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
586 elif args:
587 remotes = args
588 else:
589 current_branch = subprocess.check_output(
590 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
591 if current_branch != 'HEAD':
592 upstream = subprocess.check_output(
593 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
594 ).strip()
595 if upstream and upstream != '.':
596 remotes = [upstream]
597 if not remotes:
598 remotes = ['origin']
599
600 cachepath = Mirror.GetCachePath()
601 git_dir = os.path.abspath(subprocess.check_output(
602 [Mirror.git_exe, 'rev-parse', '--git-dir']))
603 git_dir = os.path.abspath(git_dir)
604 if git_dir.startswith(cachepath):
605 mirror = Mirror.FromPath(git_dir)
606 mirror.populate()
607 return 0
608 for remote in remotes:
609 remote_url = subprocess.check_output(
610 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
611 if remote_url.startswith(cachepath):
612 mirror = Mirror.FromPath(remote_url)
613 mirror.print = lambda *args: None
614 print('Updating git cache...')
615 mirror.populate()
616 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
617 return 0
618
619
agable@chromium.org5a306a22014-02-24 22:13:59 +0000620@subcommand.usage('[url of repo to unlock, or -a|--all]')
621def CMDunlock(parser, args):
622 """Unlock one or all repos if their lock files are still around."""
623 parser.add_option('--force', '-f', action='store_true',
624 help='Actually perform the action')
625 parser.add_option('--all', '-a', action='store_true',
626 help='Unlock all repository caches')
627 options, args = parser.parse_args(args)
628 if len(args) > 1 or (len(args) == 0 and not options.all):
629 parser.error('git cache unlock takes exactly one repo url, or --all')
630
agable@chromium.org5a306a22014-02-24 22:13:59 +0000631 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000632 cachepath = Mirror.GetCachePath()
633 lockfiles = [os.path.join(cachepath, path)
634 for path in os.listdir(cachepath)
635 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000636 parser.error('git cache unlock requires -f|--force to do anything. '
637 'Refusing to unlock the following repo caches: '
638 ', '.join(lockfiles))
639
szager@chromium.org848fd492014-04-09 19:06:44 +0000640 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000641 if options.all:
642 unlocked_repos.extend(Mirror.UnlockAll())
643 else:
644 m = Mirror(args[0])
645 if m.unlock():
646 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000647
szager@chromium.org848fd492014-04-09 19:06:44 +0000648 if unlocked_repos:
649 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
650 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000651
652
653class OptionParser(optparse.OptionParser):
654 """Wrapper class for OptionParser to handle global options."""
655
656 def __init__(self, *args, **kwargs):
657 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
658 self.add_option('-c', '--cache-dir',
659 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000660 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000661 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000662 self.add_option('-q', '--quiet', action='store_true',
663 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000664
665 def parse_args(self, args=None, values=None):
666 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000667 if options.quiet:
668 options.verbose = 0
669
670 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
671 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000672
673 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000674 global_cache_dir = Mirror.GetCachePath()
675 except RuntimeError:
676 global_cache_dir = None
677 if options.cache_dir:
678 if global_cache_dir and (
679 os.path.abspath(options.cache_dir) !=
680 os.path.abspath(global_cache_dir)):
681 logging.warn('Overriding globally-configured cache directory.')
682 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000683
agable@chromium.org5a306a22014-02-24 22:13:59 +0000684 return options, args
685
686
687def main(argv):
688 dispatcher = subcommand.CommandDispatcher(__name__)
689 return dispatcher.execute(OptionParser(), argv)
690
691
692if __name__ == '__main__':
693 sys.exit(main(sys.argv[1:]))