blob: 60dfa1c1fa637b26b2e24556052dfaefac0294c6 [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
hinokadcd84042016-06-09 14:26:17 -070041class ClobberNeeded(Exception):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +000042 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.
hinokadcd84042016-06-09 14:26:17 -0700249 try:
250 self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
251 except subprocess.CalledProcessError:
252 # Hard error, need to clobber.
253 raise ClobberNeeded()
szager@chromium.org301a7c32014-06-16 17:13:50 +0000254
255 # Don't combine pack files into one big pack file. It's really slow for
256 # repositories, and there's no way to track progress and make sure it's
257 # not stuck.
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000258 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000259
260 # Allocate more RAM for cache-ing delta chains, for better performance
261 # of "Resolving deltas".
szager@chromium.org848fd492014-04-09 19:06:44 +0000262 self.RunGit(['config', 'core.deltaBaseCacheLimit',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000263 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
szager@chromium.org301a7c32014-06-16 17:13:50 +0000264
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000265 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000266 self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000267 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000268 for spec, value_regex in self.fetch_specs:
szager@chromium.org965c44f2014-08-19 21:19:19 +0000269 self.RunGit(
szager@chromium.org66c8b852015-09-22 23:19:07 +0000270 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
hinoka@chromium.org8e095af2015-06-10 19:19:07 +0000271 cwd=cwd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000272
273 def bootstrap_repo(self, directory):
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000274 """Bootstrap the repo from Google Stroage if possible.
275
276 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
277 """
szager@chromium.org848fd492014-04-09 19:06:44 +0000278
hinoka@google.com776a2c32014-04-25 07:54:25 +0000279 python_fallback = False
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000280 if (sys.platform.startswith('win') and
281 not gclient_utils.FindExecutable('7z')):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000282 python_fallback = True
283 elif sys.platform.startswith('darwin'):
284 # The OSX version of unzip doesn't support zip64.
285 python_fallback = True
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000286 elif not gclient_utils.FindExecutable('unzip'):
hinoka@google.com776a2c32014-04-25 07:54:25 +0000287 python_fallback = True
szager@chromium.org848fd492014-04-09 19:06:44 +0000288
289 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
hinoka@chromium.org199bc5f2014-12-17 02:17:14 +0000290 gsutil = Gsutil(self.gsutil_exe, boto_path=None)
szager@chromium.org848fd492014-04-09 19:06:44 +0000291 # Get the most recent version of the zipfile.
292 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
293 ls_out_sorted = sorted(ls_out.splitlines())
294 if not ls_out_sorted:
295 # This repo is not on Google Storage.
296 return False
297 latest_checkout = ls_out_sorted[-1]
298
299 # Download zip file to a temporary directory.
300 try:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000301 tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
szager@chromium.org848fd492014-04-09 19:06:44 +0000302 self.print('Downloading %s' % latest_checkout)
hinoka@chromium.orgc58d11d2014-06-09 23:34:35 +0000303 code = gsutil.call('cp', latest_checkout, tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000304 if code:
szager@chromium.org848fd492014-04-09 19:06:44 +0000305 return False
306 filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
307
hinoka@google.com776a2c32014-04-25 07:54:25 +0000308 # Unpack the file with 7z on Windows, unzip on linux, or fallback.
309 if not python_fallback:
310 if sys.platform.startswith('win'):
311 cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
312 else:
313 cmd = ['unzip', filename, '-d', directory]
314 retcode = subprocess.call(cmd)
szager@chromium.org848fd492014-04-09 19:06:44 +0000315 else:
hinoka@google.com776a2c32014-04-25 07:54:25 +0000316 try:
317 with zipfile.ZipFile(filename, 'r') as f:
318 f.printdir()
319 f.extractall(directory)
320 except Exception as e:
321 self.print('Encountered error: %s' % str(e), file=sys.stderr)
322 retcode = 1
323 else:
324 retcode = 0
szager@chromium.org848fd492014-04-09 19:06:44 +0000325 finally:
326 # Clean up the downloaded zipfile.
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000327 gclient_utils.rm_file_or_tree(tempdir)
szager@chromium.org848fd492014-04-09 19:06:44 +0000328
329 if retcode:
330 self.print(
331 'Extracting bootstrap zipfile %s failed.\n'
332 'Resuming normal operations.' % filename)
333 return False
334 return True
335
336 def exists(self):
337 return os.path.isfile(os.path.join(self.mirror_path, 'config'))
338
szager@chromium.org66c8b852015-09-22 23:19:07 +0000339 def _preserve_fetchspec(self):
340 """Read and preserve remote.origin.fetch from an existing mirror.
341
342 This modifies self.fetch_specs.
343 """
344 if not self.exists():
345 return
346 try:
347 config_fetchspecs = subprocess.check_output(
348 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
349 cwd=self.mirror_path)
350 for fetchspec in config_fetchspecs.splitlines():
351 self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
352 except subprocess.CalledProcessError:
353 logging.warn('Tried and failed to preserve remote.origin.fetch from the '
354 'existing cache directory. You may need to manually edit '
355 '%s and "git cache fetch" again.'
356 % os.path.join(self.mirror_path, 'config'))
357
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000358 def _ensure_bootstrapped(self, depth, bootstrap, force=False):
359 tempdir = None
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000360 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
361 pack_files = []
362
363 if os.path.isdir(pack_dir):
364 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
365
366 should_bootstrap = (force or
szager@chromium.org66c8b852015-09-22 23:19:07 +0000367 not self.exists() or
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000368 len(pack_files) > GC_AUTOPACKLIMIT)
369 if should_bootstrap:
szager@chromium.org66c8b852015-09-22 23:19:07 +0000370 if self.exists():
371 # Re-bootstrapping an existing mirror; preserve existing fetch spec.
372 self._preserve_fetchspec()
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000373 tempdir = tempfile.mkdtemp(
374 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
375 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
376 if bootstrapped:
377 # Bootstrap succeeded; delete previous cache, if any.
hinoka@chromium.org42f9adf2014-09-05 11:10:35 +0000378 gclient_utils.rmtree(self.mirror_path)
szager@chromium.org66c8b852015-09-22 23:19:07 +0000379 elif not self.exists():
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000380 # Bootstrap failed, no previous cache; start with a bare git dir.
381 self.RunGit(['init', '--bare'], cwd=tempdir)
382 else:
383 # Bootstrap failed, previous cache exists; warn and continue.
384 logging.warn(
385 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
386 'but failed. Continuing with non-optimized repository.'
387 % len(pack_files))
388 gclient_utils.rmtree(tempdir)
389 tempdir = None
390 else:
391 if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
392 logging.warn(
393 'Shallow fetch requested, but repo cache already exists.')
394 return tempdir
395
396 def _fetch(self, rundir, verbose, depth):
397 self.config(rundir)
398 v = []
399 d = []
400 if verbose:
401 v = ['-v', '--progress']
402 if depth:
403 d = ['--depth', str(depth)]
404 fetch_cmd = ['fetch'] + v + d + ['origin']
405 fetch_specs = subprocess.check_output(
406 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
407 cwd=rundir).strip().splitlines()
408 for spec in fetch_specs:
409 try:
410 self.print('Fetching %s' % spec)
411 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
412 except subprocess.CalledProcessError:
413 if spec == '+refs/heads/*:refs/heads/*':
hinokadcd84042016-06-09 14:26:17 -0700414 raise ClobberNeeded() # Corrupted cache.
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000415 logging.warn('Fetch of %s failed' % spec)
416
szager@chromium.org848fd492014-04-09 19:06:44 +0000417 def populate(self, depth=None, shallow=False, bootstrap=False,
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000418 verbose=False, ignore_lock=False, lock_timeout=0):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000419 assert self.GetCachePath()
szager@chromium.org848fd492014-04-09 19:06:44 +0000420 if shallow and not depth:
421 depth = 10000
422 gclient_utils.safe_makedirs(self.GetCachePath())
423
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000424 lockfile = Lockfile(self.mirror_path, lock_timeout)
szager@chromium.org108eced2014-06-19 21:22:43 +0000425 if not ignore_lock:
426 lockfile.lock()
427
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000428 tempdir = None
szager@chromium.org108eced2014-06-19 21:22:43 +0000429 try:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000430 tempdir = self._ensure_bootstrapped(depth, bootstrap)
szager@chromium.org848fd492014-04-09 19:06:44 +0000431 rundir = tempdir or self.mirror_path
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000432 self._fetch(rundir, verbose, depth)
hinokadcd84042016-06-09 14:26:17 -0700433 except ClobberNeeded:
hinoka@chromium.orgaa1e1a42014-06-26 21:58:51 +0000434 # This is a major failure, we need to clean and force a bootstrap.
435 gclient_utils.rmtree(rundir)
436 self.print(GIT_CACHE_CORRUPT_MESSAGE)
437 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
438 assert tempdir
439 self._fetch(tempdir or self.mirror_path, verbose, depth)
440 finally:
szager@chromium.org848fd492014-04-09 19:06:44 +0000441 if tempdir:
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000442 try:
hinoka@chromium.orga5cda1e2014-08-24 12:09:13 +0000443 if os.path.exists(self.mirror_path):
444 gclient_utils.rmtree(self.mirror_path)
hinoka@chromium.org4e2ad842014-07-19 01:23:45 +0000445 os.rename(tempdir, self.mirror_path)
446 except OSError as e:
447 # This is somehow racy on Windows.
448 # Catching OSError because WindowsError isn't portable and
449 # pylint complains.
450 self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
451 str(e)))
szager@chromium.org108eced2014-06-19 21:22:43 +0000452 if not ignore_lock:
453 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000454
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000455 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000456 # The files are named <git number>.zip
457 gen_number = subprocess.check_output(
458 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000459 # Run Garbage Collect to compress packfile.
460 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000461 # Creating a temp file and then deleting it ensures we can use this name.
462 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
463 os.remove(tmp_zipfile)
464 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
465 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000466 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
467 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000468 gsutil.call('cp', tmp_zipfile, dest_name)
469 os.remove(tmp_zipfile)
470
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000471 # Remove all other files in the same directory.
472 if prune:
473 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
474 for filename in ls_out.splitlines():
475 if filename == dest_name:
476 continue
477 gsutil.call('rm', filename)
478
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000479 @staticmethod
480 def DeleteTmpPackFiles(path):
481 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000482 if not os.path.isdir(pack_dir):
483 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000484 pack_files = [f for f in os.listdir(pack_dir) if
485 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
486 for f in pack_files:
487 f = os.path.join(pack_dir, f)
488 try:
489 os.remove(f)
490 logging.warn('Deleted stale temporary pack file %s' % f)
491 except OSError:
492 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000493
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000494 @classmethod
495 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000496 did_unlock = False
497 lf = Lockfile(path)
498 if lf.break_lock():
499 did_unlock = True
500 # Look for lock files that might have been left behind by an interrupted
501 # git process.
502 lf = os.path.join(path, 'config.lock')
503 if os.path.exists(lf):
504 os.remove(lf)
505 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000506 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000507 return did_unlock
508
szager@chromium.org848fd492014-04-09 19:06:44 +0000509 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000510 return self.BreakLocks(self.mirror_path)
511
512 @classmethod
513 def UnlockAll(cls):
514 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000515 if not cachepath:
516 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000517 dirlist = os.listdir(cachepath)
518 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
519 if os.path.isdir(os.path.join(cachepath, path))])
520 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000521 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000522 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000523 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000524 os.path.isfile(os.path.join(cachepath, dirent))):
525 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
526
527 unlocked_repos = []
528 for repo_dir in repo_dirs:
529 if cls.BreakLocks(repo_dir):
530 unlocked_repos.append(repo_dir)
531
532 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000533
agable@chromium.org5a306a22014-02-24 22:13:59 +0000534@subcommand.usage('[url of repo to check for caching]')
535def CMDexists(parser, args):
536 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000537 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000538 if not len(args) == 1:
539 parser.error('git cache exists only takes exactly one repo url.')
540 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000541 mirror = Mirror(url)
542 if mirror.exists():
543 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000544 return 0
545 return 1
546
547
hinoka@google.com563559c2014-04-02 00:36:24 +0000548@subcommand.usage('[url of repo to create a bootstrap zip file]')
549def CMDupdate_bootstrap(parser, args):
550 """Create and uploads a bootstrap tarball."""
551 # Lets just assert we can't do this on Windows.
552 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000553 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000554 return 1
555
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000556 parser.add_option('--prune', action='store_true',
557 help='Prune all other cached zipballs of the same repo.')
558
hinoka@google.com563559c2014-04-02 00:36:24 +0000559 # First, we need to ensure the cache is populated.
560 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000561 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000562 CMDpopulate(parser, populate_args)
563
564 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000565 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000566 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000567 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000568 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000569 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000570
571
agable@chromium.org5a306a22014-02-24 22:13:59 +0000572@subcommand.usage('[url of repo to add to or update in cache]')
573def CMDpopulate(parser, args):
574 """Ensure that the cache has all up-to-date objects for the given repo."""
575 parser.add_option('--depth', type='int',
576 help='Only cache DEPTH commits of history')
577 parser.add_option('--shallow', '-s', action='store_true',
578 help='Only cache 10000 commits of history')
579 parser.add_option('--ref', action='append',
580 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000581 parser.add_option('--no_bootstrap', '--no-bootstrap',
582 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000583 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000584 parser.add_option('--ignore_locks', '--ignore-locks',
585 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000586 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000587
agable@chromium.org5a306a22014-02-24 22:13:59 +0000588 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000589 if not len(args) == 1:
590 parser.error('git cache populate only takes exactly one repo url.')
591 url = args[0]
592
szager@chromium.org848fd492014-04-09 19:06:44 +0000593 mirror = Mirror(url, refs=options.ref)
594 kwargs = {
595 'verbose': options.verbose,
596 'shallow': options.shallow,
597 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000598 'ignore_lock': options.ignore_locks,
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000599 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000600 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000601 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000602 kwargs['depth'] = options.depth
603 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000604
605
szager@chromium.orgf3145112014-08-07 21:02:36 +0000606@subcommand.usage('Fetch new commits into cache and current checkout')
607def CMDfetch(parser, args):
608 """Update mirror, and fetch in cwd."""
609 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000610 parser.add_option('--no_bootstrap', '--no-bootstrap',
611 action='store_true',
612 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000613 options, args = parser.parse_args(args)
614
615 # Figure out which remotes to fetch. This mimics the behavior of regular
616 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
617 # this will NOT try to traverse up the branching structure to find the
618 # ultimate remote to update.
619 remotes = []
620 if options.all:
621 assert not args, 'fatal: fetch --all does not take a repository argument'
622 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
623 elif args:
624 remotes = args
625 else:
626 current_branch = subprocess.check_output(
627 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
628 if current_branch != 'HEAD':
629 upstream = subprocess.check_output(
630 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
631 ).strip()
632 if upstream and upstream != '.':
633 remotes = [upstream]
634 if not remotes:
635 remotes = ['origin']
636
637 cachepath = Mirror.GetCachePath()
638 git_dir = os.path.abspath(subprocess.check_output(
639 [Mirror.git_exe, 'rev-parse', '--git-dir']))
640 git_dir = os.path.abspath(git_dir)
641 if git_dir.startswith(cachepath):
642 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000643 mirror.populate(
644 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000645 return 0
646 for remote in remotes:
647 remote_url = subprocess.check_output(
648 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
649 if remote_url.startswith(cachepath):
650 mirror = Mirror.FromPath(remote_url)
651 mirror.print = lambda *args: None
652 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000653 mirror.populate(
654 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000655 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
656 return 0
657
658
agable@chromium.org5a306a22014-02-24 22:13:59 +0000659@subcommand.usage('[url of repo to unlock, or -a|--all]')
660def CMDunlock(parser, args):
661 """Unlock one or all repos if their lock files are still around."""
662 parser.add_option('--force', '-f', action='store_true',
663 help='Actually perform the action')
664 parser.add_option('--all', '-a', action='store_true',
665 help='Unlock all repository caches')
666 options, args = parser.parse_args(args)
667 if len(args) > 1 or (len(args) == 0 and not options.all):
668 parser.error('git cache unlock takes exactly one repo url, or --all')
669
agable@chromium.org5a306a22014-02-24 22:13:59 +0000670 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000671 cachepath = Mirror.GetCachePath()
672 lockfiles = [os.path.join(cachepath, path)
673 for path in os.listdir(cachepath)
674 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000675 parser.error('git cache unlock requires -f|--force to do anything. '
676 'Refusing to unlock the following repo caches: '
677 ', '.join(lockfiles))
678
szager@chromium.org848fd492014-04-09 19:06:44 +0000679 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000680 if options.all:
681 unlocked_repos.extend(Mirror.UnlockAll())
682 else:
683 m = Mirror(args[0])
684 if m.unlock():
685 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000686
szager@chromium.org848fd492014-04-09 19:06:44 +0000687 if unlocked_repos:
688 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
689 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000690
691
692class OptionParser(optparse.OptionParser):
693 """Wrapper class for OptionParser to handle global options."""
694
695 def __init__(self, *args, **kwargs):
696 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
697 self.add_option('-c', '--cache-dir',
698 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000699 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000700 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000701 self.add_option('-q', '--quiet', action='store_true',
702 help='Suppress all extraneous output')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000703 self.add_option('--timeout', type='int', default=0,
704 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000705
706 def parse_args(self, args=None, values=None):
707 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000708 if options.quiet:
709 options.verbose = 0
710
711 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
712 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000713
714 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000715 global_cache_dir = Mirror.GetCachePath()
716 except RuntimeError:
717 global_cache_dir = None
718 if options.cache_dir:
719 if global_cache_dir and (
720 os.path.abspath(options.cache_dir) !=
721 os.path.abspath(global_cache_dir)):
722 logging.warn('Overriding globally-configured cache directory.')
723 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000724
agable@chromium.org5a306a22014-02-24 22:13:59 +0000725 return options, args
726
727
728def main(argv):
729 dispatcher = subcommand.CommandDispatcher(__name__)
730 return dispatcher.execute(OptionParser(), argv)
731
732
733if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000734 try:
735 sys.exit(main(sys.argv[1:]))
736 except KeyboardInterrupt:
737 sys.stderr.write('interrupted\n')
738 sys.exit(1)