blob: cb7f7e30dfcd55bd563fb51f92c4b753501c789b [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)
153 self.print = print_func or print
154
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000155 @property
156 def bootstrap_bucket(self):
157 if 'chrome-internal' in self.url:
158 return 'chrome-git-cache'
159 else:
160 return 'chromium-git-cache'
161
szager@chromium.org174766f2014-05-13 21:27:46 +0000162 @classmethod
163 def FromPath(cls, path):
164 return cls(cls.CacheDirToUrl(path))
165
szager@chromium.org848fd492014-04-09 19:06:44 +0000166 @staticmethod
167 def UrlToCacheDir(url):
168 """Convert a git url to a normalized form for the cache dir path."""
169 parsed = urlparse.urlparse(url)
170 norm_url = parsed.netloc + parsed.path
171 if norm_url.endswith('.git'):
172 norm_url = norm_url[:-len('.git')]
173 return norm_url.replace('-', '--').replace('/', '-').lower()
174
175 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000176 def CacheDirToUrl(path):
177 """Convert a cache dir path to its corresponding url."""
178 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
179 return 'https://%s' % netpath
180
181 @staticmethod
szager@chromium.org848fd492014-04-09 19:06:44 +0000182 def FindExecutable(executable):
183 """This mimics the "which" utility."""
184 path_folders = os.environ.get('PATH').split(os.pathsep)
185
186 for path_folder in path_folders:
187 target = os.path.join(path_folder, executable)
188 # Just incase we have some ~/blah paths.
189 target = os.path.abspath(os.path.expanduser(target))
190 if os.path.isfile(target) and os.access(target, os.X_OK):
191 return target
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000192 if sys.platform.startswith('win'):
193 for suffix in ('.bat', '.cmd', '.exe'):
194 alt_target = target + suffix
szager@chromium.org4039b312014-04-09 21:56:46 +0000195 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
szager@chromium.org6b5faf52014-04-09 21:54:21 +0000196 return alt_target
szager@chromium.org848fd492014-04-09 19:06:44 +0000197 return None
198
199 @classmethod
200 def SetCachePath(cls, cachepath):
szager@chromium.org84c56002014-08-23 03:33:28 +0000201 with cls.cachepath_lock:
202 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000203
204 @classmethod
205 def GetCachePath(cls):
szager@chromium.org84c56002014-08-23 03:33:28 +0000206 with cls.cachepath_lock:
207 if not hasattr(cls, 'cachepath'):
208 try:
209 cachepath = subprocess.check_output(
210 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
211 except subprocess.CalledProcessError:
212 cachepath = None
213 if not cachepath:
214 raise RuntimeError(
215 'No global cache.cachepath git configuration found.')
216 setattr(cls, 'cachepath', cachepath)
217 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000218
219 def RunGit(self, cmd, **kwargs):
220 """Run git in a subprocess."""
221 cwd = kwargs.setdefault('cwd', self.mirror_path)
222 kwargs.setdefault('print_stdout', False)
223 kwargs.setdefault('filter_fn', self.print)
224 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
225 env.setdefault('GIT_ASKPASS', 'true')
226 env.setdefault('SSH_ASKPASS', 'true')
227 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
228 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
229
230 def config(self, cwd=None):
231 if cwd is None:
232 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000233
234 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
235 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
236
237 # Don't combine pack files into one big pack file. It's really slow for
238 # repositories, and there's no way to track progress and make sure it's
239 # not stuck.
240 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
241
242 # Allocate more RAM for cache-ing delta chains, for better performance
243 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000244 self.RunGit(['config', 'core.deltaBaseCacheLimit',
245 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000246
szager@chromium.org848fd492014-04-09 19:06:44 +0000247 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
248 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
szager@chromium.org965c44f2014-08-19 21:19:19 +0000249 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000250 for ref in self.refs:
251 ref = ref.lstrip('+').rstrip('/')
252 if ref.startswith('refs/'):
253 refspec = '+%s:%s' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000254 regex = r'\+%s:.*' % ref.replace('*', r'\*')
szager@chromium.org848fd492014-04-09 19:06:44 +0000255 else:
256 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
szager@chromium.org965c44f2014-08-19 21:19:19 +0000257 regex = r'\+refs/heads/%s:.*' % ref.replace('*', r'\*')
258 self.RunGit(
259 ['config', '--replace-all', 'remote.origin.fetch', refspec, regex],
260 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000261
262 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000263 """Bootstrap the repo from Google Stroage if possible.
264
265 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
266 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000267
hinoka@google.com776a2c32014-04-25 07:54:25 +0000268 python_fallback = False
269 if sys.platform.startswith('win') and not self.FindExecutable('7z'):
270 python_fallback = True
271 elif sys.platform.startswith('darwin'):
272 # The OSX version of unzip doesn't support zip64.
273 python_fallback = True
274 elif not self.FindExecutable('unzip'):
275 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000276
277 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000278 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000279 # Get the most recent version of the zipfile.
280 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
281 ls_out_sorted = sorted(ls_out.splitlines())
282 if not ls_out_sorted:
283 # This repo is not on Google Storage.
284 return False
285 latest_checkout = ls_out_sorted[-1]
286
287 # Download zip file to a temporary directory.
288 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000289 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000290 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000291 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000292 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000293 return False
294 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
295
hinoka@google.com776a2c32014-04-25 07:54:25 +0000296 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
297 if not python_fallback:
298 if sys.platform.startswith('win'):
299 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
300 else:
301 cmd = ['unzip', filename, '-d', directory]
302 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000303 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000304 try:
305 with zipfile.ZipFile(filename, 'r') as f:
306 f.printdir()
307 f.extractall(directory)
308 except Exception as e:
309 self.print('Encountered error: %s' % str(e), file=sys.stderr)
310 retcode = 1
311 else:
312 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000313 finally:
314 # Clean up the downloaded zipfile.
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000315 gclient_utils.rm_file_or_tree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000316
317 if retcode:
318 self.print(
319 'Extracting bootstrap zipfile %s failed.\n'
320 'Resuming normal operations.' % filename)
321 return False
322 return True
323
324 def exists(self):
325 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
326
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000327 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
328 tempdir = None
329 config_file = os.path.join(self.mirror_path, 'config')
330 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
331 pack_files = []
332
333 if os.path.isdir(pack_dir):
334 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
335
336 should_bootstrap = (force or
337 not os.path.exists(config_file) or
338 len(pack_files) > GC_AUTOPACKLIMIT)
339 if should_bootstrap:
340 tempdir = tempfile.mkdtemp(
341 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
342 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
343 if bootstrapped:
344 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000345 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000346 elif not os.path.exists(config_file):
347 # Bootstrap failed, no previous cache; start with a bare git dir.
348 self.RunGit(['init', '--bare'], cwd=tempdir)
349 else:
350 # Bootstrap failed, previous cache exists; warn and continue.
351 logging.warn(
352 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
353 'but failed. Continuing with non-optimized repository.'
354 % len(pack_files))
355 gclient_utils.rmtree(tempdir)
356 tempdir = None
357 else:
358 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
359 logging.warn(
360 'Shallow fetch requested, but repo cache already exists.')
361 return tempdir
362
363 def _fetch(self, rundir, verbose, depth):
364 self.config(rundir)
365 v = []
366 d = []
367 if verbose:
368 v = ['-v', '--progress']
369 if depth:
370 d = ['--depth', str(depth)]
371 fetch_cmd = ['fetch'] + v + d + ['origin']
372 fetch_specs = subprocess.check_output(
373 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
374 cwd=rundir).strip().splitlines()
375 for spec in fetch_specs:
376 try:
377 self.print('Fetching %s' % spec)
378 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
379 except subprocess.CalledProcessError:
380 if spec == '+refs/heads/*:refs/heads/*':
381 raise RefsHeadsFailedToFetch
382 logging.warn('Fetch of %s failed' % spec)
383
szager@chromium.org848fd492014-04-09 19:06:44 +0000384 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.org108eced2014-06-19 21:22:43 +0000385 verbose=False, ignore_lock=False):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000386 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000387 if shallow and not depth:
388 depth = 10000
389 gclient_utils.safe_makedirs(self.GetCachePath())
390
szager@chromium.org108eced2014-06-19 21:22:43 +0000391 lockfile = Lockfile(self.mirror_path)
392 if not ignore_lock:
393 lockfile.lock()
394
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000395 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000396 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000397 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000398 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000399 self._fetch(rundir, verbose, depth)
400 except RefsHeadsFailedToFetch:
401 # This is a major failure, we need to clean and force a bootstrap.
402 gclient_utils.rmtree(rundir)
403 self.print(GIT_CACHE_CORRUPT_MESSAGE)
404 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
405 assert tempdir
406 self._fetch(tempdir or self.mirror_path, verbose, depth)
407 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000408 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000409 try:
hinoka@chromium.orga5cda1e2014-08-24 12:09:13 +0000410 if os.path.exists(self.mirror_path):
411 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000412 os.rename(tempdir, self.mirror_path)
413 except OSError as e:
414 # This is somehow racy on Windows.
415 # Catching OSError because WindowsError isn't portable and
416 # pylint complains.
417 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
418 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000419 if not ignore_lock:
420 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000421
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000422 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000423 # The files are named <git number>.zip
424 gen_number = subprocess.check_output(
425 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000426 # Run Garbage Collect to compress packfile.
427 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000428 # Creating a temp file and then deleting it ensures we can use this name.
429 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
430 os.remove(tmp_zipfile)
431 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
432 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000433 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
434 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000435 gsutil.call('cp', tmp_zipfile, dest_name)
436 os.remove(tmp_zipfile)
437
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000438 # Remove all other files in the same directory.
439 if prune:
440 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
441 for filename in ls_out.splitlines():
442 if filename == dest_name:
443 continue
444 gsutil.call('rm', filename)
445
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000446 @staticmethod
447 def DeleteTmpPackFiles(path):
448 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000449 if not os.path.isdir(pack_dir):
450 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000451 pack_files = [f for f in os.listdir(pack_dir) if
452 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
453 for f in pack_files:
454 f = os.path.join(pack_dir, f)
455 try:
456 os.remove(f)
457 logging.warn('Deleted stale temporary pack file %s' % f)
458 except OSError:
459 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000460
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000461 @classmethod
462 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000463 did_unlock = False
464 lf = Lockfile(path)
465 if lf.break_lock():
466 did_unlock = True
467 # Look for lock files that might have been left behind by an interrupted
468 # git process.
469 lf = os.path.join(path, 'config.lock')
470 if os.path.exists(lf):
471 os.remove(lf)
472 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000473 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000474 return did_unlock
475
szager@chromium.org848fd492014-04-09 19:06:44 +0000476 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000477 return self.BreakLocks(self.mirror_path)
478
479 @classmethod
480 def UnlockAll(cls):
481 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000482 if not cachepath:
483 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000484 dirlist = os.listdir(cachepath)
485 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
486 if os.path.isdir(os.path.join(cachepath, path))])
487 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000488 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000489 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000490 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000491 os.path.isfile(os.path.join(cachepath, dirent))):
492 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
493
494 unlocked_repos = []
495 for repo_dir in repo_dirs:
496 if cls.BreakLocks(repo_dir):
497 unlocked_repos.append(repo_dir)
498
499 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000500
agable@chromium.org5a306a22014-02-24 22:13:59 +0000501@subcommand.usage('[url of repo to check for caching]')
502def CMDexists(parser, args):
503 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000504 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000505 if not len(args) == 1:
506 parser.error('git cache exists only takes exactly one repo url.')
507 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000508 mirror = Mirror(url)
509 if mirror.exists():
510 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000511 return 0
512 return 1
513
514
hinoka@google.com563559c2014-04-02 00:36:24 +0000515@subcommand.usage('[url of repo to create a bootstrap zip file]')
516def CMDupdate_bootstrap(parser, args):
517 """Create and uploads a bootstrap tarball."""
518 # Lets just assert we can't do this on Windows.
519 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000520 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000521 return 1
522
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000523 parser.add_option('--prune', action='store_true',
524 help='Prune all other cached zipballs of the same repo.')
525
hinoka@google.com563559c2014-04-02 00:36:24 +0000526 # First, we need to ensure the cache is populated.
527 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000528 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000529 CMDpopulate(parser, populate_args)
530
531 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000532 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000533 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000534 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000535 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000536 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000537
538
agable@chromium.org5a306a22014-02-24 22:13:59 +0000539@subcommand.usage('[url of repo to add to or update in cache]')
540def CMDpopulate(parser, args):
541 """Ensure that the cache has all up-to-date objects for the given repo."""
542 parser.add_option('--depth', type='int',
543 help='Only cache DEPTH commits of history')
544 parser.add_option('--shallow', '-s', action='store_true',
545 help='Only cache 10000 commits of history')
546 parser.add_option('--ref', action='append',
547 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000548 parser.add_option('--no_bootstrap', '--no-bootstrap',
549 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000550 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000551 parser.add_option('--ignore_locks', '--ignore-locks',
552 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000553 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000554
agable@chromium.org5a306a22014-02-24 22:13:59 +0000555 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000556 if not len(args) == 1:
557 parser.error('git cache populate only takes exactly one repo url.')
558 url = args[0]
559
szager@chromium.org848fd492014-04-09 19:06:44 +0000560 mirror = Mirror(url, refs=options.ref)
561 kwargs = {
562 'verbose': options.verbose,
563 'shallow': options.shallow,
564 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000565 'ignore_lock': options.ignore_locks,
szager@chromium.org848fd492014-04-09 19:06:44 +0000566 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000567 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000568 kwargs['depth'] = options.depth
569 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000570
571
szager@chromium.orgf3145112014-08-07 21:02:36 +0000572@subcommand.usage('Fetch new commits into cache and current checkout')
573def CMDfetch(parser, args):
574 """Update mirror, and fetch in cwd."""
575 parser.add_option('--all', action='store_true', help='Fetch all remotes')
576 options, args = parser.parse_args(args)
577
578 # Figure out which remotes to fetch. This mimics the behavior of regular
579 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
580 # this will NOT try to traverse up the branching structure to find the
581 # ultimate remote to update.
582 remotes = []
583 if options.all:
584 assert not args, 'fatal: fetch --all does not take a repository argument'
585 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
586 elif args:
587 remotes = args
588 else:
589 current_branch = subprocess.check_output(
590 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
591 if current_branch != 'HEAD':
592 upstream = subprocess.check_output(
593 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
594 ).strip()
595 if upstream and upstream != '.':
596 remotes = [upstream]
597 if not remotes:
598 remotes = ['origin']
599
600 cachepath = Mirror.GetCachePath()
601 git_dir = os.path.abspath(subprocess.check_output(
602 [Mirror.git_exe, 'rev-parse', '--git-dir']))
603 git_dir = os.path.abspath(git_dir)
604 if git_dir.startswith(cachepath):
605 mirror = Mirror.FromPath(git_dir)
606 mirror.populate()
607 return 0
608 for remote in remotes:
609 remote_url = subprocess.check_output(
610 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
611 if remote_url.startswith(cachepath):
612 mirror = Mirror.FromPath(remote_url)
613 mirror.print = lambda *args: None
614 print('Updating git cache...')
615 mirror.populate()
616 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
617 return 0
618
619
agable@chromium.org5a306a22014-02-24 22:13:59 +0000620@subcommand.usage('[url of repo to unlock, or -a|--all]')
621def CMDunlock(parser, args):
622 """Unlock one or all repos if their lock files are still around."""
623 parser.add_option('--force', '-f', action='store_true',
624 help='Actually perform the action')
625 parser.add_option('--all', '-a', action='store_true',
626 help='Unlock all repository caches')
627 options, args = parser.parse_args(args)
628 if len(args) > 1 or (len(args) == 0 and not options.all):
629 parser.error('git cache unlock takes exactly one repo url, or --all')
630
agable@chromium.org5a306a22014-02-24 22:13:59 +0000631 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000632 cachepath = Mirror.GetCachePath()
633 lockfiles = [os.path.join(cachepath, path)
634 for path in os.listdir(cachepath)
635 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000636 parser.error('git cache unlock requires -f|--force to do anything. '
637 'Refusing to unlock the following repo caches: '
638 ', '.join(lockfiles))
639
szager@chromium.org848fd492014-04-09 19:06:44 +0000640 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000641 if options.all:
642 unlocked_repos.extend(Mirror.UnlockAll())
643 else:
644 m = Mirror(args[0])
645 if m.unlock():
646 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000647
szager@chromium.org848fd492014-04-09 19:06:44 +0000648 if unlocked_repos:
649 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
650 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000651
652
653class OptionParser(optparse.OptionParser):
654 """Wrapper class for OptionParser to handle global options."""
655
656 def __init__(self, *args, **kwargs):
657 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
658 self.add_option('-c', '--cache-dir',
659 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000660 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000661 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000662 self.add_option('-q', '--quiet', action='store_true',
663 help='Suppress all extraneous output')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000664
665 def parse_args(self, args=None, values=None):
666 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000667 if options.quiet:
668 options.verbose = 0
669
670 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
671 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000672
673 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000674 global_cache_dir = Mirror.GetCachePath()
675 except RuntimeError:
676 global_cache_dir = None
677 if options.cache_dir:
678 if global_cache_dir and (
679 os.path.abspath(options.cache_dir) !=
680 os.path.abspath(global_cache_dir)):
681 logging.warn('Overriding globally-configured cache directory.')
682 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000683
agable@chromium.org5a306a22014-02-24 22:13:59 +0000684 return options, args
685
686
687def main(argv):
688 dispatcher = subcommand.CommandDispatcher(__name__)
689 return dispatcher.execute(OptionParser(), argv)
690
691
692if __name__ == '__main__':
693 sys.exit(main(sys.argv[1:]))