blob: 9137ef5f58e27cc95a0e0a701693d61af364cba4 [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.
96 If self.timeout is zero, this is a NON-BLOCKING FAIL-FAST operation.
agable@chromium.org5a306a22014-02-24 22:13:59 +000097 """
szager@chromium.orgdbb6f822016-02-02 22:59:30 +000098 elapsed = 0
99 while True:
100 try:
101 self._make_lockfile()
102 return
103 except OSError as e:
104 if elapsed < self.timeout:
105 sleep_time = min(3, self.timeout - elapsed)
106 logging.info('Could not create git cache lockfile; '
107 'will retry after sleep(%d).', sleep_time);
108 elapsed += sleep_time
109 time.sleep(sleep_time)
110 continue
111 if e.errno == errno.EEXIST:
112 raise LockError("%s is already locked" % self.path)
113 else:
114 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000115
116 def unlock(self):
117 """Release the lock."""
szager@chromium.org108eced2014-06-19 21:22:43 +0000118 try:
119 if not self.is_locked():
120 raise LockError("%s is not locked" % self.path)
121 if not self.i_am_locking():
122 raise LockError("%s is locked, but not by me" % self.path)
123 self._remove_lockfile()
124 except WinErr:
125 # Windows is unreliable when it comes to file locking. YMMV.
126 pass
agable@chromium.org5a306a22014-02-24 22:13:59 +0000127
128 def break_lock(self):
129 """Remove the lock, even if it was created by someone else."""
130 try:
131 self._remove_lockfile()
132 return True
133 except OSError as exc:
134 if exc.errno == errno.ENOENT:
135 return False
136 else:
137 raise
138
139 def is_locked(self):
140 """Test if the file is locked by anyone.
141
142 Note: This method is potentially racy. By the time it returns the lockfile
143 may have been unlocked, removed, or stolen by some other process.
144 """
145 return os.path.exists(self.lockfile)
146
147 def i_am_locking(self):
148 """Test if the file is locked by this process."""
149 return self.is_locked() and self.pid == self._read_pid()
150
agable@chromium.org5a306a22014-02-24 22:13:59 +0000151
szager@chromium.org848fd492014-04-09 19:06:44 +0000152class Mirror(object):
153
154 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
155 gsutil_exe = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +0000156 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
szager@chromium.org1132f5f2014-08-23 01:57:59 +0000157 cachepath_lock = threading.Lock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000158
szager@chromium.org66c8b852015-09-22 23:19:07 +0000159 @staticmethod
160 def parse_fetch_spec(spec):
161 """Parses and canonicalizes a fetch spec.
162
163 Returns (fetchspec, value_regex), where value_regex can be used
164 with 'git config --replace-all'.
165 """
166 parts = spec.split(':', 1)
167 src = parts[0].lstrip('+').rstrip('/')
168 if not src.startswith('refs/'):
169 src = 'refs/heads/%s' % src
170 dest = parts[1].rstrip('/') if len(parts) > 1 else src
171 regex = r'\+%s:.*' % src.replace('*', r'\*')
172 return ('+%s:%s' % (src, dest), regex)
173
szager@chromium.org848fd492014-04-09 19:06:44 +0000174 def __init__(self, url, refs=None, print_func=None):
175 self.url = url
szager@chromium.org66c8b852015-09-22 23:19:07 +0000176 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
szager@chromium.org848fd492014-04-09 19:06:44 +0000177 self.basedir = self.UrlToCacheDir(url)
178 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
loislo@chromium.org0fb693f2014-12-25 15:28:22 +0000179 if print_func:
180 self.print = self.print_without_file
181 self.print_func = print_func
182 else:
183 self.print = print
184
185 def print_without_file(self, message, **kwargs):
186 self.print_func(message)
szager@chromium.org848fd492014-04-09 19:06:44 +0000187
hinoka@chromium.orgf8fa23d2014-06-05 01:00:04 +0000188 @property
189 def bootstrap_bucket(self):
190 if 'chrome-internal' in self.url:
191 return 'chrome-git-cache'
192 else:
193 return 'chromium-git-cache'
194
szager@chromium.org174766f2014-05-13 21:27:46 +0000195 @classmethod
196 def FromPath(cls, path):
197 return cls(cls.CacheDirToUrl(path))
198
szager@chromium.org848fd492014-04-09 19:06:44 +0000199 @staticmethod
200 def UrlToCacheDir(url):
201 """Convert a git url to a normalized form for the cache dir path."""
202 parsed = urlparse.urlparse(url)
203 norm_url = parsed.netloc + parsed.path
204 if norm_url.endswith('.git'):
205 norm_url = norm_url[:-len('.git')]
206 return norm_url.replace('-', '--').replace('/', '-').lower()
207
208 @staticmethod
szager@chromium.org174766f2014-05-13 21:27:46 +0000209 def CacheDirToUrl(path):
210 """Convert a cache dir path to its corresponding url."""
211 netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
212 return 'https://%s' % netpath
213
szager@chromium.org848fd492014-04-09 19:06:44 +0000214 @classmethod
215 def SetCachePath(cls, cachepath):
szager@chromium.org84c56002014-08-23 03:33:28 +0000216 with cls.cachepath_lock:
217 setattr(cls, 'cachepath', cachepath)
szager@chromium.org848fd492014-04-09 19:06:44 +0000218
219 @classmethod
220 def GetCachePath(cls):
szager@chromium.org84c56002014-08-23 03:33:28 +0000221 with cls.cachepath_lock:
222 if not hasattr(cls, 'cachepath'):
223 try:
224 cachepath = subprocess.check_output(
225 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
226 except subprocess.CalledProcessError:
227 cachepath = None
228 if not cachepath:
229 raise RuntimeError(
230 'No global cache.cachepath git configuration found.')
231 setattr(cls, 'cachepath', cachepath)
232 return getattr(cls, 'cachepath')
szager@chromium.org848fd492014-04-09 19:06:44 +0000233
234 def RunGit(self, cmd, **kwargs):
235 """Run git in a subprocess."""
236 cwd = kwargs.setdefault('cwd', self.mirror_path)
237 kwargs.setdefault('print_stdout', False)
238 kwargs.setdefault('filter_fn', self.print)
239 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
240 env.setdefault('GIT_ASKPASS', 'true')
241 env.setdefault('SSH_ASKPASS', 'true')
242 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
243 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
244
245 def config(self, cwd=None):
246 if cwd is None:
247 cwd = self.mirror_path
szager@chromium.org301a7c32014-06-16 17:13:50 +0000248
249 # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000250 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000251
252 # Don't combine pack files into one big pack file. It's really slow for
253 # repositories, and there's no way to track progress and make sure it's
254 # not stuck.
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000255 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000256
257 # Allocate more RAM for cache-ing delta chains, for better performance
258 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000259 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000260 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000261
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000262 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000263 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000264 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000265 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000266 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000267 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000268 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000269
270 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000271 """Bootstrap the repo from Google Stroage if possible.
272
273 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
274 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000275
hinoka@google.com776a2c32014-04-25 07:54:25 +0000276 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000277 if (sys.platform.startswith('win') and
278 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000279 python_fallback = True
280 elif sys.platform.startswith('darwin'):
281 # The OSX version of unzip doesn't support zip64.
282 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000283 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000284 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000285
286 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000287 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000288 # Get the most recent version of the zipfile.
289 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
290 ls_out_sorted = sorted(ls_out.splitlines())
291 if not ls_out_sorted:
292 # This repo is not on Google Storage.
293 return False
294 latest_checkout = ls_out_sorted[-1]
295
296 # Download zip file to a temporary directory.
297 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000298 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000299 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000300 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000301 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000302 return False
303 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
304
hinoka@google.com776a2c32014-04-25 07:54:25 +0000305 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
306 if not python_fallback:
307 if sys.platform.startswith('win'):
308 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
309 else:
310 cmd = ['unzip', filename, '-d', directory]
311 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000312 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000313 try:
314 with zipfile.ZipFile(filename, 'r') as f:
315 f.printdir()
316 f.extractall(directory)
317 except Exception as e:
318 self.print('Encountered error: %s' % str(e), file=sys.stderr)
319 retcode = 1
320 else:
321 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000322 finally:
323 # Clean up the downloaded zipfile.
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000324 gclient_utils.rm_file_or_tree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000325
326 if retcode:
327 self.print(
328 'Extracting bootstrap zipfile %s failed.\n'
329 'Resuming normal operations.' % filename)
330 return False
331 return True
332
333 def exists(self):
334 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
335
szager@chromium.org66c8b852015-09-22 23:19:07 +0000336 def _preserve_fetchspec(self):
337 """Read and preserve remote.origin.fetch from an existing mirror.
338
339 This modifies self.fetch_specs.
340 """
341 if not self.exists():
342 return
343 try:
344 config_fetchspecs = subprocess.check_output(
345 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
346 cwd=self.mirror_path)
347 for fetchspec in config_fetchspecs.splitlines():
348 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
349 except subprocess.CalledProcessError:
350 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
351 'existing cache directory. You may need to manually edit '
352 '%s and "git cache fetch" again.'
353 % os.path.join(self.mirror_path, 'config'))
354
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000355 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
356 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000357 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
358 pack_files = []
359
360 if os.path.isdir(pack_dir):
361 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
362
363 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000364 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000365 len(pack_files) > GC_AUTOPACKLIMIT)
366 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000367 if self.exists():
368 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
369 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000370 tempdir = tempfile.mkdtemp(
371 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
372 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
373 if bootstrapped:
374 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000375 gclient_utils.rmtree(self.mirror_path)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000376 elif not self.exists():
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000377 # Bootstrap failed, no previous cache; start with a bare git dir.
378 self.RunGit(['init', '--bare'], cwd=tempdir)
379 else:
380 # Bootstrap failed, previous cache exists; warn and continue.
381 logging.warn(
382 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
383 'but failed. Continuing with non-optimized repository.'
384 % len(pack_files))
385 gclient_utils.rmtree(tempdir)
386 tempdir = None
387 else:
388 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
389 logging.warn(
390 'Shallow fetch requested, but repo cache already exists.')
391 return tempdir
392
393 def _fetch(self, rundir, verbose, depth):
394 self.config(rundir)
395 v = []
396 d = []
397 if verbose:
398 v = ['-v', '--progress']
399 if depth:
400 d = ['--depth', str(depth)]
401 fetch_cmd = ['fetch'] + v + d + ['origin']
402 fetch_specs = subprocess.check_output(
403 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
404 cwd=rundir).strip().splitlines()
405 for spec in fetch_specs:
406 try:
407 self.print('Fetching %s' % spec)
408 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
409 except subprocess.CalledProcessError:
410 if spec == '+refs/heads/*:refs/heads/*':
411 raise RefsHeadsFailedToFetch
412 logging.warn('Fetch of %s failed' % spec)
413
szager@chromium.org848fd492014-04-09 19:06:44 +0000414 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000415 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000416 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000417 if shallow and not depth:
418 depth = 10000
419 gclient_utils.safe_makedirs(self.GetCachePath())
420
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000421 lockfile = Lockfile(self.mirror_path, lock_timeout)
szager@chromium.org108eced2014-06-19 21:22:43 +0000422 if not ignore_lock:
423 lockfile.lock()
424
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000425 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000426 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000427 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000428 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000429 self._fetch(rundir, verbose, depth)
430 except RefsHeadsFailedToFetch:
431 # This is a major failure, we need to clean and force a bootstrap.
432 gclient_utils.rmtree(rundir)
433 self.print(GIT_CACHE_CORRUPT_MESSAGE)
434 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
435 assert tempdir
436 self._fetch(tempdir or self.mirror_path, verbose, depth)
437 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000438 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000439 try:
hinoka@chromium.orga5cda1e2014-08-24 12:09:13 +0000440 if os.path.exists(self.mirror_path):
441 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000442 os.rename(tempdir, self.mirror_path)
443 except OSError as e:
444 # This is somehow racy on Windows.
445 # Catching OSError because WindowsError isn't portable and
446 # pylint complains.
447 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
448 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000449 if not ignore_lock:
450 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000451
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000452 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000453 # The files are named <git number>.zip
454 gen_number = subprocess.check_output(
455 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000456 # Run Garbage Collect to compress packfile.
457 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000458 # Creating a temp file and then deleting it ensures we can use this name.
459 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
460 os.remove(tmp_zipfile)
461 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
462 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000463 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
464 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000465 gsutil.call('cp', tmp_zipfile, dest_name)
466 os.remove(tmp_zipfile)
467
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000468 # Remove all other files in the same directory.
469 if prune:
470 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
471 for filename in ls_out.splitlines():
472 if filename == dest_name:
473 continue
474 gsutil.call('rm', filename)
475
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000476 @staticmethod
477 def DeleteTmpPackFiles(path):
478 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000479 if not os.path.isdir(pack_dir):
480 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000481 pack_files = [f for f in os.listdir(pack_dir) if
482 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
483 for f in pack_files:
484 f = os.path.join(pack_dir, f)
485 try:
486 os.remove(f)
487 logging.warn('Deleted stale temporary pack file %s' % f)
488 except OSError:
489 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000490
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000491 @classmethod
492 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000493 did_unlock = False
494 lf = Lockfile(path)
495 if lf.break_lock():
496 did_unlock = True
497 # Look for lock files that might have been left behind by an interrupted
498 # git process.
499 lf = os.path.join(path, 'config.lock')
500 if os.path.exists(lf):
501 os.remove(lf)
502 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000503 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000504 return did_unlock
505
szager@chromium.org848fd492014-04-09 19:06:44 +0000506 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000507 return self.BreakLocks(self.mirror_path)
508
509 @classmethod
510 def UnlockAll(cls):
511 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000512 if not cachepath:
513 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000514 dirlist = os.listdir(cachepath)
515 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
516 if os.path.isdir(os.path.join(cachepath, path))])
517 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000518 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000519 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000520 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000521 os.path.isfile(os.path.join(cachepath, dirent))):
522 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
523
524 unlocked_repos = []
525 for repo_dir in repo_dirs:
526 if cls.BreakLocks(repo_dir):
527 unlocked_repos.append(repo_dir)
528
529 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000530
agable@chromium.org5a306a22014-02-24 22:13:59 +0000531@subcommand.usage('[url of repo to check for caching]')
532def CMDexists(parser, args):
533 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000534 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000535 if not len(args) == 1:
536 parser.error('git cache exists only takes exactly one repo url.')
537 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000538 mirror = Mirror(url)
539 if mirror.exists():
540 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000541 return 0
542 return 1
543
544
hinoka@google.com563559c2014-04-02 00:36:24 +0000545@subcommand.usage('[url of repo to create a bootstrap zip file]')
546def CMDupdate_bootstrap(parser, args):
547 """Create and uploads a bootstrap tarball."""
548 # Lets just assert we can't do this on Windows.
549 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000550 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000551 return 1
552
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000553 parser.add_option('--prune', action='store_true',
554 help='Prune all other cached zipballs of the same repo.')
555
hinoka@google.com563559c2014-04-02 00:36:24 +0000556 # First, we need to ensure the cache is populated.
557 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000558 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000559 CMDpopulate(parser, populate_args)
560
561 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000562 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000563 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000564 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000565 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000566 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000567
568
agable@chromium.org5a306a22014-02-24 22:13:59 +0000569@subcommand.usage('[url of repo to add to or update in cache]')
570def CMDpopulate(parser, args):
571 """Ensure that the cache has all up-to-date objects for the given repo."""
572 parser.add_option('--depth', type='int',
573 help='Only cache DEPTH commits of history')
574 parser.add_option('--shallow', '-s', action='store_true',
575 help='Only cache 10000 commits of history')
576 parser.add_option('--ref', action='append',
577 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000578 parser.add_option('--no_bootstrap', '--no-bootstrap',
579 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000580 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000581 parser.add_option('--ignore_locks', '--ignore-locks',
582 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000583 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000584
agable@chromium.org5a306a22014-02-24 22:13:59 +0000585 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000586 if not len(args) == 1:
587 parser.error('git cache populate only takes exactly one repo url.')
588 url = args[0]
589
szager@chromium.org848fd492014-04-09 19:06:44 +0000590 mirror = Mirror(url, refs=options.ref)
591 kwargs = {
592 'verbose': options.verbose,
593 'shallow': options.shallow,
594 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000595 'ignore_lock': options.ignore_locks,
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000596 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000597 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000598 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000599 kwargs['depth'] = options.depth
600 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000601
602
szager@chromium.orgf3145112014-08-07 21:02:36 +0000603@subcommand.usage('Fetch new commits into cache and current checkout')
604def CMDfetch(parser, args):
605 """Update mirror, and fetch in cwd."""
606 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000607 parser.add_option('--no_bootstrap', '--no-bootstrap',
608 action='store_true',
609 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000610 options, args = parser.parse_args(args)
611
612 # Figure out which remotes to fetch. This mimics the behavior of regular
613 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
614 # this will NOT try to traverse up the branching structure to find the
615 # ultimate remote to update.
616 remotes = []
617 if options.all:
618 assert not args, 'fatal: fetch --all does not take a repository argument'
619 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
620 elif args:
621 remotes = args
622 else:
623 current_branch = subprocess.check_output(
624 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
625 if current_branch != 'HEAD':
626 upstream = subprocess.check_output(
627 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
628 ).strip()
629 if upstream and upstream != '.':
630 remotes = [upstream]
631 if not remotes:
632 remotes = ['origin']
633
634 cachepath = Mirror.GetCachePath()
635 git_dir = os.path.abspath(subprocess.check_output(
636 [Mirror.git_exe, 'rev-parse', '--git-dir']))
637 git_dir = os.path.abspath(git_dir)
638 if git_dir.startswith(cachepath):
639 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000640 mirror.populate(
641 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000642 return 0
643 for remote in remotes:
644 remote_url = subprocess.check_output(
645 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
646 if remote_url.startswith(cachepath):
647 mirror = Mirror.FromPath(remote_url)
648 mirror.print = lambda *args: None
649 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000650 mirror.populate(
651 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000652 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
653 return 0
654
655
agable@chromium.org5a306a22014-02-24 22:13:59 +0000656@subcommand.usage('[url of repo to unlock, or -a|--all]')
657def CMDunlock(parser, args):
658 """Unlock one or all repos if their lock files are still around."""
659 parser.add_option('--force', '-f', action='store_true',
660 help='Actually perform the action')
661 parser.add_option('--all', '-a', action='store_true',
662 help='Unlock all repository caches')
663 options, args = parser.parse_args(args)
664 if len(args) > 1 or (len(args) == 0 and not options.all):
665 parser.error('git cache unlock takes exactly one repo url, or --all')
666
agable@chromium.org5a306a22014-02-24 22:13:59 +0000667 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000668 cachepath = Mirror.GetCachePath()
669 lockfiles = [os.path.join(cachepath, path)
670 for path in os.listdir(cachepath)
671 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000672 parser.error('git cache unlock requires -f|--force to do anything. '
673 'Refusing to unlock the following repo caches: '
674 ', '.join(lockfiles))
675
szager@chromium.org848fd492014-04-09 19:06:44 +0000676 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000677 if options.all:
678 unlocked_repos.extend(Mirror.UnlockAll())
679 else:
680 m = Mirror(args[0])
681 if m.unlock():
682 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000683
szager@chromium.org848fd492014-04-09 19:06:44 +0000684 if unlocked_repos:
685 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
686 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000687
688
689class OptionParser(optparse.OptionParser):
690 """Wrapper class for OptionParser to handle global options."""
691
692 def __init__(self, *args, **kwargs):
693 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
694 self.add_option('-c', '--cache-dir',
695 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000696 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000697 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000698 self.add_option('-q', '--quiet', action='store_true',
699 help='Suppress all extraneous output')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000700 self.add_option('--timeout', type='int', default=0,
701 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000702
703 def parse_args(self, args=None, values=None):
704 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000705 if options.quiet:
706 options.verbose = 0
707
708 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
709 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000710
711 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000712 global_cache_dir = Mirror.GetCachePath()
713 except RuntimeError:
714 global_cache_dir = None
715 if options.cache_dir:
716 if global_cache_dir and (
717 os.path.abspath(options.cache_dir) !=
718 os.path.abspath(global_cache_dir)):
719 logging.warn('Overriding globally-configured cache directory.')
720 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000721
agable@chromium.org5a306a22014-02-24 22:13:59 +0000722 return options, args
723
724
725def main(argv):
726 dispatcher = subcommand.CommandDispatcher(__name__)
727 return dispatcher.execute(OptionParser(), argv)
728
729
730if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000731 try:
732 sys.exit(main(sys.argv[1:]))
733 except KeyboardInterrupt:
734 sys.stderr.write('interrupted\n')
735 sys.exit(1)