blob: 753ccde34a46c70c24c9748827311ff0138dbc0e [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
szager@chromium.orgdbb6f822016-02-02 22:59:30 +000047 def __init__(self, path, timeout=0):
agable@chromium.org5a306a22014-02-24 22:13:59 +000048 self.path = os.path.abspath(path)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +000049 self.timeout = timeout
agable@chromium.org5a306a22014-02-24 22:13:59 +000050 self.lockfile = self.path + ".lock"
51 self.pid = os.getpid()
52
53 def _read_pid(self):
54 """Read the pid stored in the lockfile.
55
56 Note: This method is potentially racy. By the time it returns the lockfile
57 may have been unlocked, removed, or stolen by some other process.
58 """
59 try:
60 with open(self.lockfile, 'r') as f:
61 pid = int(f.readline().strip())
62 except (IOError, ValueError):
63 pid = None
64 return pid
65
66 def _make_lockfile(self):
67 """Safely creates a lockfile containing the current pid."""
68 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
69 fd = os.open(self.lockfile, open_flags, 0o644)
70 f = os.fdopen(fd, 'w')
szager@chromium.org848fd492014-04-09 19:06:44 +000071 print(self.pid, file=f)
agable@chromium.org5a306a22014-02-24 22:13:59 +000072 f.close()
73
74 def _remove_lockfile(self):
pgervais@chromium.orgf3726102014-04-17 17:24:15 +000075 """Delete the lockfile. Complains (implicitly) if it doesn't exist.
76
77 See gclient_utils.py:rmtree docstring for more explanation on the
78 windows case.
79 """
80 if sys.platform == 'win32':
81 lockfile = os.path.normcase(self.lockfile)
82 for _ in xrange(3):
83 exitcode = subprocess.call(['cmd.exe', '/c',
84 'del', '/f', '/q', lockfile])
85 if exitcode == 0:
86 return
87 time.sleep(3)
88 raise LockError('Failed to remove lock: %s' % lockfile)
89 else:
90 os.remove(self.lockfile)
agable@chromium.org5a306a22014-02-24 22:13:59 +000091
92 def lock(self):
93 """Acquire the lock.
94
szager@chromium.orgdbb6f822016-02-02 22:59:30 +000095 This will block with a deadline of self.timeout seconds.
agable@chromium.org5a306a22014-02-24 22:13:59 +000096 """
szager@chromium.orgdbb6f822016-02-02 22:59:30 +000097 elapsed = 0
98 while True:
99 try:
100 self._make_lockfile()
101 return
102 except OSError as e:
103 if elapsed < self.timeout:
nodir@chromium.org5b48e482016-03-18 20:27:54 +0000104 sleep_time = max(10, min(3, self.timeout - elapsed))
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000105 logging.info('Could not create git cache lockfile; '
106 'will retry after sleep(%d).', sleep_time);
107 elapsed += sleep_time
108 time.sleep(sleep_time)
109 continue
110 if e.errno == errno.EEXIST:
111 raise LockError("%s is already locked" % self.path)
112 else:
113 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000114
115 def unlock(self):
116 """Release the lock."""
szager@chromium.org108eced2014-06-19 21:22:43 +0000117 try:
118 if not self.is_locked():
119 raise LockError("%s is not locked" % self.path)
120 if not self.i_am_locking():
121 raise LockError("%s is locked, but not by me" % self.path)
122 self._remove_lockfile()
123 except WinErr:
124 # Windows is unreliable when it comes to file locking. YMMV.
125 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +0000126
127 def break_lock(self):
128 """Remove the lock, even if it was created by someone else."""
129 try:
130 self._remove_lockfile()
131 return True
132 except OSError as exc:
133 if exc.errno == errno.ENOENT:
134 return False
135 else:
136 raise
137
138 def is_locked(self):
139 """Test if the file is locked by anyone.
140
141 Note: This method is potentially racy. By the time it returns the lockfile
142 may have been unlocked, removed, or stolen by some other process.
143 """
144 return os.path.exists(self.lockfile)
145
146 def i_am_locking(self):
147 """Test if the file is locked by this process."""
148 return self.is_locked() and self.pid == self._read_pid()
149
agable@chromium.org5a306a22014-02-24 22:13:59 +0000150
szager@chromium.org848fd492014-04-09 19:06:44 +0000151class Mirror(object):
152
153 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
154 gsutil_exe = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +0000155 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
szager@chromium.org1132f5f2014-08-23 01:57:59 +0000156 cachepath_lock = threading.Lock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000157
szager@chromium.org66c8b852015-09-22 23:19:07 +0000158 @staticmethod
159 def parse_fetch_spec(spec):
160 """Parses and canonicalizes a fetch spec.
161
162 Returns (fetchspec, value_regex), where value_regex can be used
163 with 'git config --replace-all'.
164 """
165 parts = spec.split(':', 1)
166 src = parts[0].lstrip('+').rstrip('/')
167 if not src.startswith('refs/'):
168 src = 'refs/heads/%s' % src
169 dest = parts[1].rstrip('/') if len(parts) > 1 else src
170 regex = r'\+%s:.*' % src.replace('*', r'\*')
171 return ('+%s:%s' % (src, dest), regex)
172
szager@chromium.org848fd492014-04-09 19:06:44 +0000173 def __init__(self, url, refs=None, print_func=None):
174 self.url = url
szager@chromium.org66c8b852015-09-22 23:19:07 +0000175 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
szager@chromium.org848fd492014-04-09 19:06:44 +0000176 self.basedir = self.UrlToCacheDir(url)
177 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000178 if print_func:
179 self.print = self.print_without_file
180 self.print_func = print_func
181 else:
182 self.print = print
183
184 def print_without_file(self, message, **kwargs):
185 self.print_func(message)
szager@chromium.org848fd492014-04-09 19:06:44 +0000186
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000187 @property
188 def bootstrap_bucket(self):
189 if 'chrome-internal' in self.url:
190 return 'chrome-git-cache'
191 else:
192 return 'chromium-git-cache'
193
szager@chromium.org174766f2014-05-13 21:27:46 +0000194 @classmethod
195 def FromPath(cls, path):
196 return cls(cls.CacheDirToUrl(path))
197
szager@chromium.org848fd492014-04-09 19:06:44 +0000198 @staticmethod
199 def UrlToCacheDir(url):
200 """Convert a git url to a normalized form for the cache dir path."""
201 parsed = urlparse.urlparse(url)
202 norm_url = parsed.netloc + parsed.path
203 if norm_url.endswith('.git'):
204 norm_url = norm_url[:-len('.git')]
205 return norm_url.replace('-', '--').replace('/', '-').lower()
206
207 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000208 def CacheDirToUrl(path):
209 """Convert a cache dir path to its corresponding url."""
210 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
211 return 'https://%s' % netpath
212
szager@chromium.org848fd492014-04-09 19:06:44 +0000213 @classmethod
214 def SetCachePath(cls, cachepath):
szager@chromium.org84c56002014-08-23 03:33:28 +0000215 with cls.cachepath_lock:
216 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000217
218 @classmethod
219 def GetCachePath(cls):
szager@chromium.org84c56002014-08-23 03:33:28 +0000220 with cls.cachepath_lock:
221 if not hasattr(cls, 'cachepath'):
222 try:
223 cachepath = subprocess.check_output(
224 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
225 except subprocess.CalledProcessError:
226 cachepath = None
227 if not cachepath:
228 raise RuntimeError(
229 'No global cache.cachepath git configuration found.')
230 setattr(cls, 'cachepath', cachepath)
231 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000232
233 def RunGit(self, cmd, **kwargs):
234 """Run git in a subprocess."""
235 cwd = kwargs.setdefault('cwd', self.mirror_path)
236 kwargs.setdefault('print_stdout', False)
237 kwargs.setdefault('filter_fn', self.print)
238 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
239 env.setdefault('GIT_ASKPASS', 'true')
240 env.setdefault('SSH_ASKPASS', 'true')
241 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
242 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
243
244 def config(self, cwd=None):
245 if cwd is None:
246 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000247
248 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000249 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000250
251 # Don't combine pack files into one big pack file. It's really slow for
252 # repositories, and there's no way to track progress and make sure it's
253 # not stuck.
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000254 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000255
256 # Allocate more RAM for cache-ing delta chains, for better performance
257 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000258 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000259 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000260
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000261 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000262 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000263 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000264 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000265 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000266 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000267 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000268
269 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000270 """Bootstrap the repo from Google Stroage if possible.
271
272 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
273 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000274
hinoka@google.com776a2c32014-04-25 07:54:25 +0000275 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000276 if (sys.platform.startswith('win') and
277 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000278 python_fallback = True
279 elif sys.platform.startswith('darwin'):
280 # The OSX version of unzip doesn't support zip64.
281 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000282 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000283 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000284
285 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000286 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000287 # Get the most recent version of the zipfile.
288 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
289 ls_out_sorted = sorted(ls_out.splitlines())
290 if not ls_out_sorted:
291 # This repo is not on Google Storage.
292 return False
293 latest_checkout = ls_out_sorted[-1]
294
295 # Download zip file to a temporary directory.
296 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000297 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000298 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000299 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000300 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000301 return False
302 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
303
hinoka@google.com776a2c32014-04-25 07:54:25 +0000304 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
305 if not python_fallback:
306 if sys.platform.startswith('win'):
307 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
308 else:
309 cmd = ['unzip', filename, '-d', directory]
310 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000311 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000312 try:
313 with zipfile.ZipFile(filename, 'r') as f:
314 f.printdir()
315 f.extractall(directory)
316 except Exception as e:
317 self.print('Encountered error: %s' % str(e), file=sys.stderr)
318 retcode = 1
319 else:
320 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000321 finally:
322 # Clean up the downloaded zipfile.
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000323 gclient_utils.rm_file_or_tree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000324
325 if retcode:
326 self.print(
327 'Extracting bootstrap zipfile %s failed.\n'
328 'Resuming normal operations.' % filename)
329 return False
330 return True
331
332 def exists(self):
333 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
334
szager@chromium.org66c8b852015-09-22 23:19:07 +0000335 def _preserve_fetchspec(self):
336 """Read and preserve remote.origin.fetch from an existing mirror.
337
338 This modifies self.fetch_specs.
339 """
340 if not self.exists():
341 return
342 try:
343 config_fetchspecs = subprocess.check_output(
344 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
345 cwd=self.mirror_path)
346 for fetchspec in config_fetchspecs.splitlines():
347 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
348 except subprocess.CalledProcessError:
349 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
350 'existing cache directory. You may need to manually edit '
351 '%s and "git cache fetch" again.'
352 % os.path.join(self.mirror_path, 'config'))
353
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000354 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
355 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000356 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
357 pack_files = []
358
359 if os.path.isdir(pack_dir):
360 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
361
362 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000363 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000364 len(pack_files) > GC_AUTOPACKLIMIT)
365 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000366 if self.exists():
367 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
368 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000369 tempdir = tempfile.mkdtemp(
370 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
371 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
372 if bootstrapped:
373 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000374 gclient_utils.rmtree(self.mirror_path)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000375 elif not self.exists():
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000376 # Bootstrap failed, no previous cache; start with a bare git dir.
377 self.RunGit(['init', '--bare'], cwd=tempdir)
378 else:
379 # Bootstrap failed, previous cache exists; warn and continue.
380 logging.warn(
381 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
382 'but failed. Continuing with non-optimized repository.'
383 % len(pack_files))
384 gclient_utils.rmtree(tempdir)
385 tempdir = None
386 else:
387 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
388 logging.warn(
389 'Shallow fetch requested, but repo cache already exists.')
390 return tempdir
391
392 def _fetch(self, rundir, verbose, depth):
393 self.config(rundir)
394 v = []
395 d = []
396 if verbose:
397 v = ['-v', '--progress']
398 if depth:
399 d = ['--depth', str(depth)]
400 fetch_cmd = ['fetch'] + v + d + ['origin']
401 fetch_specs = subprocess.check_output(
402 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
403 cwd=rundir).strip().splitlines()
404 for spec in fetch_specs:
405 try:
406 self.print('Fetching %s' % spec)
407 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
408 except subprocess.CalledProcessError:
409 if spec == '+refs/heads/*:refs/heads/*':
410 raise RefsHeadsFailedToFetch
411 logging.warn('Fetch of %s failed' % spec)
412
szager@chromium.org848fd492014-04-09 19:06:44 +0000413 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000414 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000415 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000416 if shallow and not depth:
417 depth = 10000
418 gclient_utils.safe_makedirs(self.GetCachePath())
419
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000420 lockfile = Lockfile(self.mirror_path, lock_timeout)
szager@chromium.org108eced2014-06-19 21:22:43 +0000421 if not ignore_lock:
422 lockfile.lock()
423
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000424 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000425 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000426 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000427 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000428 self._fetch(rundir, verbose, depth)
429 except RefsHeadsFailedToFetch:
430 # This is a major failure, we need to clean and force a bootstrap.
431 gclient_utils.rmtree(rundir)
432 self.print(GIT_CACHE_CORRUPT_MESSAGE)
433 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
434 assert tempdir
435 self._fetch(tempdir or self.mirror_path, verbose, depth)
436 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000437 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000438 try:
hinoka@chromium.orga5cda1e2014-08-24 12:09:13 +0000439 if os.path.exists(self.mirror_path):
440 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000441 os.rename(tempdir, self.mirror_path)
442 except OSError as e:
443 # This is somehow racy on Windows.
444 # Catching OSError because WindowsError isn't portable and
445 # pylint complains.
446 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
447 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000448 if not ignore_lock:
449 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000450
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000451 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000452 # The files are named <git number>.zip
453 gen_number = subprocess.check_output(
454 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000455 # Run Garbage Collect to compress packfile.
456 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000457 # Creating a temp file and then deleting it ensures we can use this name.
458 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
459 os.remove(tmp_zipfile)
460 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
461 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000462 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
463 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000464 gsutil.call('cp', tmp_zipfile, dest_name)
465 os.remove(tmp_zipfile)
466
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000467 # Remove all other files in the same directory.
468 if prune:
469 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
470 for filename in ls_out.splitlines():
471 if filename == dest_name:
472 continue
473 gsutil.call('rm', filename)
474
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000475 @staticmethod
476 def DeleteTmpPackFiles(path):
477 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000478 if not os.path.isdir(pack_dir):
479 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000480 pack_files = [f for f in os.listdir(pack_dir) if
481 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
482 for f in pack_files:
483 f = os.path.join(pack_dir, f)
484 try:
485 os.remove(f)
486 logging.warn('Deleted stale temporary pack file %s' % f)
487 except OSError:
488 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000489
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000490 @classmethod
491 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000492 did_unlock = False
493 lf = Lockfile(path)
494 if lf.break_lock():
495 did_unlock = True
496 # Look for lock files that might have been left behind by an interrupted
497 # git process.
498 lf = os.path.join(path, 'config.lock')
499 if os.path.exists(lf):
500 os.remove(lf)
501 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000502 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000503 return did_unlock
504
szager@chromium.org848fd492014-04-09 19:06:44 +0000505 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000506 return self.BreakLocks(self.mirror_path)
507
508 @classmethod
509 def UnlockAll(cls):
510 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000511 if not cachepath:
512 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000513 dirlist = os.listdir(cachepath)
514 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
515 if os.path.isdir(os.path.join(cachepath, path))])
516 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000517 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000518 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000519 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000520 os.path.isfile(os.path.join(cachepath, dirent))):
521 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
522
523 unlocked_repos = []
524 for repo_dir in repo_dirs:
525 if cls.BreakLocks(repo_dir):
526 unlocked_repos.append(repo_dir)
527
528 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000529
agable@chromium.org5a306a22014-02-24 22:13:59 +0000530@subcommand.usage('[url of repo to check for caching]')
531def CMDexists(parser, args):
532 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000533 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000534 if not len(args) == 1:
535 parser.error('git cache exists only takes exactly one repo url.')
536 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000537 mirror = Mirror(url)
538 if mirror.exists():
539 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000540 return 0
541 return 1
542
543
hinoka@google.com563559c2014-04-02 00:36:24 +0000544@subcommand.usage('[url of repo to create a bootstrap zip file]')
545def CMDupdate_bootstrap(parser, args):
546 """Create and uploads a bootstrap tarball."""
547 # Lets just assert we can't do this on Windows.
548 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000549 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000550 return 1
551
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000552 parser.add_option('--prune', action='store_true',
553 help='Prune all other cached zipballs of the same repo.')
554
hinoka@google.com563559c2014-04-02 00:36:24 +0000555 # First, we need to ensure the cache is populated.
556 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000557 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000558 CMDpopulate(parser, populate_args)
559
560 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000561 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000562 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000563 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000564 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000565 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000566
567
agable@chromium.org5a306a22014-02-24 22:13:59 +0000568@subcommand.usage('[url of repo to add to or update in cache]')
569def CMDpopulate(parser, args):
570 """Ensure that the cache has all up-to-date objects for the given repo."""
571 parser.add_option('--depth', type='int',
572 help='Only cache DEPTH commits of history')
573 parser.add_option('--shallow', '-s', action='store_true',
574 help='Only cache 10000 commits of history')
575 parser.add_option('--ref', action='append',
576 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000577 parser.add_option('--no_bootstrap', '--no-bootstrap',
578 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000579 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000580 parser.add_option('--ignore_locks', '--ignore-locks',
581 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000582 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000583
agable@chromium.org5a306a22014-02-24 22:13:59 +0000584 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000585 if not len(args) == 1:
586 parser.error('git cache populate only takes exactly one repo url.')
587 url = args[0]
588
szager@chromium.org848fd492014-04-09 19:06:44 +0000589 mirror = Mirror(url, refs=options.ref)
590 kwargs = {
591 'verbose': options.verbose,
592 'shallow': options.shallow,
593 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000594 'ignore_lock': options.ignore_locks,
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000595 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000596 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000597 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000598 kwargs['depth'] = options.depth
599 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000600
601
szager@chromium.orgf3145112014-08-07 21:02:36 +0000602@subcommand.usage('Fetch new commits into cache and current checkout')
603def CMDfetch(parser, args):
604 """Update mirror, and fetch in cwd."""
605 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000606 parser.add_option('--no_bootstrap', '--no-bootstrap',
607 action='store_true',
608 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000609 options, args = parser.parse_args(args)
610
611 # Figure out which remotes to fetch. This mimics the behavior of regular
612 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
613 # this will NOT try to traverse up the branching structure to find the
614 # ultimate remote to update.
615 remotes = []
616 if options.all:
617 assert not args, 'fatal: fetch --all does not take a repository argument'
618 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
619 elif args:
620 remotes = args
621 else:
622 current_branch = subprocess.check_output(
623 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
624 if current_branch != 'HEAD':
625 upstream = subprocess.check_output(
626 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
627 ).strip()
628 if upstream and upstream != '.':
629 remotes = [upstream]
630 if not remotes:
631 remotes = ['origin']
632
633 cachepath = Mirror.GetCachePath()
634 git_dir = os.path.abspath(subprocess.check_output(
635 [Mirror.git_exe, 'rev-parse', '--git-dir']))
636 git_dir = os.path.abspath(git_dir)
637 if git_dir.startswith(cachepath):
638 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000639 mirror.populate(
640 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000641 return 0
642 for remote in remotes:
643 remote_url = subprocess.check_output(
644 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
645 if remote_url.startswith(cachepath):
646 mirror = Mirror.FromPath(remote_url)
647 mirror.print = lambda *args: None
648 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000649 mirror.populate(
650 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000651 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
652 return 0
653
654
agable@chromium.org5a306a22014-02-24 22:13:59 +0000655@subcommand.usage('[url of repo to unlock, or -a|--all]')
656def CMDunlock(parser, args):
657 """Unlock one or all repos if their lock files are still around."""
658 parser.add_option('--force', '-f', action='store_true',
659 help='Actually perform the action')
660 parser.add_option('--all', '-a', action='store_true',
661 help='Unlock all repository caches')
662 options, args = parser.parse_args(args)
663 if len(args) > 1 or (len(args) == 0 and not options.all):
664 parser.error('git cache unlock takes exactly one repo url, or --all')
665
agable@chromium.org5a306a22014-02-24 22:13:59 +0000666 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000667 cachepath = Mirror.GetCachePath()
668 lockfiles = [os.path.join(cachepath, path)
669 for path in os.listdir(cachepath)
670 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000671 parser.error('git cache unlock requires -f|--force to do anything. '
672 'Refusing to unlock the following repo caches: '
673 ', '.join(lockfiles))
674
szager@chromium.org848fd492014-04-09 19:06:44 +0000675 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000676 if options.all:
677 unlocked_repos.extend(Mirror.UnlockAll())
678 else:
679 m = Mirror(args[0])
680 if m.unlock():
681 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000682
szager@chromium.org848fd492014-04-09 19:06:44 +0000683 if unlocked_repos:
684 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
685 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000686
687
688class OptionParser(optparse.OptionParser):
689 """Wrapper class for OptionParser to handle global options."""
690
691 def __init__(self, *args, **kwargs):
692 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
693 self.add_option('-c', '--cache-dir',
694 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000695 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000696 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000697 self.add_option('-q', '--quiet', action='store_true',
698 help='Suppress all extraneous output')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000699 self.add_option('--timeout', type='int', default=0,
700 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000701
702 def parse_args(self, args=None, values=None):
703 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000704 if options.quiet:
705 options.verbose = 0
706
707 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
708 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000709
710 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000711 global_cache_dir = Mirror.GetCachePath()
712 except RuntimeError:
713 global_cache_dir = None
714 if options.cache_dir:
715 if global_cache_dir and (
716 os.path.abspath(options.cache_dir) !=
717 os.path.abspath(global_cache_dir)):
718 logging.warn('Overriding globally-configured cache directory.')
719 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000720
agable@chromium.org5a306a22014-02-24 22:13:59 +0000721 return options, args
722
723
724def main(argv):
725 dispatcher = subcommand.CommandDispatcher(__name__)
726 return dispatcher.execute(OptionParser(), argv)
727
728
729if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000730 try:
731 sys.exit(main(sys.argv[1:]))
732 except KeyboardInterrupt:
733 sys.stderr.write('interrupted\n')
734 sys.exit(1)