Files
pre-commit/pre_commit/store.py
Martin von Gagern 8ad5536688 Initialize git submodules
Some packages make use of git submodules, and require all of them to be in place to be installed. One example is my libtidy wrapper for node, which depends on the C sources for libtidy (unless a precompiled binary is available for a given platform). I've been asked to add pre-commit support in https://github.com/gagern/node-libtidy/issues/17 but testing that failed because the source tree was lacking its submodules.
2017-08-07 03:52:06 +01:00

148 lines
4.8 KiB
Python

from __future__ import unicode_literals
import contextlib
import io
import logging
import os.path
import sqlite3
import tempfile
from cached_property import cached_property
import pre_commit.constants as C
from pre_commit.prefixed_command_runner import PrefixedCommandRunner
from pre_commit.util import clean_path_on_failure
from pre_commit.util import cmd_output
from pre_commit.util import copy_tree_to_path
from pre_commit.util import cwd
from pre_commit.util import no_git_env
from pre_commit.util import resource_filename
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.environ.get(
'PRE_COMMIT_HOME',
os.path.join(os.path.expanduser('~'), '.pre-commit'),
)
class Store(object):
get_default_directory = staticmethod(_get_default_directory)
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 _write_sqlite_db(self):
# To avoid a race where someone ^Cs between db creation and execution
# of the CREATE TABLE statement
fd, tmpfile = tempfile.mkstemp(dir=self.directory)
# We'll be managing this file ourselves
os.close(fd)
# sqlite doesn't close its fd with its contextmanager >.<
# contextlib.closing fixes this.
# See: http://stackoverflow.com/a/28032829/812183
with contextlib.closing(sqlite3.connect(tmpfile)) as db:
db.executescript(
'CREATE TABLE repos ('
' repo CHAR(255) NOT NULL,'
' ref CHAR(255) NOT NULL,'
' path CHAR(255) NOT NULL,'
' PRIMARY KEY (repo, ref)'
');',
)
# Atomic file move
os.rename(tmpfile, self.db_path)
def _create(self):
if os.path.exists(self.db_path):
return
if not os.path.exists(self.directory):
os.makedirs(self.directory)
self._write_readme()
self._write_sqlite_db()
def require_created(self):
"""Require the pre-commit file store to be created."""
if not self.__created:
self._create()
self.__created = True
def _new_repo(self, repo, ref, make_strategy):
self.require_created()
# Check if we already exist
with sqlite3.connect(self.db_path) as db:
result = db.execute(
'SELECT path FROM repos WHERE repo = ? AND ref = ?',
[repo, ref],
).fetchone()
if result:
return result[0]
logger.info('Initializing environment for {}.'.format(repo))
directory = tempfile.mkdtemp(prefix='repo', dir=self.directory)
with clean_path_on_failure(directory):
make_strategy(directory)
# Update our db with the created repo
with sqlite3.connect(self.db_path) as db:
db.execute(
'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',
[repo, ref, directory],
)
return directory
def clone(self, repo, ref):
"""Clone the given url and checkout the specific ref."""
def clone_strategy(directory):
cmd_output(
'git', 'clone', '--no-checkout', repo, directory,
env=no_git_env(),
)
with cwd(directory):
cmd_output('git', 'reset', ref, '--hard', env=no_git_env())
cmd_output(
'git', 'submodule', 'update', '--init', '--recursive',
env=no_git_env(),
)
return self._new_repo(repo, ref, clone_strategy)
def make_local(self, deps):
def make_local_strategy(directory):
copy_tree_to_path(resource_filename('empty_template'), directory)
return self._new_repo(
'local:{}'.format(','.join(sorted(deps))), C.LOCAL_REPO_VERSION,
make_local_strategy,
)
@cached_property
def cmd_runner(self):
return PrefixedCommandRunner(self.directory)
@cached_property
def db_path(self):
return os.path.join(self.directory, 'db.db')