Skip to content

Commit 479eb51

Browse files
committed
Implement Store. pre-commit now installs files to ~/.pre-commit
1 parent 26af2ce commit 479eb51

16 files changed

+457
-234
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
.*.sw[a-z]
55
.coverage
66
.idea
7-
.pre-commit-files
87
.project
98
.pydevproject
109
.tox

pre_commit/commands.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,17 @@ class RepositoryCannotBeUpdatedError(RuntimeError):
5656
pass
5757

5858

59-
def _update_repository(repo_config):
59+
def _update_repository(repo_config, runner):
6060
"""Updates a repository to the tip of `master`. If the repository cannot
6161
be updated because a hook that is configured does not exist in `master`,
6262
this raises a RepositoryCannotBeUpdatedError
6363
6464
Args:
6565
repo_config - A config for a repository
6666
"""
67-
repo = Repository(repo_config)
67+
repo = Repository.create(repo_config, runner.store)
6868

69-
with repo.in_checkout():
69+
with local.cwd(repo.repo_path_getter.repo_path):
7070
local['git']['fetch']()
7171
head_sha = local['git']['rev-parse', 'origin/master']().strip()
7272

@@ -77,11 +77,11 @@ def _update_repository(repo_config):
7777
# Construct a new config with the head sha
7878
new_config = OrderedDict(repo_config)
7979
new_config['sha'] = head_sha
80-
new_repo = Repository(new_config)
80+
new_repo = Repository.create(new_config, runner.store)
8181

