git_cache.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  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 time
  14. import shutil
  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=E0602
  27. WinErr = WindowsError
  28. except NameError:
  29. class WinErr(Exception):
  30. pass
  31. class LockError(Exception):
  32. pass
  33. class RefsHeadsFailedToFetch(Exception):
  34. pass
  35. class Lockfile(object):
  36. """Class to represent a cross-platform process-specific lockfile."""
  37. def __init__(self, path):
  38. self.path = os.path.abspath(path)
  39. self.lockfile = self.path + ".lock"
  40. self.pid = os.getpid()
  41. def _read_pid(self):
  42. """Read the pid stored in the lockfile.
  43. Note: This method is potentially racy. By the time it returns the lockfile
  44. may have been unlocked, removed, or stolen by some other process.
  45. """
  46. try:
  47. with open(self.lockfile, 'r') as f:
  48. pid = int(f.readline().strip())
  49. except (IOError, ValueError):
  50. pid = None
  51. return pid
  52. def _make_lockfile(self):
  53. """Safely creates a lockfile containing the current pid."""
  54. open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
  55. fd = os.open(self.lockfile, open_flags, 0o644)
  56. f = os.fdopen(fd, 'w')
  57. print(self.pid, file=f)
  58. f.close()
  59. def _remove_lockfile(self):
  60. """Delete the lockfile. Complains (implicitly) if it doesn't exist.
  61. See gclient_utils.py:rmtree docstring for more explanation on the
  62. windows case.
  63. """
  64. if sys.platform == 'win32':
  65. lockfile = os.path.normcase(self.lockfile)
  66. for _ in xrange(3):
  67. exitcode = subprocess.call(['cmd.exe', '/c',
  68. 'del', '/f', '/q', lockfile])
  69. if exitcode == 0:
  70. return
  71. time.sleep(3)
  72. raise LockError('Failed to remove lock: %s' % lockfile)
  73. else:
  74. os.remove(self.lockfile)
  75. def lock(self):
  76. """Acquire the lock.
  77. Note: This is a NON-BLOCKING FAIL-FAST operation.
  78. Do. Or do not. There is no try.
  79. """
  80. try:
  81. self._make_lockfile()
  82. except OSError as e:
  83. if e.errno == errno.EEXIST:
  84. raise LockError("%s is already locked" % self.path)
  85. else:
  86. raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
  87. def unlock(self):
  88. """Release the lock."""
  89. try:
  90. if not self.is_locked():
  91. raise LockError("%s is not locked" % self.path)
  92. if not self.i_am_locking():
  93. raise LockError("%s is locked, but not by me" % self.path)
  94. self._remove_lockfile()
  95. except WinErr:
  96. # Windows is unreliable when it comes to file locking. YMMV.
  97. pass
  98. def break_lock(self):
  99. """Remove the lock, even if it was created by someone else."""
  100. try:
  101. self._remove_lockfile()
  102. return True
  103. except OSError as exc:
  104. if exc.errno == errno.ENOENT:
  105. return False
  106. else:
  107. raise
  108. def is_locked(self):
  109. """Test if the file is locked by anyone.
  110. Note: This method is potentially racy. By the time it returns the lockfile
  111. may have been unlocked, removed, or stolen by some other process.
  112. """
  113. return os.path.exists(self.lockfile)
  114. def i_am_locking(self):
  115. """Test if the file is locked by this process."""
  116. return self.is_locked() and self.pid == self._read_pid()
  117. class Mirror(object):
  118. git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
  119. gsutil_exe = os.path.join(
  120. os.path.dirname(os.path.abspath(__file__)),
  121. 'third_party', 'gsutil', 'gsutil')
  122. def __init__(self, url, refs=None, print_func=None):
  123. self.url = url
  124. self.refs = refs or []
  125. self.basedir = self.UrlToCacheDir(url)
  126. self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
  127. self.print = print_func or print
  128. @property
  129. def bootstrap_bucket(self):
  130. if 'chrome-internal' in self.url:
  131. return 'chrome-git-cache'
  132. else:
  133. return 'chromium-git-cache'
  134. @classmethod
  135. def FromPath(cls, path):
  136. return cls(cls.CacheDirToUrl(path))
  137. @staticmethod
  138. def UrlToCacheDir(url):
  139. """Convert a git url to a normalized form for the cache dir path."""
  140. parsed = urlparse.urlparse(url)
  141. norm_url = parsed.netloc + parsed.path
  142. if norm_url.endswith('.git'):
  143. norm_url = norm_url[:-len('.git')]
  144. return norm_url.replace('-', '--').replace('/', '-').lower()
  145. @staticmethod
  146. def CacheDirToUrl(path):
  147. """Convert a cache dir path to its corresponding url."""
  148. netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
  149. return 'https://%s' % netpath
  150. @staticmethod
  151. def FindExecutable(executable):
  152. """This mimics the "which" utility."""
  153. path_folders = os.environ.get('PATH').split(os.pathsep)
  154. for path_folder in path_folders:
  155. target = os.path.join(path_folder, executable)
  156. # Just incase we have some ~/blah paths.
  157. target = os.path.abspath(os.path.expanduser(target))
  158. if os.path.isfile(target) and os.access(target, os.X_OK):
  159. return target
  160. if sys.platform.startswith('win'):
  161. for suffix in ('.bat', '.cmd', '.exe'):
  162. alt_target = target + suffix
  163. if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
  164. return alt_target
  165. return None
  166. @classmethod
  167. def SetCachePath(cls, cachepath):
  168. setattr(cls, 'cachepath', cachepath)
  169. @classmethod
  170. def GetCachePath(cls):
  171. if not hasattr(cls, 'cachepath'):
  172. try:
  173. cachepath = subprocess.check_output(
  174. [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip()
  175. except subprocess.CalledProcessError:
  176. cachepath = None
  177. if not cachepath:
  178. raise RuntimeError('No global cache.cachepath git configuration found.')
  179. setattr(cls, 'cachepath', cachepath)
  180. return getattr(cls, 'cachepath')
  181. def RunGit(self, cmd, **kwargs):
  182. """Run git in a subprocess."""
  183. cwd = kwargs.setdefault('cwd', self.mirror_path)
  184. kwargs.setdefault('print_stdout', False)
  185. kwargs.setdefault('filter_fn', self.print)
  186. env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
  187. env.setdefault('GIT_ASKPASS', 'true')
  188. env.setdefault('SSH_ASKPASS', 'true')
  189. self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd))
  190. gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs)
  191. def config(self, cwd=None):
  192. if cwd is None:
  193. cwd = self.mirror_path
  194. # Don't run git-gc in a daemon. Bad things can happen if it gets killed.
  195. self.RunGit(['config', 'gc.autodetach', '0'], cwd=cwd)
  196. # Don't combine pack files into one big pack file. It's really slow for
  197. # repositories, and there's no way to track progress and make sure it's
  198. # not stuck.
  199. self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd)
  200. # Allocate more RAM for cache-ing delta chains, for better performance
  201. # of "Resolving deltas".
  202. self.RunGit(['config', 'core.deltaBaseCacheLimit',
  203. gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd)
  204. self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
  205. self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
  206. '+refs/heads/*:refs/heads/*'], cwd=cwd)
  207. for ref in self.refs:
  208. ref = ref.lstrip('+').rstrip('/')
  209. if ref.startswith('refs/'):
  210. refspec = '+%s:%s' % (ref, ref)
  211. else:
  212. refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
  213. self.RunGit(['config', '--add', 'remote.origin.fetch', refspec], cwd=cwd)
  214. def bootstrap_repo(self, directory):
  215. """Bootstrap the repo from Google Stroage if possible.
  216. More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing().
  217. """
  218. python_fallback = False
  219. if sys.platform.startswith('win') and not self.FindExecutable('7z'):
  220. python_fallback = True
  221. elif sys.platform.startswith('darwin'):
  222. # The OSX version of unzip doesn't support zip64.
  223. python_fallback = True
  224. elif not self.FindExecutable('unzip'):
  225. python_fallback = True
  226. gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
  227. gsutil = Gsutil(self.gsutil_exe, boto_path=None, bypass_prodaccess=True)
  228. # Get the most recent version of the zipfile.
  229. _, ls_out, _ = gsutil.check_call('ls', gs_folder)
  230. ls_out_sorted = sorted(ls_out.splitlines())
  231. if not ls_out_sorted:
  232. # This repo is not on Google Storage.
  233. return False
  234. latest_checkout = ls_out_sorted[-1]
  235. # Download zip file to a temporary directory.
  236. try:
  237. tempdir = tempfile.mkdtemp(prefix='_cache_tmp', dir=self.GetCachePath())
  238. self.print('Downloading %s' % latest_checkout)
  239. code = gsutil.call('cp', latest_checkout, tempdir)
  240. if code:
  241. return False
  242. filename = os.path.join(tempdir, latest_checkout.split('/')[-1])
  243. # Unpack the file with 7z on Windows, unzip on linux, or fallback.
  244. if not python_fallback:
  245. if sys.platform.startswith('win'):
  246. cmd = ['7z', 'x', '-o%s' % directory, '-tzip', filename]
  247. else:
  248. cmd = ['unzip', filename, '-d', directory]
  249. retcode = subprocess.call(cmd)
  250. else:
  251. try:
  252. with zipfile.ZipFile(filename, 'r') as f:
  253. f.printdir()
  254. f.extractall(directory)
  255. except Exception as e:
  256. self.print('Encountered error: %s' % str(e), file=sys.stderr)
  257. retcode = 1
  258. else:
  259. retcode = 0
  260. finally:
  261. # Clean up the downloaded zipfile.
  262. gclient_utils.rmtree(tempdir)
  263. if retcode:
  264. self.print(
  265. 'Extracting bootstrap zipfile %s failed.\n'
  266. 'Resuming normal operations.' % filename)
  267. return False
  268. return True
  269. def exists(self):
  270. return os.path.isfile(os.path.join(self.mirror_path, 'config'))
  271. def _ensure_bootstrapped(self, depth, bootstrap, force=False):
  272. tempdir = None
  273. config_file = os.path.join(self.mirror_path, 'config')
  274. pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
  275. pack_files = []
  276. if os.path.isdir(pack_dir):
  277. pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
  278. should_bootstrap = (force or
  279. not os.path.exists(config_file) or
  280. len(pack_files) > GC_AUTOPACKLIMIT)
  281. if should_bootstrap:
  282. tempdir = tempfile.mkdtemp(
  283. prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
  284. bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
  285. if bootstrapped:
  286. # Bootstrap succeeded; delete previous cache, if any.
  287. try:
  288. # Try to move folder to tempdir if possible.
  289. defunct_dir = tempfile.mkdtemp()
  290. shutil.move(self.mirror_path, defunct_dir)
  291. self.print('Moved defunct directory for repository %s from %s to %s'
  292. % (self.url, self.mirror_path, defunct_dir))
  293. except Exception:
  294. gclient_utils.rmtree(self.mirror_path)
  295. elif not os.path.exists(config_file):
  296. # Bootstrap failed, no previous cache; start with a bare git dir.
  297. self.RunGit(['init', '--bare'], cwd=tempdir)
  298. else:
  299. # Bootstrap failed, previous cache exists; warn and continue.
  300. logging.warn(
  301. 'Git cache has a lot of pack files (%d). Tried to re-bootstrap '
  302. 'but failed. Continuing with non-optimized repository.'
  303. % len(pack_files))
  304. gclient_utils.rmtree(tempdir)
  305. tempdir = None
  306. else:
  307. if depth and os.path.exists(os.path.join(self.mirror_path, 'shallow')):
  308. logging.warn(
  309. 'Shallow fetch requested, but repo cache already exists.')
  310. return tempdir
  311. def _fetch(self, rundir, verbose, depth):
  312. self.config(rundir)
  313. v = []
  314. d = []
  315. if verbose:
  316. v = ['-v', '--progress']
  317. if depth:
  318. d = ['--depth', str(depth)]
  319. fetch_cmd = ['fetch'] + v + d + ['origin']
  320. fetch_specs = subprocess.check_output(
  321. [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
  322. cwd=rundir).strip().splitlines()
  323. for spec in fetch_specs:
  324. try:
  325. self.print('Fetching %s' % spec)
  326. self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True)
  327. except subprocess.CalledProcessError:
  328. if spec == '+refs/heads/*:refs/heads/*':
  329. raise RefsHeadsFailedToFetch
  330. logging.warn('Fetch of %s failed' % spec)
  331. def populate(self, depth=None, shallow=False, bootstrap=False,
  332. verbose=False, ignore_lock=False):
  333. assert self.GetCachePath()
  334. if shallow and not depth:
  335. depth = 10000
  336. gclient_utils.safe_makedirs(self.GetCachePath())
  337. lockfile = Lockfile(self.mirror_path)
  338. if not ignore_lock:
  339. lockfile.lock()
  340. tempdir = None
  341. try:
  342. tempdir = self._ensure_bootstrapped(depth, bootstrap)
  343. rundir = tempdir or self.mirror_path
  344. self._fetch(rundir, verbose, depth)
  345. except RefsHeadsFailedToFetch:
  346. # This is a major failure, we need to clean and force a bootstrap.
  347. gclient_utils.rmtree(rundir)
  348. self.print(GIT_CACHE_CORRUPT_MESSAGE)
  349. tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True)
  350. assert tempdir
  351. self._fetch(tempdir or self.mirror_path, verbose, depth)
  352. finally:
  353. if tempdir:
  354. try:
  355. os.rename(tempdir, self.mirror_path)
  356. except OSError as e:
  357. # This is somehow racy on Windows.
  358. # Catching OSError because WindowsError isn't portable and
  359. # pylint complains.
  360. self.print('Error moving %s to %s: %s' % (tempdir, self.mirror_path,
  361. str(e)))
  362. if not ignore_lock:
  363. lockfile.unlock()
  364. def update_bootstrap(self, prune=False):
  365. # The files are named <git number>.zip
  366. gen_number = subprocess.check_output(
  367. [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip()
  368. self.RunGit(['gc']) # Run Garbage Collect to compress packfile.
  369. # Creating a temp file and then deleting it ensures we can use this name.
  370. _, tmp_zipfile = tempfile.mkstemp(suffix='.zip')
  371. os.remove(tmp_zipfile)
  372. subprocess.call(['zip', '-r', tmp_zipfile, '.'], cwd=self.mirror_path)
  373. gsutil = Gsutil(path=self.gsutil_exe, boto_path=None)
  374. gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
  375. dest_name = '%s/%s.zip' % (gs_folder, gen_number)
  376. gsutil.call('cp', tmp_zipfile, dest_name)
  377. os.remove(tmp_zipfile)
  378. # Remove all other files in the same directory.
  379. if prune:
  380. _, ls_out, _ = gsutil.check_call('ls', gs_folder)
  381. for filename in ls_out.splitlines():
  382. if filename == dest_name:
  383. continue
  384. gsutil.call('rm', filename)
  385. @staticmethod
  386. def DeleteTmpPackFiles(path):
  387. pack_dir = os.path.join(path, 'objects', 'pack')
  388. if not os.path.isdir(pack_dir):
  389. return
  390. pack_files = [f for f in os.listdir(pack_dir) if
  391. f.startswith('.tmp-') or f.startswith('tmp_pack_')]
  392. for f in pack_files:
  393. f = os.path.join(pack_dir, f)
  394. try:
  395. os.remove(f)
  396. logging.warn('Deleted stale temporary pack file %s' % f)
  397. except OSError:
  398. logging.warn('Unable to delete temporary pack file %s' % f)
  399. @classmethod
  400. def BreakLocks(cls, path):
  401. did_unlock = False
  402. lf = Lockfile(path)
  403. if lf.break_lock():
  404. did_unlock = True
  405. # Look for lock files that might have been left behind by an interrupted
  406. # git process.
  407. lf = os.path.join(path, 'config.lock')
  408. if os.path.exists(lf):
  409. os.remove(lf)
  410. did_unlock = True
  411. cls.DeleteTmpPackFiles(path)
  412. return did_unlock
  413. def unlock(self):
  414. return self.BreakLocks(self.mirror_path)
  415. @classmethod
  416. def UnlockAll(cls):
  417. cachepath = cls.GetCachePath()
  418. if not cachepath:
  419. return
  420. dirlist = os.listdir(cachepath)
  421. repo_dirs = set([os.path.join(cachepath, path) for path in dirlist
  422. if os.path.isdir(os.path.join(cachepath, path))])
  423. for dirent in dirlist:
  424. if dirent.startswith('_cache_tmp') or dirent.startswith('tmp'):
  425. gclient_utils.rmtree(os.path.join(cachepath, dirent))
  426. elif (dirent.endswith('.lock') and
  427. os.path.isfile(os.path.join(cachepath, dirent))):
  428. repo_dirs.add(os.path.join(cachepath, dirent[:-5]))
  429. unlocked_repos = []
  430. for repo_dir in repo_dirs:
  431. if cls.BreakLocks(repo_dir):
  432. unlocked_repos.append(repo_dir)
  433. return unlocked_repos
  434. @subcommand.usage('[url of repo to check for caching]')
  435. def CMDexists(parser, args):
  436. """Check to see if there already is a cache of the given repo."""
  437. _, args = parser.parse_args(args)
  438. if not len(args) == 1:
  439. parser.error('git cache exists only takes exactly one repo url.')
  440. url = args[0]
  441. mirror = Mirror(url)
  442. if mirror.exists():
  443. print(mirror.mirror_path)
  444. return 0
  445. return 1
  446. @subcommand.usage('[url of repo to create a bootstrap zip file]')
  447. def CMDupdate_bootstrap(parser, args):
  448. """Create and uploads a bootstrap tarball."""
  449. # Lets just assert we can't do this on Windows.
  450. if sys.platform.startswith('win'):
  451. print('Sorry, update bootstrap will not work on Windows.', file=sys.stderr)
  452. return 1
  453. parser.add_option('--prune', action='store_true',
  454. help='Prune all other cached zipballs of the same repo.')
  455. # First, we need to ensure the cache is populated.
  456. populate_args = args[:]
  457. populate_args.append('--no-bootstrap')
  458. CMDpopulate(parser, populate_args)
  459. # Get the repo directory.
  460. options, args = parser.parse_args(args)
  461. url = args[0]
  462. mirror = Mirror(url)
  463. mirror.update_bootstrap(options.prune)
  464. return 0
  465. @subcommand.usage('[url of repo to add to or update in cache]')
  466. def CMDpopulate(parser, args):
  467. """Ensure that the cache has all up-to-date objects for the given repo."""
  468. parser.add_option('--depth', type='int',
  469. help='Only cache DEPTH commits of history')
  470. parser.add_option('--shallow', '-s', action='store_true',
  471. help='Only cache 10000 commits of history')
  472. parser.add_option('--ref', action='append',
  473. help='Specify additional refs to be fetched')
  474. parser.add_option('--no_bootstrap', '--no-bootstrap',
  475. action='store_true',
  476. help='Don\'t bootstrap from Google Storage')
  477. parser.add_option('--ignore_locks', '--ignore-locks',
  478. action='store_true',
  479. help='Don\'t try to lock repository')
  480. options, args = parser.parse_args(args)
  481. if not len(args) == 1:
  482. parser.error('git cache populate only takes exactly one repo url.')
  483. url = args[0]
  484. mirror = Mirror(url, refs=options.ref)
  485. kwargs = {
  486. 'verbose': options.verbose,
  487. 'shallow': options.shallow,
  488. 'bootstrap': not options.no_bootstrap,
  489. 'ignore_lock': options.ignore_locks,
  490. }
  491. if options.depth:
  492. kwargs['depth'] = options.depth
  493. mirror.populate(**kwargs)
  494. @subcommand.usage('Fetch new commits into cache and current checkout')
  495. def CMDfetch(parser, args):
  496. """Update mirror, and fetch in cwd."""
  497. parser.add_option('--all', action='store_true', help='Fetch all remotes')
  498. options, args = parser.parse_args(args)
  499. # Figure out which remotes to fetch. This mimics the behavior of regular
  500. # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches,
  501. # this will NOT try to traverse up the branching structure to find the
  502. # ultimate remote to update.
  503. remotes = []
  504. if options.all:
  505. assert not args, 'fatal: fetch --all does not take a repository argument'
  506. remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines()
  507. elif args:
  508. remotes = args
  509. else:
  510. current_branch = subprocess.check_output(
  511. [Mirror.git_exe, 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
  512. if current_branch != 'HEAD':
  513. upstream = subprocess.check_output(
  514. [Mirror.git_exe, 'config', 'branch.%s.remote' % current_branch]
  515. ).strip()
  516. if upstream and upstream != '.':
  517. remotes = [upstream]
  518. if not remotes:
  519. remotes = ['origin']
  520. cachepath = Mirror.GetCachePath()
  521. git_dir = os.path.abspath(subprocess.check_output(
  522. [Mirror.git_exe, 'rev-parse', '--git-dir']))
  523. git_dir = os.path.abspath(git_dir)
  524. if git_dir.startswith(cachepath):
  525. mirror = Mirror.FromPath(git_dir)
  526. mirror.populate()
  527. return 0
  528. for remote in remotes:
  529. remote_url = subprocess.check_output(
  530. [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip()
  531. if remote_url.startswith(cachepath):
  532. mirror = Mirror.FromPath(remote_url)
  533. mirror.print = lambda *args: None
  534. print('Updating git cache...')
  535. mirror.populate()
  536. subprocess.check_call([Mirror.git_exe, 'fetch', remote])
  537. return 0
  538. @subcommand.usage('[url of repo to unlock, or -a|--all]')
  539. def CMDunlock(parser, args):
  540. """Unlock one or all repos if their lock files are still around."""
  541. parser.add_option('--force', '-f', action='store_true',
  542. help='Actually perform the action')
  543. parser.add_option('--all', '-a', action='store_true',
  544. help='Unlock all repository caches')
  545. options, args = parser.parse_args(args)
  546. if len(args) > 1 or (len(args) == 0 and not options.all):
  547. parser.error('git cache unlock takes exactly one repo url, or --all')
  548. if not options.force:
  549. cachepath = Mirror.GetCachePath()
  550. lockfiles = [os.path.join(cachepath, path)
  551. for path in os.listdir(cachepath)
  552. if path.endswith('.lock') and os.path.isfile(path)]
  553. parser.error('git cache unlock requires -f|--force to do anything. '
  554. 'Refusing to unlock the following repo caches: '
  555. ', '.join(lockfiles))
  556. unlocked_repos = []
  557. if options.all:
  558. unlocked_repos.extend(Mirror.UnlockAll())
  559. else:
  560. m = Mirror(args[0])
  561. if m.unlock():
  562. unlocked_repos.append(m.mirror_path)
  563. if unlocked_repos:
  564. logging.info('Broke locks on these caches:\n %s' % '\n '.join(
  565. unlocked_repos))
  566. class OptionParser(optparse.OptionParser):
  567. """Wrapper class for OptionParser to handle global options."""
  568. def __init__(self, *args, **kwargs):
  569. optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
  570. self.add_option('-c', '--cache-dir',
  571. help='Path to the directory containing the cache')
  572. self.add_option('-v', '--verbose', action='count', default=1,
  573. help='Increase verbosity (can be passed multiple times)')
  574. self.add_option('-q', '--quiet', action='store_true',
  575. help='Suppress all extraneous output')
  576. def parse_args(self, args=None, values=None):
  577. options, args = optparse.OptionParser.parse_args(self, args, values)
  578. if options.quiet:
  579. options.verbose = 0
  580. levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG]
  581. logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
  582. try:
  583. global_cache_dir = Mirror.GetCachePath()
  584. except RuntimeError:
  585. global_cache_dir = None
  586. if options.cache_dir:
  587. if global_cache_dir and (
  588. os.path.abspath(options.cache_dir) !=
  589. os.path.abspath(global_cache_dir)):
  590. logging.warn('Overriding globally-configured cache directory.')
  591. Mirror.SetCachePath(options.cache_dir)
  592. return options, args
  593. def main(argv):
  594. dispatcher = subcommand.CommandDispatcher(__name__)
  595. return dispatcher.execute(OptionParser(), argv)
  596. if __name__ == '__main__':
  597. sys.exit(main(sys.argv[1:]))