OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """A git command for managing a local cache of git repositories.""" | 6 """A git command for managing a local cache of git repositories.""" |
7 | 7 |
8 from __future__ import print_function | 8 from __future__ import print_function |
9 import errno | 9 import errno |
10 import logging | 10 import logging |
(...skipping 127 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
138 return self.is_locked() and self.pid == self._read_pid() | 138 return self.is_locked() and self.pid == self._read_pid() |
139 | 139 |
140 | 140 |
141 class Mirror(object): | 141 class Mirror(object): |
142 | 142 |
143 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git' | 143 git_exe = 'git.bat' if sys.platform.startswith('win') else 'git' |
144 gsutil_exe = os.path.join( | 144 gsutil_exe = os.path.join( |
145 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py') | 145 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py') |
146 cachepath_lock = threading.Lock() | 146 cachepath_lock = threading.Lock() |
147 | 147 |
| 148 @staticmethod |
| 149 def parse_fetch_spec(spec): |
| 150 """Parses and canonicalizes a fetch spec. |
| 151 |
| 152 Returns (fetchspec, value_regex), where value_regex can be used |
| 153 with 'git config --replace-all'. |
| 154 """ |
| 155 parts = spec.split(':', 1) |
| 156 src = parts[0].lstrip('+').rstrip('/') |
| 157 if not src.startswith('refs/'): |
| 158 src = 'refs/heads/%s' % src |
| 159 dest = parts[1].rstrip('/') if len(parts) > 1 else src |
| 160 regex = r'\+%s:.*' % src.replace('*', r'\*') |
| 161 return ('+%s:%s' % (src, dest), regex) |
| 162 |
148 def __init__(self, url, refs=None, print_func=None): | 163 def __init__(self, url, refs=None, print_func=None): |
149 self.url = url | 164 self.url = url |
150 self.refs = refs or [] | 165 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])]) |
151 self.basedir = self.UrlToCacheDir(url) | 166 self.basedir = self.UrlToCacheDir(url) |
152 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir) | 167 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir) |
153 if print_func: | 168 if print_func: |
154 self.print = self.print_without_file | 169 self.print = self.print_without_file |
155 self.print_func = print_func | 170 self.print_func = print_func |
156 else: | 171 else: |
157 self.print = print | 172 self.print = print |
158 | 173 |
159 def print_without_file(self, message, **kwargs): | 174 def print_without_file(self, message, **kwargs): |
160 self.print_func(message) | 175 self.print_func(message) |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
229 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd) | 244 self.RunGit(['config', 'gc.autopacklimit', '0'], cwd=cwd) |
230 | 245 |
231 # Allocate more RAM for cache-ing delta chains, for better performance | 246 # Allocate more RAM for cache-ing delta chains, for better performance |
232 # of "Resolving deltas". | 247 # of "Resolving deltas". |
233 self.RunGit(['config', 'core.deltaBaseCacheLimit', | 248 self.RunGit(['config', 'core.deltaBaseCacheLimit', |
234 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd) | 249 gclient_utils.DefaultDeltaBaseCacheLimit()], cwd=cwd) |
235 | 250 |
236 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd) | 251 self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd) |
237 self.RunGit(['config', '--replace-all', 'remote.origin.fetch', | 252 self.RunGit(['config', '--replace-all', 'remote.origin.fetch', |
238 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd) | 253 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd) |
239 for ref in self.refs: | 254 for spec, value_regex in self.fetch_specs: |
240 ref = ref.lstrip('+').rstrip('/') | |
241 if ref.startswith('refs/'): | |
242 refspec = '+%s:%s' % (ref, ref) | |
243 regex = r'\+%s:.*' % ref.replace('*', r'\*') | |
244 else: | |
245 refspec = '+refs/%s/*:refs/%s/*' % (ref, ref) | |
246 regex = r'\+refs/heads/%s:.*' % ref.replace('*', r'\*') | |
247 self.RunGit( | 255 self.RunGit( |
248 ['config', '--replace-all', 'remote.origin.fetch', refspec, regex], | 256 ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex], |
249 cwd=cwd) | 257 cwd=cwd) |
250 | 258 |
251 def bootstrap_repo(self, directory): | 259 def bootstrap_repo(self, directory): |
252 """Bootstrap the repo from Google Stroage if possible. | 260 """Bootstrap the repo from Google Stroage if possible. |
253 | 261 |
254 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing(). | 262 More apt-ly named bootstrap_repo_from_cloud_if_possible_else_do_nothing(). |
255 """ | 263 """ |
256 | 264 |
257 python_fallback = False | 265 python_fallback = False |
258 if (sys.platform.startswith('win') and | 266 if (sys.platform.startswith('win') and |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
307 if retcode: | 315 if retcode: |
308 self.print( | 316 self.print( |
309 'Extracting bootstrap zipfile %s failed.\n' | 317 'Extracting bootstrap zipfile %s failed.\n' |
310 'Resuming normal operations.' % filename) | 318 'Resuming normal operations.' % filename) |
311 return False | 319 return False |
312 return True | 320 return True |
313 | 321 |
314 def exists(self): | 322 def exists(self): |
315 return os.path.isfile(os.path.join(self.mirror_path, 'config')) | 323 return os.path.isfile(os.path.join(self.mirror_path, 'config')) |
316 | 324 |
| 325 def _preserve_fetchspec(self): |
| 326 """Read and preserve remote.origin.fetch from an existing mirror. |
| 327 |
| 328 This modifies self.fetch_specs. |
| 329 """ |
| 330 if not self.exists(): |
| 331 return |
| 332 try: |
| 333 config_fetchspecs = subprocess.check_output( |
| 334 [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'], |
| 335 cwd=self.mirror_path) |
| 336 for fetchspec in config_fetchspecs.splitlines(): |
| 337 self.fetch_specs.add(self.parse_fetch_spec(fetchspec)) |
| 338 except subprocess.CalledProcessError: |
| 339 logging.warn('Tried and failed to preserve remote.origin.fetch from the ' |
| 340 'existing cache directory. You may need to manually edit ' |
| 341 '%s and "git cache fetch" again.' |
| 342 % os.path.join(self.mirror_path, 'config')) |
| 343 |
317 def _ensure_bootstrapped(self, depth, bootstrap, force=False): | 344 def _ensure_bootstrapped(self, depth, bootstrap, force=False): |
318 tempdir = None | 345 tempdir = None |
319 config_file = os.path.join(self.mirror_path, 'config') | |
320 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack') | 346 pack_dir = os.path.join(self.mirror_path, 'objects', 'pack') |
321 pack_files = [] | 347 pack_files = [] |
322 | 348 |
323 if os.path.isdir(pack_dir): | 349 if os.path.isdir(pack_dir): |
324 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')] | 350 pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')] |
325 | 351 |
326 should_bootstrap = (force or | 352 should_bootstrap = (force or |
327 not os.path.exists(config_file) or | 353 not self.exists() or |
328 len(pack_files) > GC_AUTOPACKLIMIT) | 354 len(pack_files) > GC_AUTOPACKLIMIT) |
329 if should_bootstrap: | 355 if should_bootstrap: |
| 356 if self.exists(): |
| 357 # Re-bootstrapping an existing mirror; preserve existing fetch spec. |
| 358 self._preserve_fetchspec() |
330 tempdir = tempfile.mkdtemp( | 359 tempdir = tempfile.mkdtemp( |
331 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath()) | 360 prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath()) |
332 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir) | 361 bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir) |
333 if bootstrapped: | 362 if bootstrapped: |
334 # Bootstrap succeeded; delete previous cache, if any. | 363 # Bootstrap succeeded; delete previous cache, if any. |
335 gclient_utils.rmtree(self.mirror_path) | 364 gclient_utils.rmtree(self.mirror_path) |
336 elif not os.path.exists(config_file): | 365 elif not self.exists(): |
337 # Bootstrap failed, no previous cache; start with a bare git dir. | 366 # Bootstrap failed, no previous cache; start with a bare git dir. |
338 self.RunGit(['init', '--bare'], cwd=tempdir) | 367 self.RunGit(['init', '--bare'], cwd=tempdir) |
339 else: | 368 else: |
340 # Bootstrap failed, previous cache exists; warn and continue. | 369 # Bootstrap failed, previous cache exists; warn and continue. |
341 logging.warn( | 370 logging.warn( |
342 'Git cache has a lot of pack files (%d). Tried to re-bootstrap ' | 371 'Git cache has a lot of pack files (%d). Tried to re-bootstrap ' |
343 'but failed. Continuing with non-optimized repository.' | 372 'but failed. Continuing with non-optimized repository.' |
344 % len(pack_files)) | 373 % len(pack_files)) |
345 gclient_utils.rmtree(tempdir) | 374 gclient_utils.rmtree(tempdir) |
346 tempdir = None | 375 tempdir = None |
(...skipping 209 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
556 } | 585 } |
557 if options.depth: | 586 if options.depth: |
558 kwargs['depth'] = options.depth | 587 kwargs['depth'] = options.depth |
559 mirror.populate(**kwargs) | 588 mirror.populate(**kwargs) |
560 | 589 |
561 | 590 |
562 @subcommand.usage('Fetch new commits into cache and current checkout') | 591 @subcommand.usage('Fetch new commits into cache and current checkout') |
563 def CMDfetch(parser, args): | 592 def CMDfetch(parser, args): |
564 """Update mirror, and fetch in cwd.""" | 593 """Update mirror, and fetch in cwd.""" |
565 parser.add_option('--all', action='store_true', help='Fetch all remotes') | 594 parser.add_option('--all', action='store_true', help='Fetch all remotes') |
| 595 parser.add_option('--no_bootstrap', '--no-bootstrap', |
| 596 action='store_true', |
| 597 help='Don\'t (re)bootstrap from Google Storage') |
566 options, args = parser.parse_args(args) | 598 options, args = parser.parse_args(args) |
567 | 599 |
568 # Figure out which remotes to fetch. This mimics the behavior of regular | 600 # Figure out which remotes to fetch. This mimics the behavior of regular |
569 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches, | 601 # 'git fetch'. Note that in the case of "stacked" or "pipelined" branches, |
570 # this will NOT try to traverse up the branching structure to find the | 602 # this will NOT try to traverse up the branching structure to find the |
571 # ultimate remote to update. | 603 # ultimate remote to update. |
572 remotes = [] | 604 remotes = [] |
573 if options.all: | 605 if options.all: |
574 assert not args, 'fatal: fetch --all does not take a repository argument' | 606 assert not args, 'fatal: fetch --all does not take a repository argument' |
575 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines() | 607 remotes = subprocess.check_output([Mirror.git_exe, 'remote']).splitlines() |
(...skipping 10 matching lines...) Expand all Loading... |
586 remotes = [upstream] | 618 remotes = [upstream] |
587 if not remotes: | 619 if not remotes: |
588 remotes = ['origin'] | 620 remotes = ['origin'] |
589 | 621 |
590 cachepath = Mirror.GetCachePath() | 622 cachepath = Mirror.GetCachePath() |
591 git_dir = os.path.abspath(subprocess.check_output( | 623 git_dir = os.path.abspath(subprocess.check_output( |
592 [Mirror.git_exe, 'rev-parse', '--git-dir'])) | 624 [Mirror.git_exe, 'rev-parse', '--git-dir'])) |
593 git_dir = os.path.abspath(git_dir) | 625 git_dir = os.path.abspath(git_dir) |
594 if git_dir.startswith(cachepath): | 626 if git_dir.startswith(cachepath): |
595 mirror = Mirror.FromPath(git_dir) | 627 mirror = Mirror.FromPath(git_dir) |
596 mirror.populate() | 628 mirror.populate(bootstrap=not options.no_bootstrap) |
597 return 0 | 629 return 0 |
598 for remote in remotes: | 630 for remote in remotes: |
599 remote_url = subprocess.check_output( | 631 remote_url = subprocess.check_output( |
600 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip() | 632 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip() |
601 if remote_url.startswith(cachepath): | 633 if remote_url.startswith(cachepath): |
602 mirror = Mirror.FromPath(remote_url) | 634 mirror = Mirror.FromPath(remote_url) |
603 mirror.print = lambda *args: None | 635 mirror.print = lambda *args: None |
604 print('Updating git cache...') | 636 print('Updating git cache...') |
605 mirror.populate() | 637 mirror.populate(bootstrap=not options.no_bootstrap) |
606 subprocess.check_call([Mirror.git_exe, 'fetch', remote]) | 638 subprocess.check_call([Mirror.git_exe, 'fetch', remote]) |
607 return 0 | 639 return 0 |
608 | 640 |
609 | 641 |
610 @subcommand.usage('[url of repo to unlock, or -a|--all]') | 642 @subcommand.usage('[url of repo to unlock, or -a|--all]') |
611 def CMDunlock(parser, args): | 643 def CMDunlock(parser, args): |
612 """Unlock one or all repos if their lock files are still around.""" | 644 """Unlock one or all repos if their lock files are still around.""" |
613 parser.add_option('--force', '-f', action='store_true', | 645 parser.add_option('--force', '-f', action='store_true', |
614 help='Actually perform the action') | 646 help='Actually perform the action') |
615 parser.add_option('--all', '-a', action='store_true', | 647 parser.add_option('--all', '-a', action='store_true', |
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
678 dispatcher = subcommand.CommandDispatcher(__name__) | 710 dispatcher = subcommand.CommandDispatcher(__name__) |
679 return dispatcher.execute(OptionParser(), argv) | 711 return dispatcher.execute(OptionParser(), argv) |
680 | 712 |
681 | 713 |
682 if __name__ == '__main__': | 714 if __name__ == '__main__': |
683 try: | 715 try: |
684 sys.exit(main(sys.argv[1:])) | 716 sys.exit(main(sys.argv[1:])) |
685 except KeyboardInterrupt: | 717 except KeyboardInterrupt: |
686 sys.stderr.write('interrupted\n') | 718 sys.stderr.write('interrupted\n') |
687 sys.exit(1) | 719 sys.exit(1) |
OLD | NEW |