Implement Store. pre-commit now installs files to ~/.pre-commit

This commit is contained in:
Anthony Sottile 2014-04-20 22:41:14 -07:00
parent 26af2cecab
commit 479eb51873
16 changed files with 457 additions and 234 deletions

View file

@ -56,7 +56,7 @@ class RepositoryCannotBeUpdatedError(RuntimeError):
pass
def _update_repository(repo_config):
def _update_repository(repo_config, runner):
"""Updates a repository to the tip of `master`. If the repository cannot
be updated because a hook that is configured does not exist in `master`,
this raises a RepositoryCannotBeUpdatedError
@ -64,9 +64,9 @@ def _update_repository(repo_config):
Args:
repo_config - A config for a repository
"""
repo = Repository(repo_config)
repo = Repository.create(repo_config, runner.store)
with repo.in_checkout():
with local.cwd(repo.repo_path_getter.repo_path):
local['git']['fetch']()
head_sha = local['git']['rev-parse', 'origin/master']().strip()
@ -77,11 +77,11 @@ def _update_repository(repo_config):
# Construct a new config with the head sha
new_config = OrderedDict(repo_config)
new_config['sha'] = head_sha
new_repo = Repository(new_config)
new_repo = Repository.create(new_config, runner.store)
# See if any of our hooks were deleted with the new commits
hooks = set(repo.hooks.keys())
hooks_missing = hooks - (hooks & set(new_repo.manifest.keys()))
hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks.keys()))
if hooks_missing:
raise RepositoryCannotBeUpdatedError(
'Cannot update because the tip of master is missing these hooks:\n'
@ -106,7 +106,7 @@ def autoupdate(runner):
sys.stdout.write('Updating {0}...'.format(repo_config['repo']))
sys.stdout.flush()
try:
new_repo_config = _update_repository(repo_config)
new_repo_config = _update_repository(repo_config, runner)
except RepositoryCannotBeUpdatedError as error:
print(error.args[0])
output_configs.append(repo_config)
@ -135,9 +135,9 @@ def autoupdate(runner):
def clean(runner):
if os.path.exists(runner.hooks_workspace_path):
shutil.rmtree(runner.hooks_workspace_path)
print('Cleaned {0}.'.format(runner.hooks_workspace_path))
if os.path.exists(runner.store.directory):
shutil.rmtree(runner.store.directory)
print('Cleaned {0}.'.format(runner.store.directory))
return 0

View file

@ -1,7 +1,5 @@
CONFIG_FILE = '.pre-commit-config.yaml'
HOOKS_WORKSPACE = '.pre-commit-files'
MANIFEST_FILE = 'hooks.yaml'
YAML_DUMP_KWARGS = {

View file

@ -1,20 +0,0 @@
import contextlib
import os.path
from plumbum import local
import pre_commit.constants as C
from pre_commit import git
def get_pre_commit_dir_path():
return os.path.join(git.get_root(), C.HOOKS_WORKSPACE)
@contextlib.contextmanager
def in_hooks_workspace():
"""Change into the hooks workspace. If it does not exist create it."""
if not os.path.exists(get_pre_commit_dir_path()):
local.path(get_pre_commit_dir_path()).mkdir()
with local.cwd(get_pre_commit_dir_path()):
yield

21
pre_commit/manifest.py Normal file
View file

@ -0,0 +1,21 @@
import os.path
import pre_commit.constants as C
from pre_commit.clientlib.validate_manifest import load_manifest
from pre_commit.util import cached_property
class Manifest(object):
def __init__(self, repo_path_getter):
self.repo_path_getter = repo_path_getter
@cached_property
def manifest_contents(self):
manifest_path = os.path.join(
self.repo_path_getter.repo_path, C.MANIFEST_FILE,
)
return load_manifest(manifest_path)
@cached_property
def hooks(self):
return dict((hook['id'], hook) for hook in self.manifest_contents)

View file

@ -1,27 +1,24 @@
import contextlib
import logging
from asottile.ordereddict import OrderedDict
from plumbum import local
import pre_commit.constants as C
from pre_commit import five
from pre_commit.clientlib.validate_manifest import load_manifest
from pre_commit.hooks_workspace import in_hooks_workspace
from pre_commit.languages.all import languages
from pre_commit.manifest import Manifest
from pre_commit.prefixed_command_runner import PrefixedCommandRunner
from pre_commit.util import cached_property
from pre_commit.util import clean_path_on_failure
logger = logging.getLogger('pre_commit')
class Repository(object):
def __init__(self, repo_config):
def __init__(self, repo_config, repo_path_getter):
self.repo_config = repo_config
self.__created = False
self.repo_path_getter = repo_path_getter
self.__installed = False
@classmethod
def create(cls, config, store):
repo_path_getter = store.get_repo_path_getter(
config['repo'], config['sha']
)
return cls(config, repo_path_getter)
@cached_property
def repo_url(self):
return self.repo_config['repo']
@ -36,46 +33,22 @@ class Repository(object):
@cached_property
def hooks(self):
# TODO: merging in manifest dicts is a smell imo
return OrderedDict(
(hook['id'], dict(hook, **self.manifest[hook['id']]))
(hook['id'], dict(hook, **self.manifest.hooks[hook['id']]))
for hook in self.repo_config['hooks']
)
@cached_property
def manifest(self):
with self.in_checkout():
return dict(
(hook['id'], hook)
for hook in load_manifest(C.MANIFEST_FILE)
)
return Manifest(self.repo_path_getter)
def get_cmd_runner(self, hooks_cmd_runner):
# TODO: this effectively throws away the original cmd runner
return PrefixedCommandRunner.from_command_runner(
hooks_cmd_runner, self.sha,
hooks_cmd_runner, self.repo_path_getter.repo_path,
)
def require_created(self):
if self.__created:
return
self.create()
self.__created = True
def create(self):
with in_hooks_workspace():
if local.path(self.sha).exists():
# Project already exists, no reason to re-create it
return
# Checking out environment for the first time
logger.info('Installing environment for {0}.'.format(self.repo_url))
logger.info('Once installed this environment will be reused.')
logger.info('This may take a few minutes...')
with clean_path_on_failure(five.u(local.path(self.sha))):
local['git']['clone', '--no-checkout', self.repo_url, self.sha]()
with self.in_checkout():
local['git']['checkout', self.sha]()
def require_installed(self, cmd_runner):
if self.__installed:
return
@ -89,7 +62,6 @@ class Repository(object):
Args:
cmd_runner - A `PrefixedCommandRunner` bound to the hooks workspace
"""
self.require_created()
repo_cmd_runner = self.get_cmd_runner(cmd_runner)
for language_name in self.languages:
language = languages[language_name]
@ -101,13 +73,6 @@ class Repository(object):
continue
language.install_environment(repo_cmd_runner)
@contextlib.contextmanager
def in_checkout(self):
self.require_created()
with in_hooks_workspace():
with local.cwd(self.sha):
yield
def run_hook(self, cmd_runner, hook_id, file_args):
"""Run a hook.

View file

@ -4,8 +4,8 @@ import os.path
import pre_commit.constants as C
from pre_commit import git
from pre_commit.clientlib.validate_config import load_config
from pre_commit.prefixed_command_runner import PrefixedCommandRunner
from pre_commit.repository import Repository
from pre_commit.store import Store
from pre_commit.util import cached_property
@ -27,10 +27,6 @@ class Runner(object):
os.chdir(root)
return cls(root)
@cached_property
def hooks_workspace_path(self):
return os.path.join(self.git_root, C.HOOKS_WORKSPACE)
@cached_property
def config_file_path(self):
return os.path.join(self.git_root, C.CONFIG_FILE)
@ -39,7 +35,7 @@ class Runner(object):
def repositories(self):
"""Returns a tuple of the configured repositories."""
config = load_config(self.config_file_path)
return tuple(Repository(x) for x in config)
return tuple(Repository.create(x, self.store) for x in config)
@cached_property
def pre_commit_path(self):
@ -47,4 +43,9 @@ class Runner(object):
@cached_property
def cmd_runner(self):
return PrefixedCommandRunner(self.hooks_workspace_path)
# TODO: remove this and inline runner.store.cmd_runner
return self.store.cmd_runner
@cached_property
def store(self):
return Store()

97
pre_commit/store.py Normal file
View file

@ -0,0 +1,97 @@
from __future__ import unicode_literals
import io
import logging
import os
import os.path
import tempfile
from plumbum import local
from pre_commit.prefixed_command_runner import PrefixedCommandRunner
from pre_commit.util import cached_property
from pre_commit.util import clean_path_on_failure
logger = logging.getLogger('pre_commit')
def _get_default_directory():
"""Returns the default directory for the Store. This is intentionally
underscored to indicate that `Store.get_default_directory` is the intended
way to get this information. This is also done so
`Store.get_default_directory` can be mocked in tests and
`_get_default_directory` can be tested.
"""
return os.path.join(os.environ['HOME'], '.pre-commit')
class Store(object):
get_default_directory = staticmethod(_get_default_directory)
class RepoPathGetter(object):
def __init__(self, repo, sha, store):
self._repo = repo
self._sha = sha
self._store = store
@cached_property
def repo_path(self):
return self._store.clone(self._repo, self._sha)
def __init__(self, directory=None):
if directory is None:
directory = self.get_default_directory()
self.directory = directory
self.__created = False
def _write_readme(self):
with io.open(os.path.join(self.directory, 'README'), 'w') as readme:
readme.write(
'This directory is maintained by the pre-commit project.\n'
'Learn more: https://github.com/pre-commit/pre-commit\n'
)
def _create(self):
if os.path.exists(self.directory):
return
os.makedirs(self.directory)
self._write_readme()
def require_created(self):
"""Require the pre-commit file store to be created."""
if self.__created:
return
self._create()
self.__created = True
def clone(self, url, sha):
"""Clone the given url and checkout the specific sha."""
self.require_created()
# Check if we already exist
sha_path = os.path.join(self.directory, sha)
if os.path.exists(sha_path):
return os.readlink(sha_path)
logger.info('Installing environment for {0}.'.format(url))
logger.info('Once installed this environment will be reused.')
logger.info('This may take a few minutes...')
dir = tempfile.mkdtemp(prefix='repo', dir=self.directory)
with clean_path_on_failure(dir):
local['git']('clone', '--no-checkout', url, dir)
with local.cwd(dir):
local['git']('checkout', sha)
# Make a symlink from sha->repo
os.symlink(dir, sha_path)
return dir
def get_repo_path_getter(self, repo, sha):
return self.RepoPathGetter(repo, sha, self)
@cached_property
def cmd_runner(self):
return PrefixedCommandRunner(self.directory)