git_cache.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  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. if 'chrome-internal' in self.url:
  192. return 'chrome-git-cache'
  193. else:
  194. return 'chromium-git-cache'
  195. @classmethod
  196. def FromPath(cls, path):
  197. return cls(cls.CacheDirToUrl(path))
  198. @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. @staticmethod
  207. def CacheDirToUrl(path):
  208. """Convert a cache dir path to its corresponding url."""
  209. netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
  210. return 'https://%s' % netpath
  211. @classmethod
  212. def SetCachePath(cls, cachepath):
  213. with cls.cachepath_lock:
  214. setattr(cls, 'cachepath', cachepath)
  215. @classmethod
  216. def GetCachePath(cls):
  217. with cls.cachepath_lock:
  218. if not hasattr(cls, 'cachepath'):
  219. try:
  220. cachepath = subprocess.check_output(
  221. [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
  222. except subprocess.CalledProcessError:
  223. cachepath = None
  224. if not cachepath:
  225. raise RuntimeError(
  226. 'No global cache.cachepath git configuration found.')
  227. setattr(cls, 'cachepath', cachepath)
  228. return getattr(cls, 'cachepath')
  229. def Rename(self, src, dst):
  230. # This is somehow racy on Windows.
  231. # Catching OSError because WindowsError isn't portable and
  232. # pylint complains.
  233. exponential_backoff_retry(
  234. lambda: os.rename(src, dst),
  235. excs=(OSError,),
  236. name='rename [%s] => [%s]' % (src, dst),
  237. printerr=self.print)
  238. def RunGit(self, cmd, **kwargs):
  239. """Run git in a subprocess."""
  240. cwd = kwargs.setdefault('cwd', self.mirror_path)
  241. kwargs.setdefault('print_stdout', False)
  242. kwargs.setdefault('filter_fn', self.print)
  243. env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
  244. env.setdefault('GIT_ASKPASS', 'true')
  245. env.setdefault('SSH_ASKPASS', 'true')
  246. self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
  247. gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
  248. def config(self, cwd=None):
  249. if cwd is None:
  250. cwd = self.mirror_path
  251. # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
  252. try:
  253. self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
  254. except subprocess.CalledProcessError:
  255. # Hard error, need to clobber.
  256. raise ClobberNeeded()
  257. # Don't combine pack files into one big pack file. It's really slow for
  258. # repositories, and there's no way to track progress and make sure it's
  259. # not stuck.
  260. self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
  261. # Allocate more RAM for cache-ing delta chains, for better performance
  262. # of "Resolving deltas".
  263. self.RunGit(['config', 'core.deltaBaseCacheLimit',
  264. gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
  265. self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
  266. self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
  267. '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
  268. for spec, value_regex in self.fetch_specs:
  269. self.RunGit(
  270. ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
  271. cwd=cwd)
  272. def bootstrap_repo(self, directory):
  273. """Bootstrap the repo from Google Stroage if possible.
  274. More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
  275. """
  276. python_fallback = False
  277. if (sys.platform.startswith('win') and
  278. not gclient_utils.FindExecutable('7z')):
  279. python_fallback = True
  280. elif sys.platform.startswith('darwin'):
  281. # The OSX version of unzip doesn't support zip64.
  282. python_fallback = True
  283. elif not gclient_utils.FindExecutable('unzip'):
  284. python_fallback = True
  285. gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
  286. gsutil = Gsutil(self.gsutil_exe, boto_path=None)
  287. # Get the most recent version of the zipfile.
  288. _, ls_out, _ = gsutil.check_call('ls', gs_folder)
  289. ls_out_sorted = sorted(ls_out.splitlines())
  290. if not ls_out_sorted:
  291. # This repo is not on Google Storage.
  292. return False
  293. latest_checkout = ls_out_sorted[-1]
  294. # Download zip file to a temporary directory.
  295. try:
  296. tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
  297. self.print('Downloading %s' % latest_checkout)
  298. code = gsutil.call('cp', latest_checkout, tempdir)
  299. if code:
  300. return False
  301. filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
  302. # Unpack the file with 7z on Windows, unzip on linux, or fallback.
  303. if not python_fallback:
  304. if sys.platform.startswith('win'):
  305. cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
  306. else:
  307. cmd = ['unzip', filename, '-d', directory]
  308. retcode = subprocess.call(cmd)
  309. else:
  310. try:
  311. with zipfile.ZipFile(filename, 'r') as f:
  312. f.printdir()
  313. f.extractall(directory)
  314. except Exception as e:
  315. self.print('Encountered error: %s' % str(e), file=sys.stderr)
  316. retcode = 1
  317. else:
  318. retcode = 0
  319. finally:
  320. # Clean up the downloaded zipfile.
  321. #
  322. # This is somehow racy on Windows.
  323. # Catching OSError because WindowsError isn't portable and
  324. # pylint complains.
  325. exponential_backoff_retry(
  326. lambda: gclient_utils.rm_file_or_tree(tempdir),
  327. excs=(OSError,),
  328. name='rmtree [%s]' % (tempdir,),
  329. printerr=self.print)
  330. if retcode:
  331. self.print(
  332. 'Extracting bootstrap zipfile %s failed.\n'
  333. 'Resuming normal operations.' % filename)
  334. return False
  335. return True
  336. def exists(self):
  337. return os.path.isfile(os.path.join(self.mirror_path, 'config'))
  338. def _preserve_fetchspec(self):
  339. """Read and preserve remote.origin.fetch from an existing mirror.
  340. This modifies self.fetch_specs.
  341. """
  342. if not self.exists():
  343. return
  344. try:
  345. config_fetchspecs = subprocess.check_output(
  346. [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
  347. cwd=self.mirror_path)
  348. for fetchspec in config_fetchspecs.splitlines():
  349. self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
  350. except subprocess.CalledProcessError:
  351. logging.warn('Tried and failed to preserve remote.origin.fetch from the '
  352. 'existing cache directory. You may need to manually edit '
  353. '%s and "git cache fetch" again.'
  354. % os.path.join(self.mirror_path, 'config'))
  355. def _ensure_bootstrapped(self, depth, bootstrap, force=False):
  356. tempdir = None
  357. pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
  358. pack_files = []
  359. if os.path.isdir(pack_dir):
  360. pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
  361. should_bootstrap = (force or
  362. not self.exists() or
  363. len(pack_files) > GC_AUTOPACKLIMIT)
  364. if should_bootstrap:
  365. if self.exists():
  366. # Re-bootstrapping an existing mirror; preserve existing fetch spec.
  367. self._preserve_fetchspec()
  368. tempdir = tempfile.mkdtemp(
  369. prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
  370. bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
  371. if bootstrapped:
  372. # Bootstrap succeeded; delete previous cache, if any.
  373. gclient_utils.rmtree(self.mirror_path)
  374. elif not self.exists():
  375. # Bootstrap failed, no previous cache; start with a bare git dir.
  376. self.RunGit(['init', '--bare'], cwd=tempdir)
  377. else:
  378. # Bootstrap failed, previous cache exists; warn and continue.
  379. logging.warn(
  380. 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
  381. 'but failed. Continuing with non-optimized repository.'
  382. % len(pack_files))
  383. gclient_utils.rmtree(tempdir)
  384. tempdir = None
  385. else:
  386. if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
  387. logging.warn(
  388. 'Shallow fetch requested, but repo cache already exists.')
  389. return tempdir
  390. def _fetch(self, rundir, verbose, depth):
  391. self.config(rundir)
  392. v = []
  393. d = []
  394. if verbose:
  395. v = ['-v', '--progress']
  396. if depth:
  397. d = ['--depth', str(depth)]
  398. fetch_cmd = ['fetch'] + v + d + ['origin']
  399. fetch_specs = subprocess.check_output(
  400. [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
  401. cwd=rundir).strip().splitlines()
  402. for spec in fetch_specs:
  403. try:
  404. self.print('Fetching %s' % spec)
  405. self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
  406. except subprocess.CalledProcessError:
  407. if spec == '+refs/heads/*:refs/heads/*':
  408. raise ClobberNeeded() # Corrupted cache.
  409. logging.warn('Fetch of %s failed' % spec)
  410. def populate(self, depth=None, shallow=False, bootstrap=False,
  411. verbose=False, ignore_lock=False, lock_timeout=0):
  412. assert self.GetCachePath()
  413. if shallow and not depth:
  414. depth = 10000
  415. gclient_utils.safe_makedirs(self.GetCachePath())
  416. lockfile = Lockfile(self.mirror_path, lock_timeout)
  417. if not ignore_lock:
  418. lockfile.lock()
  419. tempdir = None
  420. try:
  421. tempdir = self._ensure_bootstrapped(depth, bootstrap)
  422. rundir = tempdir or self.mirror_path
  423. self._fetch(rundir, verbose, depth)
  424. except ClobberNeeded:
  425. # This is a major failure, we need to clean and force a bootstrap.
  426. gclient_utils.rmtree(rundir)
  427. self.print(GIT_CACHE_CORRUPT_MESSAGE)
  428. tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
  429. assert tempdir
  430. self._fetch(tempdir or self.mirror_path, verbose, depth)
  431. finally:
  432. if tempdir:
  433. if os.path.exists(self.mirror_path):
  434. gclient_utils.rmtree(self.mirror_path)
  435. self.Rename(tempdir, self.mirror_path)
  436. if not ignore_lock:
  437. lockfile.unlock()
  438. def update_bootstrap(self, prune=False):
  439. # The files are named <git number>.zip
  440. gen_number = subprocess.check_output(
  441. [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
  442. # Run Garbage Collect to compress packfile.
  443. self.RunGit(['gc', '--prune=all'])
  444. # Creating a temp file and then deleting it ensures we can use this name.
  445. _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
  446. os.remove(tmp_zipfile)
  447. subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
  448. gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
  449. gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
  450. dest_name = '%s/%s.zip' % (gs_folder, gen_number)
  451. gsutil.call('cp', tmp_zipfile, dest_name)
  452. os.remove(tmp_zipfile)
  453. # Remove all other files in the same directory.
  454. if prune:
  455. _, ls_out, _ = gsutil.check_call('ls', gs_folder)
  456. for filename in ls_out.splitlines():
  457. if filename == dest_name:
  458. continue
  459. gsutil.call('rm', filename)
  460. @staticmethod
  461. def DeleteTmpPackFiles(path):
  462. pack_dir = os.path.join(path, 'objects', 'pack')
  463. if not os.path.isdir(pack_dir):
  464. return
  465. pack_files = [f for f in os.listdir(pack_dir) if
  466. f.startswith('.tmp-') or f.startswith('tmp_pack_')]
  467. for f in pack_files:
  468. f = os.path.join(pack_dir, f)
  469. try:
  470. os.remove(f)
  471. logging.warn('Deleted stale temporary pack file %s' % f)
  472. except OSError:
  473. logging.warn('Unable to delete temporary pack file %s' % f)
  474. @classmethod
  475. def BreakLocks(cls, path):
  476. did_unlock = False
  477. lf = Lockfile(path)
  478. if lf.break_lock():
  479. did_unlock = True
  480. # Look for lock files that might have been left behind by an interrupted
  481. # git process.
  482. lf = os.path.join(path, 'config.lock')
  483. if os.path.exists(lf):
  484. os.remove(lf)
  485. did_unlock = True
  486. cls.DeleteTmpPackFiles(path)
  487. return did_unlock
  488. def unlock(self):
  489. return self.BreakLocks(self.mirror_path)
  490. @classmethod
  491. def UnlockAll(cls):
  492. cachepath = cls.GetCachePath()
  493. if not cachepath:
  494. return
  495. dirlist = os.listdir(cachepath)
  496. repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
  497. if os.path.isdir(os.path.join(cachepath, path))])
  498. for dirent in dirlist:
  499. if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
  500. gclient_utils.rm_file_or_tree(os.path.join(cachepath, dirent))
  501. elif (dirent.endswith('.lock') and
  502. os.path.isfile(os.path.join(cachepath, dirent))):
  503. repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
  504. unlocked_repos = []
  505. for repo_dir in repo_dirs:
  506. if cls.BreakLocks(repo_dir):
  507. unlocked_repos.append(repo_dir)
  508. return unlocked_repos
  509. @subcommand.usage('[url of repo to check for caching]')
  510. def CMDexists(parser, args):
  511. """Check to see if there already is a cache of the given repo."""
  512. _, args = parser.parse_args(args)
  513. if not len(args) == 1:
  514. parser.error('git cache exists only takes exactly one repo url.')
  515. url = args[0]
  516. mirror = Mirror(url)
  517. if mirror.exists():
  518. print(mirror.mirror_path)
  519. return 0
  520. return 1
  521. @subcommand.usage('[url of repo to create a bootstrap zip file]')
  522. def CMDupdate_bootstrap(parser, args):
  523. """Create and uploads a bootstrap tarball."""
  524. # Lets just assert we can't do this on Windows.
  525. if sys.platform.startswith('win'):
  526. print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
  527. return 1
  528. parser.add_option('--prune', action='store_true',
  529. help='Prune all other cached zipballs of the same repo.')
  530. # First, we need to ensure the cache is populated.
  531. populate_args = args[:]
  532. populate_args.append('--no-bootstrap')
  533. CMDpopulate(parser, populate_args)
  534. # Get the repo directory.
  535. options, args = parser.parse_args(args)
  536. url = args[0]
  537. mirror = Mirror(url)
  538. mirror.update_bootstrap(options.prune)
  539. return 0
  540. @subcommand.usage('[url of repo to add to or update in cache]')
  541. def CMDpopulate(parser, args):
  542. """Ensure that the cache has all up-to-date objects for the given repo."""
  543. parser.add_option('--depth', type='int',
  544. help='Only cache DEPTH commits of history')
  545. parser.add_option('--shallow', '-s', action='store_true',
  546. help='Only cache 10000 commits of history')
  547. parser.add_option('--ref', action='append',
  548. help='Specify additional refs to be fetched')
  549. parser.add_option('--no_bootstrap', '--no-bootstrap',
  550. action='store_true',
  551. help='Don\'t bootstrap from Google Storage')
  552. parser.add_option('--ignore_locks', '--ignore-locks',
  553. action='store_true',
  554. help='Don\'t try to lock repository')
  555. options, args = parser.parse_args(args)
  556. if not len(args) == 1:
  557. parser.error('git cache populate only takes exactly one repo url.')
  558. url = args[0]
  559. mirror = Mirror(url, refs=options.ref)
  560. kwargs = {
  561. 'verbose': options.verbose,
  562. 'shallow': options.shallow,
  563. 'bootstrap': not options.no_bootstrap,
  564. 'ignore_lock': options.ignore_locks,
  565. 'lock_timeout': options.timeout,
  566. }
  567. if options.depth:
  568. kwargs['depth'] = options.depth
  569. mirror.populate(**kwargs)
  570. @subcommand.usage('Fetch new commits into cache and current checkout')
  571. def CMDfetch(parser, args):
  572. """Update mirror, and fetch in cwd."""
  573. parser.add_option('--all', action='store_true', help='Fetch all remotes')
  574. parser.add_option('--no_bootstrap', '--no-bootstrap',
  575. action='store_true',
  576. help='Don\'t (re)bootstrap from Google Storage')
  577. options, args = parser.parse_args(args)
  578. # Figure out which remotes to fetch. This mimics the behavior of regular
  579. # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
  580. # this will NOT try to traverse up the branching structure to find the
  581. # ultimate remote to update.
  582. remotes = []
  583. if options.all:
  584. assert not args, 'fatal: fetch --all does not take a repository argument'
  585. remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
  586. elif args:
  587. remotes = args
  588. else:
  589. current_branch = subprocess.check_output(
  590. [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
  591. if current_branch != 'HEAD':
  592. upstream = subprocess.check_output(
  593. [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
  594. ).strip()
  595. if upstream and upstream != '.':
  596. remotes = [upstream]
  597. if not remotes:
  598. remotes = ['origin']
  599. cachepath = Mirror.GetCachePath()
  600. git_dir = os.path.abspath(subprocess.check_output(
  601. [Mirror.git_exe, 'rev-parse', '--git-dir']))
  602. git_dir = os.path.abspath(git_dir)
  603. if git_dir.startswith(cachepath):
  604. mirror = Mirror.FromPath(git_dir)
  605. mirror.populate(
  606. bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
  607. return 0
  608. for remote in remotes:
  609. remote_url = subprocess.check_output(
  610. [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
  611. if remote_url.startswith(cachepath):
  612. mirror = Mirror.FromPath(remote_url)
  613. mirror.print = lambda *args: None
  614. print('Updating git cache...')
  615. mirror.populate(
  616. bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
  617. subprocess.check_call([Mirror.git_exe, 'fetch', remote])
  618. return 0
  619. @subcommand.usage('[url of repo to unlock, or -a|--all]')
  620. def CMDunlock(parser, args):
  621. """Unlock one or all repos if their lock files are still around."""
  622. parser.add_option('--force', '-f', action='store_true',
  623. help='Actually perform the action')
  624. parser.add_option('--all', '-a', action='store_true',
  625. help='Unlock all repository caches')
  626. options, args = parser.parse_args(args)
  627. if len(args) > 1 or (len(args) == 0 and not options.all):
  628. parser.error('git cache unlock takes exactly one repo url, or --all')
  629. if not options.force:
  630. cachepath = Mirror.GetCachePath()
  631. lockfiles = [os.path.join(cachepath, path)
  632. for path in os.listdir(cachepath)
  633. if path.endswith('.lock') and os.path.isfile(path)]
  634. parser.error('git cache unlock requires -f|--force to do anything. '
  635. 'Refusing to unlock the following repo caches: '
  636. ', '.join(lockfiles))
  637. unlocked_repos = []
  638. if options.all:
  639. unlocked_repos.extend(Mirror.UnlockAll())
  640. else:
  641. m = Mirror(args[0])
  642. if m.unlock():
  643. unlocked_repos.append(m.mirror_path)
  644. if unlocked_repos:
  645. logging.info('Broke locks on these caches:\n %s' % '\n '.join(
  646. unlocked_repos))
  647. class OptionParser(optparse.OptionParser):
  648. """Wrapper class for OptionParser to handle global options."""
  649. def __init__(self, *args, **kwargs):
  650. optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
  651. self.add_option('-c', '--cache-dir',
  652. help='Path to the directory containing the cache')
  653. self.add_option('-v', '--verbose', action='count', default=1,
  654. help='Increase verbosity (can be passed multiple times)')
  655. self.add_option('-q', '--quiet', action='store_true',
  656. help='Suppress all extraneous output')
  657. self.add_option('--timeout', type='int', default=0,
  658. help='Timeout for acquiring cache lock, in seconds')
  659. def parse_args(self, args=None, values=None):
  660. options, args = optparse.OptionParser.parse_args(self, args, values)
  661. if options.quiet:
  662. options.verbose = 0
  663. levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
  664. logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
  665. try:
  666. global_cache_dir = Mirror.GetCachePath()
  667. except RuntimeError:
  668. global_cache_dir = None
  669. if options.cache_dir:
  670. if global_cache_dir and (
  671. os.path.abspath(options.cache_dir) !=
  672. os.path.abspath(global_cache_dir)):
  673. logging.warn('Overriding globally-configured cache directory.')
  674. Mirror.SetCachePath(options.cache_dir)
  675. return options, args
  676. def main(argv):
  677. dispatcher = subcommand.CommandDispatcher(__name__)
  678. return dispatcher.execute(OptionParser(), argv)
  679. if __name__ == '__main__':
  680. try:
  681. sys.exit(main(sys.argv[1:]))
  682. except KeyboardInterrupt:
  683. sys.stderr.write('interrupted\n')
  684. sys.exit(1)