blob: 0f66de72e9746b170df01982ccbf6ebaf0ac1936 [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:
dnjb445ef52016-11-10 15:51:39 -0800442 if os.path.exists(self.mirror_path):
443 gclient_utils.rmtree(self.mirror_path)
444 os.rename(tempdir, self.mirror_path)
szager@chromium.org108eced2014-06-19 21:22:43 +0000445 if not ignore_lock:
446 lockfile.unlock()
szager@chromium.org848fd492014-04-09 19:06:44 +0000447
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000448 def update_bootstrap(self, prune=False):
szager@chromium.org848fd492014-04-09 19:06:44 +0000449 # The files are named <git number>.zip
450 gen_number = subprocess.check_output(
451 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
hinoka@chromium.org7b1cb6f2014-09-08 21:40:50 +0000452 # Run Garbage Collect to compress packfile.
453 self.RunGit(['gc', '--prune=all'])
szager@chromium.org848fd492014-04-09 19:06:44 +0000454 # Creating a temp file and then deleting it ensures we can use this name.
455 _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
456 os.remove(tmp_zipfile)
457 subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
458 gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000459 gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
460 dest_name = '%s/%s.zip' % (gs_folder, gen_number)
szager@chromium.org848fd492014-04-09 19:06:44 +0000461 gsutil.call('cp', tmp_zipfile, dest_name)
462 os.remove(tmp_zipfile)
463
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000464 # Remove all other files in the same directory.
465 if prune:
466 _, ls_out, _ = gsutil.check_call('ls', gs_folder)
467 for filename in ls_out.splitlines():
468 if filename == dest_name:
469 continue
470 gsutil.call('rm', filename)
471
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000472 @staticmethod
473 def DeleteTmpPackFiles(path):
474 pack_dir = os.path.join(path, 'objects', 'pack')
szager@chromium.org33418492014-06-18 19:03:39 +0000475 if not os.path.isdir(pack_dir):
476 return
szager@chromium.orgcdfcd7c2014-06-10 23:40:46 +0000477 pack_files = [f for f in os.listdir(pack_dir) if
478 f.startswith('.tmp-') or f.startswith('tmp_pack_')]
479 for f in pack_files:
480 f = os.path.join(pack_dir, f)
481 try:
482 os.remove(f)
483 logging.warn('Deleted stale temporary pack file %s' % f)
484 except OSError:
485 logging.warn('Unable to delete temporary pack file %s' % f)
szager@chromium.org174766f2014-05-13 21:27:46 +0000486
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000487 @classmethod
488 def BreakLocks(cls, path):
szager@chromium.org174766f2014-05-13 21:27:46 +0000489 did_unlock = False
490 lf = Lockfile(path)
491 if lf.break_lock():
492 did_unlock = True
493 # Look for lock files that might have been left behind by an interrupted
494 # git process.
495 lf = os.path.join(path, 'config.lock')
496 if os.path.exists(lf):
497 os.remove(lf)
498 did_unlock = True
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000499 cls.DeleteTmpPackFiles(path)
szager@chromium.org174766f2014-05-13 21:27:46 +0000500 return did_unlock
501
szager@chromium.org848fd492014-04-09 19:06:44 +0000502 def unlock(self):
szager@chromium.org174766f2014-05-13 21:27:46 +0000503 return self.BreakLocks(self.mirror_path)
504
505 @classmethod
506 def UnlockAll(cls):
507 cachepath = cls.GetCachePath()
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000508 if not cachepath:
509 return
szager@chromium.org174766f2014-05-13 21:27:46 +0000510 dirlist = os.listdir(cachepath)
511 repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
512 if os.path.isdir(os.path.join(cachepath, path))])
513 for dirent in dirlist:
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000514 if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000515 gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
szager@chromium.org1cbf1042014-06-17 18:26:24 +0000516 elif (dirent.endswith('.lock') and
szager@chromium.org174766f2014-05-13 21:27:46 +0000517 os.path.isfile(os.path.join(cachepath, dirent))):
518 repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
519
520 unlocked_repos = []
521 for repo_dir in repo_dirs:
522 if cls.BreakLocks(repo_dir):
523 unlocked_repos.append(repo_dir)
524
525 return unlocked_repos
szager@chromium.org848fd492014-04-09 19:06:44 +0000526
agable@chromium.org5a306a22014-02-24 22:13:59 +0000527@subcommand.usage('[url of repo to check for caching]')
528def CMDexists(parser, args):
529 """Check to see if there already is a cache of the given repo."""
szager@chromium.org848fd492014-04-09 19:06:44 +0000530 _, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000531 if not len(args) == 1:
532 parser.error('git cache exists only takes exactly one repo url.')
533 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000534 mirror = Mirror(url)
535 if mirror.exists():
536 print(mirror.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000537 return 0
538 return 1
539
540
hinoka@google.com563559c2014-04-02 00:36:24 +0000541@subcommand.usage('[url of repo to create a bootstrap zip file]')
542def CMDupdate_bootstrap(parser, args):
543 """Create and uploads a bootstrap tarball."""
544 # Lets just assert we can't do this on Windows.
545 if sys.platform.startswith('win'):
szager@chromium.org848fd492014-04-09 19:06:44 +0000546 print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
hinoka@google.com563559c2014-04-02 00:36:24 +0000547 return 1
548
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000549 parser.add_option('--prune', action='store_true',
550 help='Prune all other cached zipballs of the same repo.')
551
hinoka@google.com563559c2014-04-02 00:36:24 +0000552 # First, we need to ensure the cache is populated.
553 populate_args = args[:]
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000554 populate_args.append('--no-bootstrap')
hinoka@google.com563559c2014-04-02 00:36:24 +0000555 CMDpopulate(parser, populate_args)
556
557 # Get the repo directory.
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000558 options, args = parser.parse_args(args)
hinoka@google.com563559c2014-04-02 00:36:24 +0000559 url = args[0]
szager@chromium.org848fd492014-04-09 19:06:44 +0000560 mirror = Mirror(url)
hinoka@chromium.orgc8444f32014-06-18 23:18:17 +0000561 mirror.update_bootstrap(options.prune)
szager@chromium.org848fd492014-04-09 19:06:44 +0000562 return 0
hinoka@google.com563559c2014-04-02 00:36:24 +0000563
564
agable@chromium.org5a306a22014-02-24 22:13:59 +0000565@subcommand.usage('[url of repo to add to or update in cache]')
566def CMDpopulate(parser, args):
567 """Ensure that the cache has all up-to-date objects for the given repo."""
568 parser.add_option('--depth', type='int',
569 help='Only cache DEPTH commits of history')
570 parser.add_option('--shallow', '-s', action='store_true',
571 help='Only cache 10000 commits of history')
572 parser.add_option('--ref', action='append',
573 help='Specify additional refs to be fetched')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000574 parser.add_option('--no_bootstrap', '--no-bootstrap',
575 action='store_true',
hinoka@google.com563559c2014-04-02 00:36:24 +0000576 help='Don\'t bootstrap from Google Storage')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +0000577 parser.add_option('--ignore_locks', '--ignore-locks',
578 action='store_true',
szager@chromium.org108eced2014-06-19 21:22:43 +0000579 help='Don\'t try to lock repository')
hinoka@google.com563559c2014-04-02 00:36:24 +0000580
agable@chromium.org5a306a22014-02-24 22:13:59 +0000581 options, args = parser.parse_args(args)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000582 if not len(args) == 1:
583 parser.error('git cache populate only takes exactly one repo url.')
584 url = args[0]
585
szager@chromium.org848fd492014-04-09 19:06:44 +0000586 mirror = Mirror(url, refs=options.ref)
587 kwargs = {
588 'verbose': options.verbose,
589 'shallow': options.shallow,
590 'bootstrap': not options.no_bootstrap,
szager@chromium.org108eced2014-06-19 21:22:43 +0000591 'ignore_lock': options.ignore_locks,
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000592 'lock_timeout': options.timeout,
szager@chromium.org848fd492014-04-09 19:06:44 +0000593 }
agable@chromium.org5a306a22014-02-24 22:13:59 +0000594 if options.depth:
szager@chromium.org848fd492014-04-09 19:06:44 +0000595 kwargs['depth'] = options.depth
596 mirror.populate(**kwargs)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000597
598
szager@chromium.orgf3145112014-08-07 21:02:36 +0000599@subcommand.usage('Fetch new commits into cache and current checkout')
600def CMDfetch(parser, args):
601 """Update mirror, and fetch in cwd."""
602 parser.add_option('--all', action='store_true', help='Fetch all remotes')
szager@chromium.org66c8b852015-09-22 23:19:07 +0000603 parser.add_option('--no_bootstrap', '--no-bootstrap',
604 action='store_true',
605 help='Don\'t (re)bootstrap from Google Storage')
szager@chromium.orgf3145112014-08-07 21:02:36 +0000606 options, args = parser.parse_args(args)
607
608 # Figure out which remotes to fetch. This mimics the behavior of regular
609 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
610 # this will NOT try to traverse up the branching structure to find the
611 # ultimate remote to update.
612 remotes = []
613 if options.all:
614 assert not args, 'fatal: fetch --all does not take a repository argument'
615 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
616 elif args:
617 remotes = args
618 else:
619 current_branch = subprocess.check_output(
620 [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
621 if current_branch != 'HEAD':
622 upstream = subprocess.check_output(
623 [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
624 ).strip()
625 if upstream and upstream != '.':
626 remotes = [upstream]
627 if not remotes:
628 remotes = ['origin']
629
630 cachepath = Mirror.GetCachePath()
631 git_dir = os.path.abspath(subprocess.check_output(
632 [Mirror.git_exe, 'rev-parse', '--git-dir']))
633 git_dir = os.path.abspath(git_dir)
634 if git_dir.startswith(cachepath):
635 mirror = Mirror.FromPath(git_dir)
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000636 mirror.populate(
637 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000638 return 0
639 for remote in remotes:
640 remote_url = subprocess.check_output(
641 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
642 if remote_url.startswith(cachepath):
643 mirror = Mirror.FromPath(remote_url)
644 mirror.print = lambda *args: None
645 print('Updating git cache...')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000646 mirror.populate(
647 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
szager@chromium.orgf3145112014-08-07 21:02:36 +0000648 subprocess.check_call([Mirror.git_exe, 'fetch', remote])
649 return 0
650
651
agable@chromium.org5a306a22014-02-24 22:13:59 +0000652@subcommand.usage('[url of repo to unlock, or -a|--all]')
653def CMDunlock(parser, args):
654 """Unlock one or all repos if their lock files are still around."""
655 parser.add_option('--force', '-f', action='store_true',
656 help='Actually perform the action')
657 parser.add_option('--all', '-a', action='store_true',
658 help='Unlock all repository caches')
659 options, args = parser.parse_args(args)
660 if len(args) > 1 or (len(args) == 0 and not options.all):
661 parser.error('git cache unlock takes exactly one repo url, or --all')
662
agable@chromium.org5a306a22014-02-24 22:13:59 +0000663 if not options.force:
szager@chromium.org174766f2014-05-13 21:27:46 +0000664 cachepath = Mirror.GetCachePath()
665 lockfiles = [os.path.join(cachepath, path)
666 for path in os.listdir(cachepath)
667 if path.endswith('.lock') and os.path.isfile(path)]
agable@chromium.org5a306a22014-02-24 22:13:59 +0000668 parser.error('git cache unlock requires -f|--force to do anything. '
669 'Refusing to unlock the following repo caches: '
670 ', '.join(lockfiles))
671
szager@chromium.org848fd492014-04-09 19:06:44 +0000672 unlocked_repos = []
szager@chromium.org174766f2014-05-13 21:27:46 +0000673 if options.all:
674 unlocked_repos.extend(Mirror.UnlockAll())
675 else:
676 m = Mirror(args[0])
677 if m.unlock():
678 unlocked_repos.append(m.mirror_path)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000679
szager@chromium.org848fd492014-04-09 19:06:44 +0000680 if unlocked_repos:
681 logging.info('Broke locks on these caches:\n %s' % '\n '.join(
682 unlocked_repos))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000683
684
685class OptionParser(optparse.OptionParser):
686 """Wrapper class for OptionParser to handle global options."""
687
688 def __init__(self, *args, **kwargs):
689 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
690 self.add_option('-c', '--cache-dir',
691 help='Path to the directory containing the cache')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000692 self.add_option('-v', '--verbose', action='count', default=1,
agable@chromium.org5a306a22014-02-24 22:13:59 +0000693 help='Increase verbosity (can be passed multiple times)')
szager@chromium.org2c391af2014-05-23 09:07:15 +0000694 self.add_option('-q', '--quiet', action='store_true',
695 help='Suppress all extraneous output')
szager@chromium.orgdbb6f822016-02-02 22:59:30 +0000696 self.add_option('--timeout', type='int', default=0,
697 help='Timeout for acquiring cache lock, in seconds')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000698
699 def parse_args(self, args=None, values=None):
700 options, args = optparse.OptionParser.parse_args(self, args, values)
szager@chromium.org2c391af2014-05-23 09:07:15 +0000701 if options.quiet:
702 options.verbose = 0
703
704 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
705 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
agable@chromium.org5a306a22014-02-24 22:13:59 +0000706
707 try:
szager@chromium.org848fd492014-04-09 19:06:44 +0000708 global_cache_dir = Mirror.GetCachePath()
709 except RuntimeError:
710 global_cache_dir = None
711 if options.cache_dir:
712 if global_cache_dir and (
713 os.path.abspath(options.cache_dir) !=
714 os.path.abspath(global_cache_dir)):
715 logging.warn('Overriding globally-configured cache directory.')
716 Mirror.SetCachePath(options.cache_dir)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000717
agable@chromium.org5a306a22014-02-24 22:13:59 +0000718 return options, args
719
720
721def main(argv):
722 dispatcher = subcommand.CommandDispatcher(__name__)
723 return dispatcher.execute(OptionParser(), argv)
724
725
726if __name__ == '__main__':
sbc@chromium.org013731e2015-02-26 18:28:43 +0000727 try:
728 sys.exit(main(sys.argv[1:]))
729 except KeyboardInterrupt:
730 sys.stderr.write('interrupted\n')
731 sys.exit(1)