blob: 9e3eaec9fd2a67a3d0988954da03a6fd6200c94f [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.orge877e7b2015-06-09 20:35:48 +0000222 env = os.environ.copy()
223 env['GIT_DIR'] = cwd
szager@chromium.org301a7c32014-06-16 17:13:50 +0000224
225 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
szager@chromium.orge877e7b2015-06-09 20:35:48 +0000226 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd, env=env)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000227
228 # Don't combine pack files into one big pack file. It's really slow for
229 # repositories, and there's no way to track progress and make sure it's
230 # not stuck.
szager@chromium.orge877e7b2015-06-09 20:35:48 +0000231 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd, env=env)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000232
233 # Allocate more RAM for cache-ing delta chains, for better performance
234 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000235 self.RunGit(['config', 'core.deltaBaseCacheLimit',
szager@chromium.orge877e7b2015-06-09 20:35:48 +0000236 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd, env=env)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000237
szager@chromium.orge877e7b2015-06-09 20:35:48 +0000238 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd, env=env)
szager@chromium.org848fd492014-04-09 19:06:44 +0000239 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
szager@chromium.orge877e7b2015-06-09 20:35:48 +0000240 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'],
241 cwd=cwd, env=env)
szager@chromium.org848fd492014-04-09 19:06:44 +0000242 for ref in self.refs:
243 ref = ref.lstrip('+').rstrip('/')
244 if ref.startswith('refs/'):
245 refspec = '+%s:%s' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000246 regex = r'\+%s:.*' % ref.replace('*', r'\*')
szager@chromium.org848fd492014-04-09 19:06:44 +0000247 else:
248 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000249 regex = r'\+refs/heads/%s:.*' % ref.replace('*', r'\*')
250 self.RunGit(
251 ['config', '--replace-all', 'remote.origin.fetch', refspec, regex],
szager@chromium.orge877e7b2015-06-09 20:35:48 +0000252 cwd=cwd, env=env)
szager@chromium.org848fd492014-04-09 19:06:44 +0000253
254 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000255 """Bootstrap the repo from Google Stroage if possible.
256
257 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
258 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000259
hinoka@google.com776a2c32014-04-25 07:54:25 +0000260 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000261 if (sys.platform.startswith('win') and
262 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000263 python_fallback = True
264 elif sys.platform.startswith('darwin'):
265 # The OSX version of unzip doesn't support zip64.
266 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000267 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000268 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000269
270 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000271 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000272 # Get the most recent version of the zipfile.
273 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
274 ls_out_sorted = sorted(ls_out.splitlines())
275 if not ls_out_sorted:
276 # This repo is not on Google Storage.
277 return False
278 latest_checkout = ls_out_sorted[-1]
279
280 # Download zip file to a temporary directory.
281 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000282 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000283 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000284 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000285 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000286 return False
287 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
288
hinoka@google.com776a2c32014-04-25 07:54:25 +0000289 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
290 if not python_fallback:
291 if sys.platform.startswith('win'):
292 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
293 else:
294 cmd = ['unzip', filename, '-d', directory]
295 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000296 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000297 try:
298 with zipfile.ZipFile(filename, 'r') as f:
299 f.printdir()
300 f.extractall(directory)
301 except Exception as e:
302 self.print('Encountered error: %s' % str(e), file=sys.stderr)
303 retcode = 1
304 else:
305 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000306 finally:
307 # Clean up the downloaded zipfile.
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000308 gclient_utils.rm_file_or_tree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000309
310 if retcode:
311 self.print(
312 'Extracting bootstrap zipfile %s failed.\n'
313 'Resuming normal operations.' % filename)
314 return False
315 return True
316
317 def exists(self):
318 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
319
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000320 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
321 tempdir = None
322 config_file = os.path.join(self.mirror_path, 'config')
323 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
324 pack_files = []
325
326 if os.path.isdir(pack_dir):
327 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
328
329 should_bootstrap = (force or
330 not os.path.exists(config_file) or
331 len(pack_files) > GC_AUTOPACKLIMIT)
332 if should_bootstrap:
333 tempdir = tempfile.mkdtemp(
334 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
335 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
336 if bootstrapped:
337 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000338 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000339 elif not os.path.exists(config_file):
340 # Bootstrap failed, no previous cache; start with a bare git dir.
341 self.RunGit(['init', '--bare'], cwd=tempdir)
342 else:
343 # Bootstrap failed, previous cache exists; warn and continue.
344 logging.warn(
345 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
346 'but failed. Continuing with non-optimized repository.'
347 % len(pack_files))
348 gclient_utils.rmtree(tempdir)
349 tempdir = None
350 else:
351 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
352 logging.warn(
353 'Shallow fetch requested, but repo cache already exists.')
354 return tempdir
355
356 def _fetch(self, rundir, verbose, depth):
357 self.config(rundir)
358 v = []
359 d = []
360 if verbose:
361 v = ['-v', '--progress']
362 if depth:
363 d = ['--depth', str(depth)]
364 fetch_cmd = ['fetch'] + v + d + ['origin']
365 fetch_specs = subprocess.check_output(
366 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
367 cwd=rundir).strip().splitlines()
368 for spec in fetch_specs:
369 try:
370 self.print('Fetching %s' % spec)
371 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
372 except subprocess.CalledProcessError:
373 if spec == '+refs/heads/*:refs/heads/*':
374 raise RefsHeadsFailedToFetch
375 logging.warn('Fetch of %s failed' % spec)
376
szager@chromium.org848fd492014-04-09 19:06:44 +0000377 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.org108eced2014-06-19 21:22:43 +0000378 verbose=False, ignore_lock=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000379 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000380 if shallow and not depth:
381 depth = 10000
382 gclient_utils.safe_makedirs(self.GetCachePath())
383
szager@chromium.org108eced2014-06-19 21:22:43 +0000384 lockfile = Lockfile(self.mirror_path)
385 if not ignore_lock:
386 lockfile.lock()
387
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000388 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000389 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000390 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000391 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000392 self._fetch(rundir, verbose, depth)
393 except RefsHeadsFailedToFetch:
394 # This is a major failure, we need to clean and force a bootstrap.
395 gclient_utils.rmtree(rundir)
396 self.print(GIT_CACHE_CORRUPT_MESSAGE)
397 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
398 assert tempdir
399 self._fetch(tempdir or self.mirror_path, verbose, depth)
400 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000401 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000402 try:
hinoka@chromium.orga5cda1e2014-08-24 12:09:13 +0000403 if os.path.exists(self.mirror_path):
404 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000405 os.rename(tempdir, self.mirror_path)
406 except OSError as e:
407 # This is somehow racy on Windows.
408 # Catching OSError because WindowsError isn't portable and
409 # pylint complains.
410 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
411 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000412 if not ignore_lock:
413 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000414
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000415 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000416 # The files are named <git number>.zip
417 gen_number = subprocess.check_output(
418 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000419 # Run Garbage Collect to compress packfile.
420 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000421 # Creating a temp file and then deleting it ensures we can use this name.
422 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
423 os.remove(tmp_zipfile)
424 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
425 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000426 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
427 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000428 gsutil.call('cp', tmp_zipfile, dest_name)
429 os.remove(tmp_zipfile)
430
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000431 # Remove all other files in the same directory.
432 if prune:
433 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
434 for filename in ls_out.splitlines():
435 if filename == dest_name:
436 continue
437 gsutil.call('rm', filename)
438
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000439 @staticmethod
440 def DeleteTmpPackFiles(path):
441 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000442 if not os.path.isdir(pack_dir):
443 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000444 pack_files = [f for f in os.listdir(pack_dir) if
445 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
446 for f in pack_files:
447 f = os.path.join(pack_dir, f)
448 try:
449 os.remove(f)
450 logging.warn('Deleted stale temporary pack file %s' % f)
451 except OSError:
452 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000453
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000454 @classmethod
455 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000456 did_unlock = False
457 lf = Lockfile(path)
458 if lf.break_lock():
459 did_unlock = True
460 # Look for lock files that might have been left behind by an interrupted
461 # git process.
462 lf = os.path.join(path, 'config.lock')
463 if os.path.exists(lf):
464 os.remove(lf)
465 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000466 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000467 return did_unlock
468
szager@chromium.org848fd492014-04-09 19:06:44 +0000469 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000470 return self.BreakLocks(self.mirror_path)
471
472 @classmethod
473 def UnlockAll(cls):
474 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000475 if not cachepath:
476 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000477 dirlist = os.listdir(cachepath)
478 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
479 if os.path.isdir(os.path.join(cachepath, path))])
480 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000481 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000482 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000483 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000484 os.path.isfile(os.path.join(cachepath, dirent))):
485 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
486
487 unlocked_repos = []
488 for repo_dir in repo_dirs:
489 if cls.BreakLocks(repo_dir):
490 unlocked_repos.append(repo_dir)
491
492 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000493
agable@chromium.org5a306a22014-02-24 22:13:59 +0000494@subcommand.usage('[url of repo to check for caching]')
495def CMDexists(parser, args):
496 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000497 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000498 if not len(args) == 1:
499 parser.error('git cache exists only takes exactly one repo url.')
500 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000501 mirror = Mirror(url)
502 if mirror.exists():
503 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000504 return 0
505 return 1
506
507
hinoka@google.com563559c2014-04-02 00:36:24 +0000508@subcommand.usage('[url of repo to create a bootstrap zip file]')
509def CMDupdate_bootstrap(parser, args):
510 """Create and uploads a bootstrap tarball."""
511 # Lets just assert we can't do this on Windows.
512 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000513 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000514 return 1
515
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000516 parser.add_option('--prune', action='store_true',
517 help='Prune all other cached zipballs of the same repo.')
518
hinoka@google.com563559c2014-04-02 00:36:24 +0000519 # First, we need to ensure the cache is populated.
520 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000521 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000522 CMDpopulate(parser, populate_args)
523
524 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000525 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000526 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000527 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000528 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000529 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000530
531
agable@chromium.org5a306a22014-02-24 22:13:59 +0000532@subcommand.usage('[url of repo to add to or update in cache]')
533def CMDpopulate(parser, args):
534 """Ensure that the cache has all up-to-date objects for the given repo."""
535 parser.add_option('--depth', type='int',
536 help='Only cache DEPTH commits of history')
537 parser.add_option('--shallow', '-s', action='store_true',
538 help='Only cache 10000 commits of history')
539 parser.add_option('--ref', action='append',
540 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000541 parser.add_option('--no_bootstrap', '--no-bootstrap',
542 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000543 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000544 parser.add_option('--ignore_locks', '--ignore-locks',
545 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000546 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000547
agable@chromium.org5a306a22014-02-24 22:13:59 +0000548 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000549 if not len(args) == 1:
550 parser.error('git cache populate only takes exactly one repo url.')
551 url = args[0]
552
szager@chromium.org848fd492014-04-09 19:06:44 +0000553 mirror = Mirror(url, refs=options.ref)
554 kwargs = {
555 'verbose': options.verbose,
556 'shallow': options.shallow,
557 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000558 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000559 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000560 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000561 kwargs['depth'] = options.depth
562 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000563
564
szager@chromium.orgf3145112014-08-07 21:02:36 +0000565@subcommand.usage('Fetch new commits into cache and current checkout')
566def CMDfetch(parser, args):
567 """Update mirror, and fetch in cwd."""
568 parser.add_option('--all', action='store_true', help='Fetch all remotes')
569 options, args = parser.parse_args(args)
570
571 # Figure out which remotes to fetch. This mimics the behavior of regular
572 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
573 # this will NOT try to traverse up the branching structure to find the
574 # ultimate remote to update.
575 remotes = []
576 if options.all:
577 assert not args, 'fatal: fetch --all does not take a repository argument'
578 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
579 elif args:
580 remotes = args
581 else:
582 current_branch = subprocess.check_output(
583 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
584 if current_branch != 'HEAD':
585 upstream = subprocess.check_output(
586 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
587 ).strip()
588 if upstream and upstream != '.':
589 remotes = [upstream]
590 if not remotes:
591 remotes = ['origin']
592
593 cachepath = Mirror.GetCachePath()
594 git_dir = os.path.abspath(subprocess.check_output(
595 [Mirror.git_exe, 'rev-parse', '--git-dir']))
596 git_dir = os.path.abspath(git_dir)
597 if git_dir.startswith(cachepath):
598 mirror = Mirror.FromPath(git_dir)
599 mirror.populate()
600 return 0
601 for remote in remotes:
602 remote_url = subprocess.check_output(
603 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
604 if remote_url.startswith(cachepath):
605 mirror = Mirror.FromPath(remote_url)
606 mirror.print = lambda *args: None
607 print('Updating git cache...')
608 mirror.populate()
609 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
610 return 0
611
612
agable@chromium.org5a306a22014-02-24 22:13:59 +0000613@subcommand.usage('[url of repo to unlock, or -a|--all]')
614def CMDunlock(parser, args):
615 """Unlock one or all repos if their lock files are still around."""
616 parser.add_option('--force', '-f', action='store_true',
617 help='Actually perform the action')
618 parser.add_option('--all', '-a', action='store_true',
619 help='Unlock all repository caches')
620 options, args = parser.parse_args(args)
621 if len(args) > 1 or (len(args) == 0 and not options.all):
622 parser.error('git cache unlock takes exactly one repo url, or --all')
623
agable@chromium.org5a306a22014-02-24 22:13:59 +0000624 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000625 cachepath = Mirror.GetCachePath()
626 lockfiles = [os.path.join(cachepath, path)
627 for path in os.listdir(cachepath)
628 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000629 parser.error('git cache unlock requires -f|--force to do anything. '
630 'Refusing to unlock the following repo caches: '
631 ', '.join(lockfiles))
632
szager@chromium.org848fd492014-04-09 19:06:44 +0000633 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000634 if options.all:
635 unlocked_repos.extend(Mirror.UnlockAll())
636 else:
637 m = Mirror(args[0])
638 if m.unlock():
639 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000640
szager@chromium.org848fd492014-04-09 19:06:44 +0000641 if unlocked_repos:
642 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
643 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000644
645
646class OptionParser(optparse.OptionParser):
647 """Wrapper class for OptionParser to handle global options."""
648
649 def __init__(self, *args, **kwargs):
650 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
651 self.add_option('-c', '--cache-dir',
652 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000653 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000654 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000655 self.add_option('-q', '--quiet', action='store_true',
656 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000657
658 def parse_args(self, args=None, values=None):
659 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000660 if options.quiet:
661 options.verbose = 0
662
663 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
664 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000665
666 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000667 global_cache_dir = Mirror.GetCachePath()
668 except RuntimeError:
669 global_cache_dir = None
670 if options.cache_dir:
671 if global_cache_dir and (
672 os.path.abspath(options.cache_dir) !=
673 os.path.abspath(global_cache_dir)):
674 logging.warn('Overriding globally-configured cache directory.')
675 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000676
agable@chromium.org5a306a22014-02-24 22:13:59 +0000677 return options, args
678
679
680def main(argv):
681 dispatcher = subcommand.CommandDispatcher(__name__)
682 return dispatcher.execute(OptionParser(), argv)
683
684
685if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000686 try:
687 sys.exit(main(sys.argv[1:]))
688 except KeyboardInterrupt:
689 sys.stderr.write('interrupted\n')
690 sys.exit(1)