git_cache.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. #!/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. """A git command for managing a local cache of git repositories."""
  6. from __future__ import print_function
  7. import errno
  8. import logging
  9. import optparse
  10. import os
  11. import re
  12. import tempfile
  13. import threading
  14. import time
  15. import subprocess
  16. import sys
  17. import urlparse
  18. import zipfile
  19. from download_from_google_storage import Gsutil
  20. import gclient_utils
  21. import subcommand
  22. # Analogous to gc.autopacklimit git config.
  23. GC_AUTOPACKLIMIT = 50
  24. GIT_CACHE_CORRUPT_MESSAGE = 'WARNING: The Git cache is corrupt.'
  25. try:
  26. # pylint: disable=undefined-variable
  27. WinErr = WindowsError
  28. except NameError:
  29. class WinErr(Exception):
  30. pass
  31. class LockError(Exception):
  32. pass
  33. class ClobberNeeded(Exception):
  34. pass
  35. def exponential_backoff_retry(fn, excs=(Exception,), name=None, count=10,
  36. sleep_time=0.25, printerr=None):
  37. """Executes |fn| up to |count| times, backing off exponentially.
  38. Args:
  39. fn (callable): The function to execute. If this raises a handled
  40. exception, the function will retry with exponential backoff.
  41. excs (tuple): A tuple of Exception types to handle. If one of these is
  42. raised by |fn|, a retry will be attempted. If |fn| raises an Exception
  43. that is not in this list, it will immediately pass through. If |excs|
  44. is empty, the Exception base class will be used.
  45. name (str): Optional operation name to print in the retry string.
  46. count (int): The number of times to try before allowing the exception to
  47. pass through.
  48. sleep_time (float): The initial number of seconds to sleep in between
  49. retries. This will be doubled each retry.
  50. printerr (callable): Function that will be called with the error string upon
  51. failures. If None, |logging.warning| will be used.
  52. Returns: The return value of the successful fn.
  53. """
  54. printerr = printerr or logging.warning
  55. for i in xrange(count):
  56. try:
  57. return fn()
  58. except excs as e:
  59. if (i+1) >= count:
  60. raise
  61. printerr('Retrying %s in %.2f second(s) (%d / %d attempts): %s' % (
  62. (name or 'operation'), sleep_time, (i+1), count, e))
  63. time.sleep(sleep_time)
  64. sleep_time *= 2
  65. class Lockfile(object):
  66. """Class to represent a cross-platform process-specific lockfile."""
  67. def __init__(self, path, timeout=0):
  68. self.path = os.path.abspath(path)
  69. self.timeout = timeout
  70. self.lockfile = self.path + ".lock"
  71. self.pid = os.getpid()
  72. def _read_pid(self):
  73. """Read the pid stored in the lockfile.
  74. Note: This method is potentially racy. By the time it returns the lockfile
  75. may have been unlocked, removed, or stolen by some other process.
  76. """
  77. try:
  78. with open(self.lockfile, 'r') as f:
  79. pid = int(f.readline().strip())
  80. except (IOError, ValueError):
  81. pid = None
  82. return pid
  83. def _make_lockfile(self):
  84. """Safely creates a lockfile containing the current pid."""
  85. open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
  86. fd = os.open(self.lockfile, open_flags, 0o644)
  87. f = os.fdopen(fd, 'w')
  88. print(self.pid, file=f)
  89. f.close()
  90. def _remove_lockfile(self):
  91. """Delete the lockfile. Complains (implicitly) if it doesn't exist.
  92. See gclient_utils.py:rmtree docstring for more explanation on the
  93. windows case.
  94. """
  95. if sys.platform == 'win32':
  96. lockfile = os.path.normcase(self.lockfile)
  97. def delete():
  98. exitcode = subprocess.call(['cmd.exe', '/c',
  99. 'del', '/f', '/q', lockfile])
  100. if exitcode != 0:
  101. raise LockError('Failed to remove lock: %s' % (lockfile,))
  102. exponential_backoff_retry(
  103. delete,
  104. excs=(LockError,),
  105. name='del [%s]' % (lockfile,))
  106. else:
  107. os.remove(self.lockfile)
  108. def lock(self):
  109. """Acquire the lock.
  110. This will block with a deadline of self.timeout seconds.
  111. """
  112. elapsed = 0
  113. while True:
  114. try:
  115. self._make_lockfile()
  116. return
  117. except OSError as e:
  118. if elapsed < self.timeout:
  119. sleep_time = max(10, min(3, self.timeout - elapsed))
  120. logging.info('Could not create git cache lockfile; '
  121. 'will retry after sleep(%d).', sleep_time);
  122. elapsed += sleep_time
  123. time.sleep(sleep_time)
  124. continue
  125. if e.errno == errno.EEXIST:
  126. raise LockError("%s is already locked" % self.path)
  127. else:
  128. raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
  129. def unlock(self):
  130. """Release the lock."""
  131. try:
  132. if not self.is_locked():
  133. raise LockError("%s is not locked" % self.path)
  134. if not self.i_am_locking():
  135. raise LockError("%s is locked, but not by me" % self.path)
  136. self._remove_lockfile()
  137. except WinErr:
  138. # Windows is unreliable when it comes to file locking. YMMV.
  139. pass
  140. def break_lock(self):
  141. """Remove the lock, even if it was created by someone else."""
  142. try:
  143. self._remove_lockfile()
  144. return True
  145. except OSError as exc:
  146. if exc.errno == errno.ENOENT:
  147. return False
  148. else:
  149. raise
  150. def is_locked(self):
  151. """Test if the file is locked by anyone.
  152. Note: This method is potentially racy. By the time it returns the lockfile
  153. may have been unlocked, removed, or stolen by some other process.
  154. """
  155. return os.path.exists(self.lockfile)
  156. def i_am_locking(self):
  157. """Test if the file is locked by this process."""
  158. return self.is_locked() and self.pid == self._read_pid()
  159. class Mirror(object):
  160. git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
  161. gsutil_exe = os.path.join(
  162. os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
  163. cachepath_lock = threading.Lock()
  164. @staticmethod
  165. def parse_fetch_spec(spec):
  166. """Parses and canonicalizes a fetch spec.
  167. Returns (fetchspec, value_regex), where value_regex can be used
  168. with 'git config --replace-all'.
  169. """
  170. parts = spec.split(':', 1)
  171. src = parts[0].lstrip('+').rstrip('/')
  172. if not src.startswith('refs/'):
  173. src = 'refs/heads/%s' % src
  174. dest = parts[1].rstrip('/') if len(parts) > 1 else src
  175. regex = r'\+%s:.*' % src.replace('*', r'\*')
  176. return ('+%s:%s' % (src, dest), regex)
  177. def __init__(self, url, refs=None, print_func=None):
  178. self.url = url
  179. self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
  180. self.basedir = self.UrlToCacheDir(url)
  181. self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
  182. if print_func:
  183. self.print = self.print_without_file
  184. self.print_func = print_func
  185. else:
  186. self.print = print
  187. def print_without_file(self, message, **_kwargs):
  188. self.print_func(message)
  189. @property
  190. def bootstrap_bucket(self):
  191. u = urlparse.urlparse(self.url)
  192. if u.netloc == 'chromium.googlesource.com':
  193. return 'chromium-git-cache'
  194. elif u.netloc == 'chrome-internal.googlesource.com':
  195. return 'chrome-git-cache'
  196. # Not recognized.
  197. return None
  198. @classmethod
  199. def FromPath(cls, path):
  200. return cls(cls.CacheDirToUrl(path))
  201. @staticmethod
  202. def UrlToCacheDir(url):
  203. """Convert a git url to a normalized form for the cache dir path."""
  204. parsed = urlparse.urlparse(url)
  205. norm_url = parsed.netloc + parsed.path
  206. if norm_url.endswith('.git'):
  207. norm_url = norm_url[:-len('.git')]
  208. return norm_url.replace('-', '--').replace('/', '-').lower()
  209. @staticmethod
  210. def CacheDirToUrl(path):
  211. """Convert a cache dir path to its corresponding url."""
  212. netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
  213. return 'https://%s' % netpath
  214. @classmethod
  215. def SetCachePath(cls, cachepath):
  216. with cls.cachepath_lock:
  217. setattr(cls, 'cachepath', cachepath)
  218. @classmethod
  219. def GetCachePath(cls):
  220. 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')
  232. def Rename(self, src, dst):
  233. # This is somehow racy on Windows.
  234. # Catching OSError because WindowsError isn't portable and
  235. # pylint complains.
  236. exponential_backoff_retry(
  237. lambda: os.rename(src, dst),
  238. excs=(OSError,),
  239. name='rename [%s] => [%s]' % (src, dst),
  240. printerr=self.print)
  241. def RunGit(self, cmd, **kwargs):
  242. """Run git in a subprocess."""
  243. cwd = kwargs.setdefault('cwd', self.mirror_path)
  244. kwargs.setdefault('print_stdout', False)
  245. kwargs.setdefault('filter_fn', self.print)
  246. env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
  247. env.setdefault('GIT_ASKPASS', 'true')
  248. env.setdefault('SSH_ASKPASS', 'true')
  249. self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
  250. gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
  251. def config(self, cwd=None):
  252. if cwd is None:
  253. cwd = self.mirror_path
  254. # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
  255. try:
  256. self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
  257. except subprocess.CalledProcessError:
  258. # Hard error, need to clobber.
  259. raise ClobberNeeded()
  260. # Don't combine pack files into one big pack file. It's really slow for
  261. # repositories, and there's no way to track progress and make sure it's
  262. # not stuck.
  263. if self.supported_project():
  264. self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
  265. # Allocate more RAM for cache-ing delta chains, for better performance
  266. # of "Resolving deltas".
  267. self.RunGit(['config', 'core.deltaBaseCacheLimit',
  268. gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
  269. self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
  270. self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
  271. '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
  272. for spec, value_regex in self.fetch_specs:
  273. self.RunGit(
  274. ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
  275. cwd=cwd)
  276. def bootstrap_repo(self, directory):
  277. """Bootstrap the repo from Google Stroage if possible.
  278. More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
  279. """
  280. if not self.bootstrap_bucket:
  281. return False
  282. python_fallback = False
  283. if (sys.platform.startswith('win') and
  284. not gclient_utils.FindExecutable('7z')):
  285. python_fallback = True
  286. elif sys.platform.startswith('darwin'):
  287. # The OSX version of unzip doesn't support zip64.
  288. python_fallback = True
  289. elif not gclient_utils.FindExecutable('unzip'):
  290. python_fallback = True
  291. gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
  292. gsutil = Gsutil(self.gsutil_exe, boto_path=None)
  293. # Get the most recent version of the zipfile.
  294. _, ls_out, _ = gsutil.check_call('ls', gs_folder)
  295. ls_out_sorted = sorted(ls_out.splitlines())
  296. if not ls_out_sorted:
  297. # This repo is not on Google Storage.
  298. return False
  299. latest_checkout = ls_out_sorted[-1]
  300. # Download zip file to a temporary directory.
  301. try:
  302. tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
  303. self.print('Downloading %s' % latest_checkout)
  304. code = gsutil.call('cp', latest_checkout, tempdir)
  305. if code:
  306. return False
  307. filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
  308. # 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)
  315. else:
  316. 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
  325. finally:
  326. # Clean up the downloaded zipfile.
  327. #
  328. # This is somehow racy on Windows.
  329. # Catching OSError because WindowsError isn't portable and
  330. # pylint complains.
  331. exponential_backoff_retry(
  332. lambda: gclient_utils.rm_file_or_tree(tempdir),
  333. excs=(OSError,),
  334. name='rmtree [%s]' % (tempdir,),
  335. printerr=self.print)
  336. if retcode:
  337. self.print(
  338. 'Extracting bootstrap zipfile %s failed.\n'
  339. 'Resuming normal operations.' % filename)
  340. return False
  341. return True
  342. def exists(self):
  343. return os.path.isfile(os.path.join(self.mirror_path, 'config'))
  344. def supported_project(self):
  345. """Returns true if this repo is known to have a bootstrap zip file."""
  346. u = urlparse.urlparse(self.url)
  347. return u.netloc in [
  348. 'chromium.googlesource.com',
  349. 'chrome-internal.googlesource.com']
  350. def _preserve_fetchspec(self):
  351. """Read and preserve remote.origin.fetch from an existing mirror.
  352. This modifies self.fetch_specs.
  353. """
  354. if not self.exists():
  355. return
  356. try:
  357. config_fetchspecs = subprocess.check_output(
  358. [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
  359. cwd=self.mirror_path)
  360. for fetchspec in config_fetchspecs.splitlines():
  361. self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
  362. except subprocess.CalledProcessError:
  363. logging.warn('Tried and failed to preserve remote.origin.fetch from the '
  364. 'existing cache directory. You may need to manually edit '
  365. '%s and "git cache fetch" again.'
  366. % os.path.join(self.mirror_path, 'config'))
  367. def _ensure_bootstrapped(self, depth, bootstrap, force=False):
  368. tempdir = None
  369. pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
  370. pack_files = []
  371. if os.path.isdir(pack_dir):
  372. pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
  373. should_bootstrap = (force or
  374. not self.exists() or
  375. len(pack_files) > GC_AUTOPACKLIMIT)
  376. if should_bootstrap:
  377. if self.exists():
  378. # Re-bootstrapping an existing mirror; preserve existing fetch spec.
  379. self._preserve_fetchspec()
  380. tempdir = tempfile.mkdtemp(
  381. prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
  382. bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
  383. if bootstrapped:
  384. # Bootstrap succeeded; delete previous cache, if any.
  385. gclient_utils.rmtree(self.mirror_path)
  386. elif not self.exists() or not self.supported_project():
  387. # Bootstrap failed due to either
  388. # 1. No previous cache
  389. # 2. Project doesn't have a bootstrap zip file
  390. # Start with a bare git dir.
  391. self.RunGit(['init', '--bare'], cwd=tempdir)
  392. else:
  393. # Bootstrap failed, previous cache exists; warn and continue.
  394. logging.warn(
  395. 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
  396. 'but failed. Continuing with non-optimized repository.'
  397. % len(pack_files))
  398. gclient_utils.rmtree(tempdir)
  399. tempdir = None
  400. else:
  401. if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
  402. logging.warn(
  403. 'Shallow fetch requested, but repo cache already exists.')
  404. return tempdir
  405. def _fetch(self, rundir, verbose, depth):
  406. self.config(rundir)
  407. v = []
  408. d = []
  409. if verbose:
  410. v = ['-v', '--progress']
  411. if depth:
  412. d = ['--depth', str(depth)]
  413. fetch_cmd = ['fetch'] + v + d + ['origin']
  414. fetch_specs = subprocess.check_output(
  415. [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
  416. cwd=rundir).strip().splitlines()
  417. for spec in fetch_specs:
  418. try:
  419. self.print('Fetching %s' % spec)
  420. self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
  421. except subprocess.CalledProcessError:
  422. if spec == '+refs/heads/*:refs/heads/*':
  423. raise ClobberNeeded() # Corrupted cache.
  424. logging.warn('Fetch of %s failed' % spec)
  425. def populate(self, depth=None, shallow=False, bootstrap=False,
  426. verbose=False, ignore_lock=False, lock_timeout=0):
  427. assert self.GetCachePath()
  428. if shallow and not depth:
  429. depth = 10000
  430. gclient_utils.safe_makedirs(self.GetCachePath())
  431. lockfile = Lockfile(self.mirror_path, lock_timeout)
  432. if not ignore_lock:
  433. lockfile.lock()
  434. tempdir = None
  435. try:
  436. tempdir = self._ensure_bootstrapped(depth, bootstrap)
  437. rundir = tempdir or self.mirror_path
  438. self._fetch(rundir, verbose, depth)
  439. except ClobberNeeded:
  440. # This is a major failure, we need to clean and force a bootstrap.
  441. gclient_utils.rmtree(rundir)
  442. self.print(GIT_CACHE_CORRUPT_MESSAGE)
  443. tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
  444. assert tempdir
  445. self._fetch(tempdir or self.mirror_path, verbose, depth)
  446. finally:
  447. if tempdir:
  448. if os.path.exists(self.mirror_path):
  449. gclient_utils.rmtree(self.mirror_path)
  450. self.Rename(tempdir, self.mirror_path)
  451. if not ignore_lock:
  452. lockfile.unlock()
  453. def update_bootstrap(self, prune=False):
  454. # The files are named <git number>.zip
  455. gen_number = subprocess.check_output(
  456. [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
  457. # Run Garbage Collect to compress packfile.
  458. self.RunGit(['gc', '--prune=all'])
  459. # Creating a temp file and then deleting it ensures we can use this name.
  460. _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
  461. os.remove(tmp_zipfile)
  462. subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
  463. gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
  464. gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
  465. dest_name = '%s/%s.zip' % (gs_folder, gen_number)
  466. gsutil.call('cp', tmp_zipfile, dest_name)
  467. os.remove(tmp_zipfile)
  468. # 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. @staticmethod
  476. def DeleteTmpPackFiles(path):
  477. pack_dir = os.path.join(path, 'objects', 'pack')
  478. if not os.path.isdir(pack_dir):
  479. return
  480. pack_files = [f for f in os.listdir(pack_dir) if
  481. f.startswith('.tmp-') or f.startswith('tmp_pack_')]
  482. for f in pack_files:
  483. f = os.path.join(pack_dir, f)
  484. try:
  485. os.remove(f)
  486. logging.warn('Deleted stale temporary pack file %s' % f)
  487. except OSError:
  488. logging.warn('Unable to delete temporary pack file %s' % f)
  489. @classmethod
  490. def BreakLocks(cls, path):
  491. did_unlock = False
  492. lf = Lockfile(path)
  493. if lf.break_lock():
  494. did_unlock = True
  495. # Look for lock files that might have been left behind by an interrupted
  496. # git process.
  497. lf = os.path.join(path, 'config.lock')
  498. if os.path.exists(lf):
  499. os.remove(lf)
  500. did_unlock = True
  501. cls.DeleteTmpPackFiles(path)
  502. return did_unlock
  503. def unlock(self):
  504. return self.BreakLocks(self.mirror_path)
  505. @classmethod
  506. def UnlockAll(cls):
  507. cachepath = cls.GetCachePath()
  508. if not cachepath:
  509. return
  510. 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:
  514. if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
  515. gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
  516. elif (dirent.endswith('.lock') and
  517. os.path.isfile(os.path.join(cachepath, dirent))):
  518. repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
  519. unlocked_repos = []
  520. for repo_dir in repo_dirs:
  521. if cls.BreakLocks(repo_dir):
  522. unlocked_repos.append(repo_dir)
  523. return unlocked_repos
  524. @subcommand.usage('[url of repo to check for caching]')
  525. def CMDexists(parser, args):
  526. """Check to see if there already is a cache of the given repo."""
  527. _, args = parser.parse_args(args)
  528. if not len(args) == 1:
  529. parser.error('git cache exists only takes exactly one repo url.')
  530. url = args[0]
  531. mirror = Mirror(url)
  532. if mirror.exists():
  533. print(mirror.mirror_path)
  534. return 0
  535. return 1
  536. @subcommand.usage('[url of repo to create a bootstrap zip file]')
  537. def CMDupdate_bootstrap(parser, args):
  538. """Create and uploads a bootstrap tarball."""
  539. # Lets just assert we can't do this on Windows.
  540. if sys.platform.startswith('win'):
  541. print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
  542. return 1
  543. parser.add_option('--prune', action='store_true',
  544. help='Prune all other cached zipballs of the same repo.')
  545. # First, we need to ensure the cache is populated.
  546. populate_args = args[:]
  547. populate_args.append('--no-bootstrap')
  548. CMDpopulate(parser, populate_args)
  549. # Get the repo directory.
  550. options, args = parser.parse_args(args)
  551. url = args[0]
  552. mirror = Mirror(url)
  553. mirror.update_bootstrap(options.prune)
  554. return 0
  555. @subcommand.usage('[url of repo to add to or update in cache]')
  556. def CMDpopulate(parser, args):
  557. """Ensure that the cache has all up-to-date objects for the given repo."""
  558. parser.add_option('--depth', type='int',
  559. help='Only cache DEPTH commits of history')
  560. parser.add_option('--shallow', '-s', action='store_true',
  561. help='Only cache 10000 commits of history')
  562. parser.add_option('--ref', action='append',
  563. help='Specify additional refs to be fetched')
  564. parser.add_option('--no_bootstrap', '--no-bootstrap',
  565. action='store_true',
  566. help='Don\'t bootstrap from Google Storage')
  567. parser.add_option('--ignore_locks', '--ignore-locks',
  568. action='store_true',
  569. help='Don\'t try to lock repository')
  570. options, args = parser.parse_args(args)
  571. if not len(args) == 1:
  572. parser.error('git cache populate only takes exactly one repo url.')
  573. url = args[0]
  574. mirror = Mirror(url, refs=options.ref)
  575. kwargs = {
  576. 'verbose': options.verbose,
  577. 'shallow': options.shallow,
  578. 'bootstrap': not options.no_bootstrap,
  579. 'ignore_lock': options.ignore_locks,
  580. 'lock_timeout': options.timeout,
  581. }
  582. if options.depth:
  583. kwargs['depth'] = options.depth
  584. mirror.populate(**kwargs)
  585. @subcommand.usage('Fetch new commits into cache and current checkout')
  586. def CMDfetch(parser, args):
  587. """Update mirror, and fetch in cwd."""
  588. parser.add_option('--all', action='store_true', help='Fetch all remotes')
  589. parser.add_option('--no_bootstrap', '--no-bootstrap',
  590. action='store_true',
  591. help='Don\'t (re)bootstrap from Google Storage')
  592. options, args = parser.parse_args(args)
  593. # Figure out which remotes to fetch. This mimics the behavior of regular
  594. # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
  595. # this will NOT try to traverse up the branching structure to find the
  596. # ultimate remote to update.
  597. remotes = []
  598. if options.all:
  599. assert not args, 'fatal: fetch --all does not take a repository argument'
  600. remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
  601. elif args:
  602. remotes = args
  603. else:
  604. current_branch = subprocess.check_output(
  605. [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
  606. if current_branch != 'HEAD':
  607. upstream = subprocess.check_output(
  608. [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
  609. ).strip()
  610. if upstream and upstream != '.':
  611. remotes = [upstream]
  612. if not remotes:
  613. remotes = ['origin']
  614. cachepath = Mirror.GetCachePath()
  615. git_dir = os.path.abspath(subprocess.check_output(
  616. [Mirror.git_exe, 'rev-parse', '--git-dir']))
  617. git_dir = os.path.abspath(git_dir)
  618. if git_dir.startswith(cachepath):
  619. mirror = Mirror.FromPath(git_dir)
  620. mirror.populate(
  621. bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
  622. return 0
  623. for remote in remotes:
  624. remote_url = subprocess.check_output(
  625. [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
  626. if remote_url.startswith(cachepath):
  627. mirror = Mirror.FromPath(remote_url)
  628. mirror.print = lambda *args: None
  629. print('Updating git cache...')
  630. mirror.populate(
  631. bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
  632. subprocess.check_call([Mirror.git_exe, 'fetch', remote])
  633. return 0
  634. @subcommand.usage('[url of repo to unlock, or -a|--all]')
  635. def CMDunlock(parser, args):
  636. """Unlock one or all repos if their lock files are still around."""
  637. parser.add_option('--force', '-f', action='store_true',
  638. help='Actually perform the action')
  639. parser.add_option('--all', '-a', action='store_true',
  640. help='Unlock all repository caches')
  641. options, args = parser.parse_args(args)
  642. if len(args) > 1 or (len(args) == 0 and not options.all):
  643. parser.error('git cache unlock takes exactly one repo url, or --all')
  644. if not options.force:
  645. cachepath = Mirror.GetCachePath()
  646. lockfiles = [os.path.join(cachepath, path)
  647. for path in os.listdir(cachepath)
  648. if path.endswith('.lock') and os.path.isfile(path)]
  649. parser.error('git cache unlock requires -f|--force to do anything. '
  650. 'Refusing to unlock the following repo caches: '
  651. ', '.join(lockfiles))
  652. unlocked_repos = []
  653. if options.all:
  654. unlocked_repos.extend(Mirror.UnlockAll())
  655. else:
  656. m = Mirror(args[0])
  657. if m.unlock():
  658. unlocked_repos.append(m.mirror_path)
  659. if unlocked_repos:
  660. logging.info('Broke locks on these caches:\n %s' % '\n '.join(
  661. unlocked_repos))
  662. class OptionParser(optparse.OptionParser):
  663. """Wrapper class for OptionParser to handle global options."""
  664. def __init__(self, *args, **kwargs):
  665. optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
  666. self.add_option('-c', '--cache-dir',
  667. help='Path to the directory containing the cache')
  668. self.add_option('-v', '--verbose', action='count', default=1,
  669. help='Increase verbosity (can be passed multiple times)')
  670. self.add_option('-q', '--quiet', action='store_true',
  671. help='Suppress all extraneous output')
  672. self.add_option('--timeout', type='int', default=0,
  673. help='Timeout for acquiring cache lock, in seconds')
  674. def parse_args(self, args=None, values=None):
  675. options, args = optparse.OptionParser.parse_args(self, args, values)
  676. if options.quiet:
  677. options.verbose = 0
  678. levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
  679. logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
  680. try:
  681. global_cache_dir = Mirror.GetCachePath()
  682. except RuntimeError:
  683. global_cache_dir = None
  684. if options.cache_dir:
  685. if global_cache_dir and (
  686. os.path.abspath(options.cache_dir) !=
  687. os.path.abspath(global_cache_dir)):
  688. logging.warn('Overriding globally-configured cache directory.')
  689. Mirror.SetCachePath(options.cache_dir)
  690. return options, args
  691. def main(argv):
  692. dispatcher = subcommand.CommandDispatcher(__name__)
  693. return dispatcher.execute(OptionParser(), argv)
  694. if __name__ == '__main__':
  695. try:
  696. sys.exit(main(sys.argv[1:]))
  697. except KeyboardInterrupt:
  698. sys.stderr.write('interrupted\n')
  699. sys.exit(1)