8282
# See if any of our hooks were deleted with the new commits
8383
hooks = set(repo.hooks.keys())
84-
hooks_missing = hooks - (hooks & set(new_repo.manifest.keys()))
84+
hooks_missing = hooks - (hooks & set(new_repo.manifest.hooks.keys()))
8585
if hooks_missing:
8686
raise RepositoryCannotBeUpdatedError(
8787
'Cannot update because the tip of master is missing these hooks:\n'
@@ -106,7 +106,7 @@ def autoupdate(runner):
106106
sys.stdout.write('Updating {0}...'.format(repo_config['repo']))
107107
sys.stdout.flush()
108108
try:
109-
new_repo_config = _update_repository(repo_config)
109+
new_repo_config = _update_repository(repo_config, runner)
110110
except RepositoryCannotBeUpdatedError as error:
111111
print(error.args[0])
112112
output_configs.append(repo_config)
@@ -135,9 +135,9 @@ def autoupdate(runner):
135135

136136

137137
def clean(runner):
138-
if os.path.exists(runner.hooks_workspace_path):
139-
shutil.rmtree(runner.hooks_workspace_path)
140-
print('Cleaned {0}.'.format(runner.hooks_workspace_path))
138+
if os.path.exists(runner.store.directory):
139+
shutil.rmtree(runner.store.directory)
140+
print('Cleaned {0}.'.format(runner.store.directory))
141141
return 0
142142

143143

pre_commit/constants.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
CONFIG_FILE = '.pre-commit-config.yaml'
22

3-
HOOKS_WORKSPACE = '.pre-commit-files'
4-
53
MANIFEST_FILE = 'hooks.yaml'
64

75
YAML_DUMP_KWARGS = {

pre_commit/hooks_workspace.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

pre_commit/manifest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import os.path
2+
3+
import pre_commit.constants as C
4+
from pre_commit.clientlib.validate_manifest import load_manifest
5+
from pre_commit.util import cached_property
6+
7+
8+
class Manifest(object):
9+
def __init__(self, repo_path_getter):
10+
self.repo_path_getter = repo_path_getter
11+
12+
@cached_property
13+
def manifest_contents(self):
14+
manifest_path = os.path.join(
15+
self.repo_path_getter.repo_path, C.MANIFEST_FILE,
16+
)
17+
return load_manifest(manifest_path)
18+
19+
@cached_property
20+
def hooks(self):
21+
return dict((hook['id'], hook) for hook in self.manifest_contents)

pre_commit/repository.py

Lines changed: 15 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
1-
import contextlib
2-
import logging
31
from asottile.ordereddict import OrderedDict
4-
from plumbum import local
52

6-
import pre_commit.constants as C
7-
from pre_commit import five
8-
from pre_commit.clientlib.validate_manifest import load_manifest
9-
from pre_commit.hooks_workspace import in_hooks_workspace
103
from pre_commit.languages.all import languages
4+
from pre_commit.manifest import Manifest
115
from pre_commit.prefixed_command_runner import PrefixedCommandRunner
126
from pre_commit.util import cached_property
13-
from pre_commit.util import clean_path_on_failure
14-
15-
16-
logger = logging.getLogger('pre_commit')
177

188

199
class Repository(object):
20-
def __init__(self, repo_config):
10+
def __init__(self, repo_config, repo_path_getter):
2111
self.repo_config = repo_config
22-
self.__created = False
12+
self.repo_path_getter = repo_path_getter
2313
self.__installed = False
2414

15+
@classmethod
16+
def create(cls, config, store):
17+
repo_path_getter = store.get_repo_path_getter(
18+
config['repo'], config['sha']
19+
)
20+
return cls(config, repo_path_getter)
21+
2522
@cached_property
2623
def repo_url(self):
2724
return self.repo_config['repo']
@@ -36,46 +33,22 @@ def languages(self):
3633

3734
@cached_property
3835
def hooks(self):
36+
# TODO: merging in manifest dicts is a smell imo
3937
return OrderedDict(
40-
(hook['id'], dict(hook, **self.manifest[hook['id']]))
38+
(hook['id'], dict(hook, **self.manifest.hooks[hook['id']]))
4139
for hook in self.repo_config['hooks']
4240
)
4341

4442
@cached_property
4543
def manifest(self):
46-
with self.in_checkout():
47-
return dict(
48-
(hook['id'], hook)
49-
for hook in load_manifest(C.MANIFEST_FILE)
50-
)
44+
return Manifest(self.repo_path_getter)
5145

5246
def get_cmd_runner(self, hooks_cmd_runner):
47+
# TODO: this effectively throws away the original cmd runner
5348
return PrefixedCommandRunner.from_command_runner(
54-
hooks_cmd_runner, self.sha,
49+
hooks_cmd_runner, self.repo_path_getter.repo_path,
5550
)
5651

57-
def require_created(self):
58-
if self.__created:
59-
return
60-
61-
self.create()
62-
self.__created = True
63-
64-
def create(self):
65-
with in_hooks_workspace():
66-
if local.path(self.sha).exists():
67-
# Project already exists, no reason to re-create it
68-
return
69-
70-
# Checking out environment for the first time
71-
logger.info('Installing environment for {0}.'.format(self.repo_url))
72-
logger.info('Once installed this environment will be reused.')
73-
logger.info('This may take a few minutes...')
74-
with clean_path_on_failure(five.u(local.path(self.sha))):
75-
local['git']['clone', '--no-checkout', self.repo_url, self.sha]()
76-
with self.in_checkout():
77-
local['git']['checkout', self.sha]()
78-
7952
def require_installed(self, cmd_runner):
8053
if self.__installed:
8154
return
@@ -89,7 +62,6 @@ def install(self, cmd_runner):
8962
Args:
9063
cmd_runner - A `PrefixedCommandRunner` bound to the hooks workspace
9164
"""
92-
self.require_created()
9365
repo_cmd_runner = self.get_cmd_runner(cmd_runner)
9466
for language_name in self.languages:
9567
language = languages[language_name]
@@ -101,13 +73,6 @@ def install(self, cmd_runner):
10173
continue
10274
language.install_environment(repo_cmd_runner)
10375

104-
@contextlib.contextmanager
105-
def in_checkout(self):
106-
self.require_created()
107-
with in_hooks_workspace():
108-
with local.cwd(self.sha):
109-
yield
110-
11176
def run_hook(self, cmd_runner, hook_id, file_args):
11277
"""Run a hook.
11378

pre_commit/runner.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import pre_commit.constants as C
55
from pre_commit import git
66
from pre_commit.clientlib.validate_config import load_config
7-
from pre_commit.prefixed_command_runner import PrefixedCommandRunner
87
from pre_commit.repository import Repository
8+
from pre_commit.store import Store
99
from pre_commit.util import cached_property
1010

1111

@@ -27,10 +27,6 @@ def create(cls):
2727
os.chdir(root)
2828
return cls(root)
2929

30-
@cached_property
31-
def hooks_workspace_path(self):
32-
return os.path.join(self.git_root, C.HOOKS_WORKSPACE)
33-
3430
@cached_property
3531
def config_file_path(self):
3632
return os.path.join(self.git_root, C.CONFIG_FILE)
@@ -39,12 +35,17 @@ def config_file_path(self):
3935
def repositories(self):
4036
"""Returns a tuple of the configured repositories."""
4137
config = load_config(self.config_file_path)
42-
return tuple(Repository(x) for x in config)
38+
return tuple(Repository.create(x, self.store) for x in config)
4339

4440
@cached_property
4541
def pre_commit_path(self):
4642
return os.path.join(self.git_root, '.git/hooks/pre-commit')
4743

4844
@cached_property
4945
def cmd_runner(self):
50-
return PrefixedCommandRunner(self.hooks_workspace_path)
46+
# TODO: remove this and inline runner.store.cmd_runner
47+
return self.store.cmd_runner
48+
49+
@cached_property
50+
def store(self):
51+
return Store()

pre_commit/store.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from __future__ import unicode_literals
2+
3+
import io
4+
import logging
5+
import os
6+
import os.path
7+
import tempfile
8+
from plumbum import local
9+
10+
from pre_commit.prefixed_command_runner import PrefixedCommandRunner
11+
from pre_commit.util import cached_property
12+
from pre_commit.util import clean_path_on_failure
13+
14+
15+
logger = logging.getLogger('pre_commit')
16+
17+
18+
def _get_default_directory():
19+
"""Returns the default directory for the Store. This is intentionally
20+
underscored to indicate that `Store.get_default_directory` is the intended
21+
way to get this information. This is also done so
22+
`Store.get_default_directory` can be mocked in tests and
23+
`_get_default_directory` can be tested.
24+
"""
25+
return os.path.join(os.environ['HOME'], '.pre-commit')
26+
27+
28+
class Store(object):
29+
get_default_directory = staticmethod(_get_default_directory)
30+
31+
class RepoPathGetter(object):
32+
def __init__(self, repo, sha, store):
33+
self._repo = repo
34+
self._sha = sha
35+
self._store = store
36+
37+
@cached_property
38+
def repo_path(self):
39+
return self._store.clone(self._repo, self._sha)
40+
41+
def __init__(self, directory=None):
42+
if directory is None:
43+
directory = self.get_default_directory()
44+
45+
self.directory = directory
46+
self.__created = False
47+
48+
def _write_readme(self):
49+
with io.open(os.path.join(self.directory, 'README'), 'w') as readme:
50+
readme.write(
51+
'This directory is maintained by the pre-commit project.\n'
52+
'Learn more: https://github.com/pre-commit/pre-commit\n'
53+
)
54+
55+
def _create(self):
56+
if os.path.exists(self.directory):
57+
return
58+
os.makedirs(self.directory)
59+
self._write_readme()
60+
61+
def require_created(self):
62+
"""Require the pre-commit file store to be created."""
63+
if self.__created:
64+
return
65+
66+
self._create()
67+
self.__created = True
68+
69+
def clone(self, url, sha):
70+
"""Clone the given url and checkout the specific sha."""
71+
self.require_created()
72+
73+
# Check if we already exist
74+
sha_path = os.path.join(self.directory, sha)
75+
if os.path.exists(sha_path):
76+
return os.readlink(sha_path)
77+
78+
logger.info('Installing environment for {0}.'.format(url))
79+
logger.info('Once installed this environment will be reused.')
80+
logger.info('This may take a few minutes...')
81+
82+
dir = tempfile.mkdtemp(prefix='repo', dir=self.directory)
83+
with clean_path_on_failure(dir):
84+
local['git']('clone', '--no-checkout', url, dir)
85+
with local.cwd(dir):
86+
local['git']('checkout', sha)
87+
88+
# Make a symlink from sha->repo
89+
os.symlink(dir, sha_path)
90+
return dir
91+
92+
def get_repo_path_getter(self, repo, sha):
93+
return self.RepoPathGetter(repo, sha, self)
94+
95+
@cached_property
96+
def cmd_runner(self):
97+
return PrefixedCommandRunner(self.directory)

0 commit comments

Comments
 (0)