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