blob: 7cc19bdb24e8db0fa8c049b0ebc345e0744c48a8 [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
188 @staticmethod
szager@chromium.org848fd492014-04-09 19:06:44 +0000189 def FindExecutable(executable):
190 """This mimics the "which" utility."""
191 path_folders = os.environ.get('PATH').split(os.pathsep)
192
193 for path_folder in path_folders:
194 target = os.path.join(path_folder, executable)
195 # Just incase we have some ~/blah paths.
196 target = os.path.abspath(os.path.expanduser(target))
197 if os.path.isfile(target) and os.access(target, os.X_OK):
198 return target
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000199 if sys.platform.startswith('win'):
200 for suffix in ('.bat', '.cmd', '.exe'):
201 alt_target = target + suffix
szager@chromium.org4039b312014-04-09 21:56:46 +0000202 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000203 return alt_target
szager@chromium.org848fd492014-04-09 19:06:44 +0000204 return None
205
206 @classmethod
207 def SetCachePath(cls, cachepath):
szager@chromium.org84c56002014-08-23 03:33:28 +0000208 with cls.cachepath_lock:
209 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000210
211 @classmethod
212 def GetCachePath(cls):
szager@chromium.org84c56002014-08-23 03:33:28 +0000213 with cls.cachepath_lock:
214 if not hasattr(cls, 'cachepath'):
215 try:
216 cachepath = subprocess.check_output(
217 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
218 except subprocess.CalledProcessError:
219 cachepath = None
220 if not cachepath:
221 raise RuntimeError(
222 'No global cache.cachepath git configuration found.')
223 setattr(cls, 'cachepath', cachepath)
224 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000225
226 def RunGit(self, cmd, **kwargs):
227 """Run git in a subprocess."""
228 cwd = kwargs.setdefault('cwd', self.mirror_path)
229 kwargs.setdefault('print_stdout', False)
230 kwargs.setdefault('filter_fn', self.print)
231 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
232 env.setdefault('GIT_ASKPASS', 'true')
233 env.setdefault('SSH_ASKPASS', 'true')
234 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
235 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
236
237 def config(self, cwd=None):
238 if cwd is None:
239 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000240
241 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
242 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
243
244 # Don't combine pack files into one big pack file. It's really slow for
245 # repositories, and there's no way to track progress and make sure it's
246 # not stuck.
247 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
248
249 # Allocate more RAM for cache-ing delta chains, for better performance
250 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000251 self.RunGit(['config', 'core.deltaBaseCacheLimit',
252 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000253
szager@chromium.org848fd492014-04-09 19:06:44 +0000254 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
255 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
szager@chromium.org965c44f2014-08-19 21:19:19 +0000256 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000257 for ref in self.refs:
258 ref = ref.lstrip('+').rstrip('/')
259 if ref.startswith('refs/'):
260 refspec = '+%s:%s' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000261 regex = r'\+%s:.*' % ref.replace('*', r'\*')
szager@chromium.org848fd492014-04-09 19:06:44 +0000262 else:
263 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000264 regex = r'\+refs/heads/%s:.*' % ref.replace('*', r'\*')
265 self.RunGit(
266 ['config', '--replace-all', 'remote.origin.fetch', refspec, regex],
267 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000268
269 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000270 """Bootstrap the repo from Google Stroage if possible.
271
272 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
273 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000274
hinoka@google.com776a2c32014-04-25 07:54:25 +0000275 python_fallback = False
276 if sys.platform.startswith('win') and not self.FindExecutable('7z'):
277 python_fallback = True
278 elif sys.platform.startswith('darwin'):
279 # The OSX version of unzip doesn't support zip64.
280 python_fallback = True
281 elif not self.FindExecutable('unzip'):
282 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000283
284 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000285 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000286 # Get the most recent version of the zipfile.
287 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
288 ls_out_sorted = sorted(ls_out.splitlines())
289 if not ls_out_sorted:
290 # This repo is not on Google Storage.
291 return False
292 latest_checkout = ls_out_sorted[-1]
293
294 # Download zip file to a temporary directory.
295 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000296 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000297 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000298 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000299 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000300 return False
301 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
302
hinoka@google.com776a2c32014-04-25 07:54:25 +0000303 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
304 if not python_fallback:
305 if sys.platform.startswith('win'):
306 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
307 else:
308 cmd = ['unzip', filename, '-d', directory]
309 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000310 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000311 try:
312 with zipfile.ZipFile(filename, 'r') as f:
313 f.printdir()
314 f.extractall(directory)
315 except Exception as e:
316 self.print('Encountered error: %s' % str(e), file=sys.stderr)
317 retcode = 1
318 else:
319 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000320 finally:
321 # Clean up the downloaded zipfile.
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000322 gclient_utils.rm_file_or_tree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000323
324 if retcode:
325 self.print(
326 'Extracting bootstrap zipfile %s failed.\n'
327 'Resuming normal operations.' % filename)
328 return False
329 return True
330
331 def exists(self):
332 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
333
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000334 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
335 tempdir = None
336 config_file = os.path.join(self.mirror_path, 'config')
337 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
338 pack_files = []
339
340 if os.path.isdir(pack_dir):
341 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
342
343 should_bootstrap = (force or
344 not os.path.exists(config_file) or
345 len(pack_files) > GC_AUTOPACKLIMIT)
346 if should_bootstrap:
347 tempdir = tempfile.mkdtemp(
348 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
349 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
350 if bootstrapped:
351 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000352 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000353 elif not os.path.exists(config_file):
354 # Bootstrap failed, no previous cache; start with a bare git dir.
355 self.RunGit(['init', '--bare'], cwd=tempdir)
356 else:
357 # Bootstrap failed, previous cache exists; warn and continue.
358 logging.warn(
359 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
360 'but failed. Continuing with non-optimized repository.'
361 % len(pack_files))
362 gclient_utils.rmtree(tempdir)
363 tempdir = None
364 else:
365 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
366 logging.warn(
367 'Shallow fetch requested, but repo cache already exists.')
368 return tempdir
369
370 def _fetch(self, rundir, verbose, depth):
371 self.config(rundir)
372 v = []
373 d = []
374 if verbose:
375 v = ['-v', '--progress']
376 if depth:
377 d = ['--depth', str(depth)]
378 fetch_cmd = ['fetch'] + v + d + ['origin']
379 fetch_specs = subprocess.check_output(
380 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
381 cwd=rundir).strip().splitlines()
382 for spec in fetch_specs:
383 try:
384 self.print('Fetching %s' % spec)
385 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
386 except subprocess.CalledProcessError:
387 if spec == '+refs/heads/*:refs/heads/*':
388 raise RefsHeadsFailedToFetch
389 logging.warn('Fetch of %s failed' % spec)
390
szager@chromium.org848fd492014-04-09 19:06:44 +0000391 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.org108eced2014-06-19 21:22:43 +0000392 verbose=False, ignore_lock=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000393 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000394 if shallow and not depth:
395 depth = 10000
396 gclient_utils.safe_makedirs(self.GetCachePath())
397
szager@chromium.org108eced2014-06-19 21:22:43 +0000398 lockfile = Lockfile(self.mirror_path)
399 if not ignore_lock:
400 lockfile.lock()
401
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000402 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000403 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000404 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000405 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000406 self._fetch(rundir, verbose, depth)
407 except RefsHeadsFailedToFetch:
408 # This is a major failure, we need to clean and force a bootstrap.
409 gclient_utils.rmtree(rundir)
410 self.print(GIT_CACHE_CORRUPT_MESSAGE)
411 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
412 assert tempdir
413 self._fetch(tempdir or self.mirror_path, verbose, depth)
414 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000415 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000416 try:
hinoka@chromium.orga5cda1e2014-08-24 12:09:13 +0000417 if os.path.exists(self.mirror_path):
418 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000419 os.rename(tempdir, self.mirror_path)
420 except OSError as e:
421 # This is somehow racy on Windows.
422 # Catching OSError because WindowsError isn't portable and
423 # pylint complains.
424 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
425 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000426 if not ignore_lock:
427 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000428
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000429 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000430 # The files are named <git number>.zip
431 gen_number = subprocess.check_output(
432 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000433 # Run Garbage Collect to compress packfile.
434 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000435 # Creating a temp file and then deleting it ensures we can use this name.
436 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
437 os.remove(tmp_zipfile)
438 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
439 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000440 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
441 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000442 gsutil.call('cp', tmp_zipfile, dest_name)
443 os.remove(tmp_zipfile)
444
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000445 # Remove all other files in the same directory.
446 if prune:
447 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
448 for filename in ls_out.splitlines():
449 if filename == dest_name:
450 continue
451 gsutil.call('rm', filename)
452
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000453 @staticmethod
454 def DeleteTmpPackFiles(path):
455 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000456 if not os.path.isdir(pack_dir):
457 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000458 pack_files = [f for f in os.listdir(pack_dir) if
459 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
460 for f in pack_files:
461 f = os.path.join(pack_dir, f)
462 try:
463 os.remove(f)
464 logging.warn('Deleted stale temporary pack file %s' % f)
465 except OSError:
466 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000467
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000468 @classmethod
469 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000470 did_unlock = False
471 lf = Lockfile(path)
472 if lf.break_lock():
473 did_unlock = True
474 # Look for lock files that might have been left behind by an interrupted
475 # git process.
476 lf = os.path.join(path, 'config.lock')
477 if os.path.exists(lf):
478 os.remove(lf)
479 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000480 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000481 return did_unlock
482
szager@chromium.org848fd492014-04-09 19:06:44 +0000483 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000484 return self.BreakLocks(self.mirror_path)
485
486 @classmethod
487 def UnlockAll(cls):
488 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000489 if not cachepath:
490 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000491 dirlist = os.listdir(cachepath)
492 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
493 if os.path.isdir(os.path.join(cachepath, path))])
494 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000495 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000496 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000497 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000498 os.path.isfile(os.path.join(cachepath, dirent))):
499 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
500
501 unlocked_repos = []
502 for repo_dir in repo_dirs:
503 if cls.BreakLocks(repo_dir):
504 unlocked_repos.append(repo_dir)
505
506 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000507
agable@chromium.org5a306a22014-02-24 22:13:59 +0000508@subcommand.usage('[url of repo to check for caching]')
509def CMDexists(parser, args):
510 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000511 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000512 if not len(args) == 1:
513 parser.error('git cache exists only takes exactly one repo url.')
514 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000515 mirror = Mirror(url)
516 if mirror.exists():
517 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000518 return 0
519 return 1
520
521
hinoka@google.com563559c2014-04-02 00:36:24 +0000522@subcommand.usage('[url of repo to create a bootstrap zip file]')
523def CMDupdate_bootstrap(parser, args):
524 """Create and uploads a bootstrap tarball."""
525 # Lets just assert we can't do this on Windows.
526 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000527 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000528 return 1
529
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000530 parser.add_option('--prune', action='store_true',
531 help='Prune all other cached zipballs of the same repo.')
532
hinoka@google.com563559c2014-04-02 00:36:24 +0000533 # First, we need to ensure the cache is populated.
534 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000535 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000536 CMDpopulate(parser, populate_args)
537
538 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000539 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000540 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000541 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000542 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000543 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000544
545
agable@chromium.org5a306a22014-02-24 22:13:59 +0000546@subcommand.usage('[url of repo to add to or update in cache]')
547def CMDpopulate(parser, args):
548 """Ensure that the cache has all up-to-date objects for the given repo."""
549 parser.add_option('--depth', type='int',
550 help='Only cache DEPTH commits of history')
551 parser.add_option('--shallow', '-s', action='store_true',
552 help='Only cache 10000 commits of history')
553 parser.add_option('--ref', action='append',
554 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000555 parser.add_option('--no_bootstrap', '--no-bootstrap',
556 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000557 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000558 parser.add_option('--ignore_locks', '--ignore-locks',
559 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000560 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000561
agable@chromium.org5a306a22014-02-24 22:13:59 +0000562 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000563 if not len(args) == 1:
564 parser.error('git cache populate only takes exactly one repo url.')
565 url = args[0]
566
szager@chromium.org848fd492014-04-09 19:06:44 +0000567 mirror = Mirror(url, refs=options.ref)
568 kwargs = {
569 'verbose': options.verbose,
570 'shallow': options.shallow,
571 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000572 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000573 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000574 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000575 kwargs['depth'] = options.depth
576 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000577
578
szager@chromium.orgf3145112014-08-07 21:02:36 +0000579@subcommand.usage('Fetch new commits into cache and current checkout')
580def CMDfetch(parser, args):
581 """Update mirror, and fetch in cwd."""
582 parser.add_option('--all', action='store_true', help='Fetch all remotes')
583 options, args = parser.parse_args(args)
584
585 # Figure out which remotes to fetch. This mimics the behavior of regular
586 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
587 # this will NOT try to traverse up the branching structure to find the
588 # ultimate remote to update.
589 remotes = []
590 if options.all:
591 assert not args, 'fatal: fetch --all does not take a repository argument'
592 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
593 elif args:
594 remotes = args
595 else:
596 current_branch = subprocess.check_output(
597 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
598 if current_branch != 'HEAD':
599 upstream = subprocess.check_output(
600 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
601 ).strip()
602 if upstream and upstream != '.':
603 remotes = [upstream]
604 if not remotes:
605 remotes = ['origin']
606
607 cachepath = Mirror.GetCachePath()
608 git_dir = os.path.abspath(subprocess.check_output(
609 [Mirror.git_exe, 'rev-parse', '--git-dir']))
610 git_dir = os.path.abspath(git_dir)
611 if git_dir.startswith(cachepath):
612 mirror = Mirror.FromPath(git_dir)
613 mirror.populate()
614 return 0
615 for remote in remotes:
616 remote_url = subprocess.check_output(
617 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
618 if remote_url.startswith(cachepath):
619 mirror = Mirror.FromPath(remote_url)
620 mirror.print = lambda *args: None
621 print('Updating git cache...')
622 mirror.populate()
623 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
624 return 0
625
626
agable@chromium.org5a306a22014-02-24 22:13:59 +0000627@subcommand.usage('[url of repo to unlock, or -a|--all]')
628def CMDunlock(parser, args):
629 """Unlock one or all repos if their lock files are still around."""
630 parser.add_option('--force', '-f', action='store_true',
631 help='Actually perform the action')
632 parser.add_option('--all', '-a', action='store_true',
633 help='Unlock all repository caches')
634 options, args = parser.parse_args(args)
635 if len(args) > 1 or (len(args) == 0 and not options.all):
636 parser.error('git cache unlock takes exactly one repo url, or --all')
637
agable@chromium.org5a306a22014-02-24 22:13:59 +0000638 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000639 cachepath = Mirror.GetCachePath()
640 lockfiles = [os.path.join(cachepath, path)
641 for path in os.listdir(cachepath)
642 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000643 parser.error('git cache unlock requires -f|--force to do anything. '
644 'Refusing to unlock the following repo caches: '
645 ', '.join(lockfiles))
646
szager@chromium.org848fd492014-04-09 19:06:44 +0000647 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000648 if options.all:
649 unlocked_repos.extend(Mirror.UnlockAll())
650 else:
651 m = Mirror(args[0])
652 if m.unlock():
653 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000654
szager@chromium.org848fd492014-04-09 19:06:44 +0000655 if unlocked_repos:
656 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
657 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000658
659
660class OptionParser(optparse.OptionParser):
661 """Wrapper class for OptionParser to handle global options."""
662
663 def __init__(self, *args, **kwargs):
664 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
665 self.add_option('-c', '--cache-dir',
666 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000667 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000668 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000669 self.add_option('-q', '--quiet', action='store_true',
670 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000671
672 def parse_args(self, args=None, values=None):
673 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000674 if options.quiet:
675 options.verbose = 0
676
677 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
678 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000679
680 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000681 global_cache_dir = Mirror.GetCachePath()
682 except RuntimeError:
683 global_cache_dir = None
684 if options.cache_dir:
685 if global_cache_dir and (
686 os.path.abspath(options.cache_dir) !=
687 os.path.abspath(global_cache_dir)):
688 logging.warn('Overriding globally-configured cache directory.')
689 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000690
agable@chromium.org5a306a22014-02-24 22:13:59 +0000691 return options, args
692
693
694def main(argv):
695 dispatcher = subcommand.CommandDispatcher(__name__)
696 return dispatcher.execute(OptionParser(), argv)
697
698
699if __name__ == '__main__':
700 sys.exit(main(sys.argv[1:]))