blob: e80923cdbb074a0c27355bd51041341fd473ed27 [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(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +0000145 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
szager@chromium.org1132f5f2014-08-23 01:57:59 +0000146 cachepath_lock = threading.Lock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000147
148 def __init__(self, url, refs=None, print_func=None):
149 self.url = url
150 self.refs = refs or []
151 self.basedir = self.UrlToCacheDir(url)
152 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000153 if print_func:
154 self.print = self.print_without_file
155 self.print_func = print_func
156 else:
157 self.print = print
158
159 def print_without_file(self, message, **kwargs):
160 self.print_func(message)
szager@chromium.org848fd492014-04-09 19:06:44 +0000161
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000162 @property
163 def bootstrap_bucket(self):
164 if 'chrome-internal' in self.url:
165 return 'chrome-git-cache'
166 else:
167 return 'chromium-git-cache'
168
szager@chromium.org174766f2014-05-13 21:27:46 +0000169 @classmethod
170 def FromPath(cls, path):
171 return cls(cls.CacheDirToUrl(path))
172
szager@chromium.org848fd492014-04-09 19:06:44 +0000173 @staticmethod
174 def UrlToCacheDir(url):
175 """Convert a git url to a normalized form for the cache dir path."""
176 parsed = urlparse.urlparse(url)
177 norm_url = parsed.netloc + parsed.path
178 if norm_url.endswith('.git'):
179 norm_url = norm_url[:-len('.git')]
180 return norm_url.replace('-', '--').replace('/', '-').lower()
181
182 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000183 def CacheDirToUrl(path):
184 """Convert a cache dir path to its corresponding url."""
185 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
186 return 'https://%s' % netpath
187
szager@chromium.org848fd492014-04-09 19:06:44 +0000188 @classmethod
189 def SetCachePath(cls, cachepath):
szager@chromium.org84c56002014-08-23 03:33:28 +0000190 with cls.cachepath_lock:
191 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000192
193 @classmethod
194 def GetCachePath(cls):
szager@chromium.org84c56002014-08-23 03:33:28 +0000195 with cls.cachepath_lock:
196 if not hasattr(cls, 'cachepath'):
197 try:
198 cachepath = subprocess.check_output(
199 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
200 except subprocess.CalledProcessError:
201 cachepath = None
202 if not cachepath:
203 raise RuntimeError(
204 'No global cache.cachepath git configuration found.')
205 setattr(cls, 'cachepath', cachepath)
206 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000207
208 def RunGit(self, cmd, **kwargs):
209 """Run git in a subprocess."""
210 cwd = kwargs.setdefault('cwd', self.mirror_path)
211 kwargs.setdefault('print_stdout', False)
212 kwargs.setdefault('filter_fn', self.print)
213 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
214 env.setdefault('GIT_ASKPASS', 'true')
215 env.setdefault('SSH_ASKPASS', 'true')
216 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
217 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
218
219 def config(self, cwd=None):
220 if cwd is None:
221 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000222
223 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
224 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
225
226 # Don't combine pack files into one big pack file. It's really slow for
227 # repositories, and there's no way to track progress and make sure it's
228 # not stuck.
229 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
230
231 # Allocate more RAM for cache-ing delta chains, for better performance
232 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000233 self.RunGit(['config', 'core.deltaBaseCacheLimit',
234 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000235
szager@chromium.org848fd492014-04-09 19:06:44 +0000236 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
237 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
szager@chromium.org965c44f2014-08-19 21:19:19 +0000238 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000239 for ref in self.refs:
240 ref = ref.lstrip('+').rstrip('/')
241 if ref.startswith('refs/'):
242 refspec = '+%s:%s' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000243 regex = r'\+%s:.*' % ref.replace('*', r'\*')
szager@chromium.org848fd492014-04-09 19:06:44 +0000244 else:
245 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000246 regex = r'\+refs/heads/%s:.*' % ref.replace('*', r'\*')
247 self.RunGit(
248 ['config', '--replace-all', 'remote.origin.fetch', refspec, regex],
249 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000250
251 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000252 """Bootstrap the repo from Google Stroage if possible.
253
254 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
255 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000256
hinoka@google.com776a2c32014-04-25 07:54:25 +0000257 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000258 if (sys.platform.startswith('win') and
259 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000260 python_fallback = True
261 elif sys.platform.startswith('darwin'):
262 # The OSX version of unzip doesn't support zip64.
263 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000264 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000265 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000266
267 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000268 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000269 # Get the most recent version of the zipfile.
270 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
271 ls_out_sorted = sorted(ls_out.splitlines())
272 if not ls_out_sorted:
273 # This repo is not on Google Storage.
274 return False
275 latest_checkout = ls_out_sorted[-1]
276
277 # Download zip file to a temporary directory.
278 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000279 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000280 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000281 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000282 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000283 return False
284 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
285
hinoka@google.com776a2c32014-04-25 07:54:25 +0000286 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
287 if not python_fallback:
288 if sys.platform.startswith('win'):
289 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
290 else:
291 cmd = ['unzip', filename, '-d', directory]
292 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000293 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000294 try:
295 with zipfile.ZipFile(filename, 'r') as f:
296 f.printdir()
297 f.extractall(directory)
298 except Exception as e:
299 self.print('Encountered error: %s' % str(e), file=sys.stderr)
300 retcode = 1
301 else:
302 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000303 finally:
304 # Clean up the downloaded zipfile.
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000305 gclient_utils.rm_file_or_tree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000306
307 if retcode:
308 self.print(
309 'Extracting bootstrap zipfile %s failed.\n'
310 'Resuming normal operations.' % filename)
311 return False
312 return True
313
314 def exists(self):
315 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
316
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000317 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
318 tempdir = None
319 config_file = os.path.join(self.mirror_path, 'config')
320 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
321 pack_files = []
322
323 if os.path.isdir(pack_dir):
324 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
325
326 should_bootstrap = (force or
327 not os.path.exists(config_file) or
328 len(pack_files) > GC_AUTOPACKLIMIT)
329 if should_bootstrap:
330 tempdir = tempfile.mkdtemp(
331 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
332 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
333 if bootstrapped:
334 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000335 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000336 elif not os.path.exists(config_file):
337 # Bootstrap failed, no previous cache; start with a bare git dir.
338 self.RunGit(['init', '--bare'], cwd=tempdir)
339 else:
340 # Bootstrap failed, previous cache exists; warn and continue.
341 logging.warn(
342 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
343 'but failed. Continuing with non-optimized repository.'
344 % len(pack_files))
345 gclient_utils.rmtree(tempdir)
346 tempdir = None
347 else:
348 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
349 logging.warn(
350 'Shallow fetch requested, but repo cache already exists.')
351 return tempdir
352
353 def _fetch(self, rundir, verbose, depth):
354 self.config(rundir)
355 v = []
356 d = []
357 if verbose:
358 v = ['-v', '--progress']
359 if depth:
360 d = ['--depth', str(depth)]
361 fetch_cmd = ['fetch'] + v + d + ['origin']
362 fetch_specs = subprocess.check_output(
363 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
364 cwd=rundir).strip().splitlines()
365 for spec in fetch_specs:
366 try:
367 self.print('Fetching %s' % spec)
368 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
369 except subprocess.CalledProcessError:
370 if spec == '+refs/heads/*:refs/heads/*':
371 raise RefsHeadsFailedToFetch
372 logging.warn('Fetch of %s failed' % spec)
373
szager@chromium.org848fd492014-04-09 19:06:44 +0000374 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.org108eced2014-06-19 21:22:43 +0000375 verbose=False, ignore_lock=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000376 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000377 if shallow and not depth:
378 depth = 10000
379 gclient_utils.safe_makedirs(self.GetCachePath())
380
szager@chromium.org108eced2014-06-19 21:22:43 +0000381 lockfile = Lockfile(self.mirror_path)
382 if not ignore_lock:
383 lockfile.lock()
384
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000385 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000386 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000387 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000388 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000389 self._fetch(rundir, verbose, depth)
390 except RefsHeadsFailedToFetch:
391 # This is a major failure, we need to clean and force a bootstrap.
392 gclient_utils.rmtree(rundir)
393 self.print(GIT_CACHE_CORRUPT_MESSAGE)
394 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
395 assert tempdir
396 self._fetch(tempdir or self.mirror_path, verbose, depth)
397 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000398 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000399 try:
hinoka@chromium.orga5cda1e2014-08-24 12:09:13 +0000400 if os.path.exists(self.mirror_path):
401 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000402 os.rename(tempdir, self.mirror_path)
403 except OSError as e:
404 # This is somehow racy on Windows.
405 # Catching OSError because WindowsError isn't portable and
406 # pylint complains.
407 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
408 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000409 if not ignore_lock:
410 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000411
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000412 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000413 # The files are named <git number>.zip
414 gen_number = subprocess.check_output(
415 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000416 # Run Garbage Collect to compress packfile.
417 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000418 # Creating a temp file and then deleting it ensures we can use this name.
419 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
420 os.remove(tmp_zipfile)
421 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
422 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000423 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
424 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000425 gsutil.call('cp', tmp_zipfile, dest_name)
426 os.remove(tmp_zipfile)
427
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000428 # Remove all other files in the same directory.
429 if prune:
430 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
431 for filename in ls_out.splitlines():
432 if filename == dest_name:
433 continue
434 gsutil.call('rm', filename)
435
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000436 @staticmethod
437 def DeleteTmpPackFiles(path):
438 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000439 if not os.path.isdir(pack_dir):
440 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000441 pack_files = [f for f in os.listdir(pack_dir) if
442 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
443 for f in pack_files:
444 f = os.path.join(pack_dir, f)
445 try:
446 os.remove(f)
447 logging.warn('Deleted stale temporary pack file %s' % f)
448 except OSError:
449 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000450
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000451 @classmethod
452 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000453 did_unlock = False
454 lf = Lockfile(path)
455 if lf.break_lock():
456 did_unlock = True
457 # Look for lock files that might have been left behind by an interrupted
458 # git process.
459 lf = os.path.join(path, 'config.lock')
460 if os.path.exists(lf):
461 os.remove(lf)
462 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000463 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000464 return did_unlock
465
szager@chromium.org848fd492014-04-09 19:06:44 +0000466 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000467 return self.BreakLocks(self.mirror_path)
468
469 @classmethod
470 def UnlockAll(cls):
471 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000472 if not cachepath:
473 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000474 dirlist = os.listdir(cachepath)
475 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
476 if os.path.isdir(os.path.join(cachepath, path))])
477 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000478 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000479 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000480 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000481 os.path.isfile(os.path.join(cachepath, dirent))):
482 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
483
484 unlocked_repos = []
485 for repo_dir in repo_dirs:
486 if cls.BreakLocks(repo_dir):
487 unlocked_repos.append(repo_dir)
488
489 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000490
agable@chromium.org5a306a22014-02-24 22:13:59 +0000491@subcommand.usage('[url of repo to check for caching]')
492def CMDexists(parser, args):
493 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000494 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000495 if not len(args) == 1:
496 parser.error('git cache exists only takes exactly one repo url.')
497 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000498 mirror = Mirror(url)
499 if mirror.exists():
500 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000501 return 0
502 return 1
503
504
hinoka@google.com563559c2014-04-02 00:36:24 +0000505@subcommand.usage('[url of repo to create a bootstrap zip file]')
506def CMDupdate_bootstrap(parser, args):
507 """Create and uploads a bootstrap tarball."""
508 # Lets just assert we can't do this on Windows.
509 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000510 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000511 return 1
512
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000513 parser.add_option('--prune', action='store_true',
514 help='Prune all other cached zipballs of the same repo.')
515
hinoka@google.com563559c2014-04-02 00:36:24 +0000516 # First, we need to ensure the cache is populated.
517 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000518 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000519 CMDpopulate(parser, populate_args)
520
521 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000522 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000523 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000524 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000525 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000526 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000527
528
agable@chromium.org5a306a22014-02-24 22:13:59 +0000529@subcommand.usage('[url of repo to add to or update in cache]')
530def CMDpopulate(parser, args):
531 """Ensure that the cache has all up-to-date objects for the given repo."""
532 parser.add_option('--depth', type='int',
533 help='Only cache DEPTH commits of history')
534 parser.add_option('--shallow', '-s', action='store_true',
535 help='Only cache 10000 commits of history')
536 parser.add_option('--ref', action='append',
537 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000538 parser.add_option('--no_bootstrap', '--no-bootstrap',
539 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000540 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000541 parser.add_option('--ignore_locks', '--ignore-locks',
542 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000543 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000544
agable@chromium.org5a306a22014-02-24 22:13:59 +0000545 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000546 if not len(args) == 1:
547 parser.error('git cache populate only takes exactly one repo url.')
548 url = args[0]
549
szager@chromium.org848fd492014-04-09 19:06:44 +0000550 mirror = Mirror(url, refs=options.ref)
551 kwargs = {
552 'verbose': options.verbose,
553 'shallow': options.shallow,
554 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000555 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000556 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000557 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000558 kwargs['depth'] = options.depth
559 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000560
561
szager@chromium.orgf3145112014-08-07 21:02:36 +0000562@subcommand.usage('Fetch new commits into cache and current checkout')
563def CMDfetch(parser, args):
564 """Update mirror, and fetch in cwd."""
565 parser.add_option('--all', action='store_true', help='Fetch all remotes')
566 options, args = parser.parse_args(args)
567
568 # Figure out which remotes to fetch. This mimics the behavior of regular
569 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
570 # this will NOT try to traverse up the branching structure to find the
571 # ultimate remote to update.
572 remotes = []
573 if options.all:
574 assert not args, 'fatal: fetch --all does not take a repository argument'
575 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
576 elif args:
577 remotes = args
578 else:
579 current_branch = subprocess.check_output(
580 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
581 if current_branch != 'HEAD':
582 upstream = subprocess.check_output(
583 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
584 ).strip()
585 if upstream and upstream != '.':
586 remotes = [upstream]
587 if not remotes:
588 remotes = ['origin']
589
590 cachepath = Mirror.GetCachePath()
591 git_dir = os.path.abspath(subprocess.check_output(
592 [Mirror.git_exe, 'rev-parse', '--git-dir']))
593 git_dir = os.path.abspath(git_dir)
594 if git_dir.startswith(cachepath):
595 mirror = Mirror.FromPath(git_dir)
596 mirror.populate()
597 return 0
598 for remote in remotes:
599 remote_url = subprocess.check_output(
600 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
601 if remote_url.startswith(cachepath):
602 mirror = Mirror.FromPath(remote_url)
603 mirror.print = lambda *args: None
604 print('Updating git cache...')
605 mirror.populate()
606 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
607 return 0
608
609
agable@chromium.org5a306a22014-02-24 22:13:59 +0000610@subcommand.usage('[url of repo to unlock, or -a|--all]')
611def CMDunlock(parser, args):
612 """Unlock one or all repos if their lock files are still around."""
613 parser.add_option('--force', '-f', action='store_true',
614 help='Actually perform the action')
615 parser.add_option('--all', '-a', action='store_true',
616 help='Unlock all repository caches')
617 options, args = parser.parse_args(args)
618 if len(args) > 1 or (len(args) == 0 and not options.all):
619 parser.error('git cache unlock takes exactly one repo url, or --all')
620
agable@chromium.org5a306a22014-02-24 22:13:59 +0000621 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000622 cachepath = Mirror.GetCachePath()
623 lockfiles = [os.path.join(cachepath, path)
624 for path in os.listdir(cachepath)
625 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000626 parser.error('git cache unlock requires -f|--force to do anything. '
627 'Refusing to unlock the following repo caches: '
628 ', '.join(lockfiles))
629
szager@chromium.org848fd492014-04-09 19:06:44 +0000630 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000631 if options.all:
632 unlocked_repos.extend(Mirror.UnlockAll())
633 else:
634 m = Mirror(args[0])
635 if m.unlock():
636 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000637
szager@chromium.org848fd492014-04-09 19:06:44 +0000638 if unlocked_repos:
639 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
640 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000641
642
643class OptionParser(optparse.OptionParser):
644 """Wrapper class for OptionParser to handle global options."""
645
646 def __init__(self, *args, **kwargs):
647 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
648 self.add_option('-c', '--cache-dir',
649 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000650 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000651 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000652 self.add_option('-q', '--quiet', action='store_true',
653 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000654
655 def parse_args(self, args=None, values=None):
656 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000657 if options.quiet:
658 options.verbose = 0
659
660 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
661 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000662
663 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000664 global_cache_dir = Mirror.GetCachePath()
665 except RuntimeError:
666 global_cache_dir = None
667 if options.cache_dir:
668 if global_cache_dir and (
669 os.path.abspath(options.cache_dir) !=
670 os.path.abspath(global_cache_dir)):
671 logging.warn('Overriding globally-configured cache directory.')
672 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000673
agable@chromium.org5a306a22014-02-24 22:13:59 +0000674 return options, args
675
676
677def main(argv):
678 dispatcher = subcommand.CommandDispatcher(__name__)
679 return dispatcher.execute(OptionParser(), argv)
680
681
682if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000683 try:
684 sys.exit(main(sys.argv[1:]))
685 except KeyboardInterrupt:
686 sys.stderr.write('interrupted\n')
687 sys.exit(1)