We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
hook using node doesn't work on FreeBSD
We have a hook using doctoc, the node package. I cannot install the hook, this is the output:

```
desktop1% pre-commit install --install-hooks                                                                                                                                                                                                  
pre-commit installed at .git/hooks/pre-commit
[INFO] Initializing environment for git@github.com:jockej/pre-commit-hooks.git.
[INFO] Initializing environment for https://github.com/thlorenz/doctoc.
[INFO] Initializing environment for https://gitlab.com/pycqa/flake8.
[INFO] Initializing environment for https://github.com/jorisroovers/gitlint.
[INFO] Installing environment for https://github.com/thlorenz/doctoc.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
An unexpected error has occurred: CalledProcessError: command: ('/home/joakim/.cache/pre-commit/repoizbdwm4g/node_env-system/bin/node', '/usr/local/bin/npm', 'install', '-g', '.')
return code: 254
expected return code: 0
stdout: (none)
stderr:
    npm ERR! code ENOENT
    npm ERR! syscall chmod
    npm ERR! path /home/joakim/.cache/pre-commit/repoizbdwm4g/node_env-system/lib/node_modules/doctoc/doctoc.js
    npm ERR! errno -2
    npm ERR! enoent ENOENT: no such file or directory, chmod '/home/joakim/.cache/pre-commit/repoizbdwm4g/node_env-system/lib/node_modules/doctoc/doctoc.js'
    npm ERR! enoent This is related to npm not being able to find a file.
    npm ERR! enoent 
    
    npm ERR! A complete log of this run can be found in:
    npm ERR!     /home/joakim/.npm/_logs/2020-05-07T10_30_16_218Z-debug.log
    
Check the log at /home/joakim/.cache/pre-commit/pre-commit.log
```
These hooks all install and run perfectly on GNU/Linux.

I believe it happens because pre-commit tries to use the --prebuilt flag to nodeenv, however this doesn't work for FreeBSD since there are no prebuilt packages (the URL constructed gives a 404). I have node installed so it should be possible to use --system, but I haven't been able to make it work myself.

Nodeenv itself seems to work fine with --system.

Please let me know if I can help with trying things on FreeBSD.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `pre_commit/store.py`
Content:
```
1 import contextlib
2 import logging
3 import os.path
4 import sqlite3
5 import tempfile
6 from typing import Callable
7 from typing import Generator
8 from typing import List
9 from typing import Optional
10 from typing import Sequence
11 from typing import Tuple
12 
13 import pre_commit.constants as C
14 from pre_commit import file_lock
15 from pre_commit import git
16 from pre_commit.util import CalledProcessError
17 from pre_commit.util import clean_path_on_failure
18 from pre_commit.util import cmd_output_b
19 from pre_commit.util import resource_text
20 from pre_commit.util import rmtree
21 
22 
23 logger = logging.getLogger('pre_commit')
24 
25 
26 def _get_default_directory() -> str:
27     """Returns the default directory for the Store.  This is intentionally
28     underscored to indicate that `Store.get_default_directory` is the intended
29     way to get this information.  This is also done so
30     `Store.get_default_directory` can be mocked in tests and
31     `_get_default_directory` can be tested.
32     """
33     return os.environ.get('PRE_COMMIT_HOME') or os.path.join(
34         os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),
35         'pre-commit',
36     )
37 
38 
39 class Store:
40     get_default_directory = staticmethod(_get_default_directory)
41 
42     def __init__(self, directory: Optional[str] = None) -> None:
43         self.directory = directory or Store.get_default_directory()
44         self.db_path = os.path.join(self.directory, 'db.db')
45 
46         if not os.path.exists(self.directory):
47             os.makedirs(self.directory, exist_ok=True)
48             with open(os.path.join(self.directory, 'README'), 'w') as f:
49                 f.write(
50                     'This directory is maintained by the pre-commit project.\n'
51                     'Learn more: https://github.com/pre-commit/pre-commit\n',
52                 )
53 
54         if os.path.exists(self.db_path):
55             return
56         with self.exclusive_lock():
57             # Another process may have already completed this work
58             if os.path.exists(self.db_path):  # pragma: no cover (race)
59                 return
60             # To avoid a race where someone ^Cs between db creation and
61             # execution of the CREATE TABLE statement
62             fd, tmpfile = tempfile.mkstemp(dir=self.directory)
63             # We'll be managing this file ourselves
64             os.close(fd)
65             with self.connect(db_path=tmpfile) as db:
66                 db.executescript(
67                     'CREATE TABLE repos ('
68                     '    repo TEXT NOT NULL,'
69                     '    ref TEXT NOT NULL,'
70                     '    path TEXT NOT NULL,'
71                     '    PRIMARY KEY (repo, ref)'
72                     ');',
73                 )
74                 self._create_config_table(db)
75 
76             # Atomic file move
77             os.rename(tmpfile, self.db_path)
78 
79     @contextlib.contextmanager
80     def exclusive_lock(self) -> Generator[None, None, None]:
81         def blocked_cb() -> None:  # pragma: no cover (tests are in-process)
82             logger.info('Locking pre-commit directory')
83 
84         with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb):
85             yield
86 
87     @contextlib.contextmanager
88     def connect(
89             self,
90             db_path: Optional[str] = None,
91     ) -> Generator[sqlite3.Connection, None, None]:
92         db_path = db_path or self.db_path
93         # sqlite doesn't close its fd with its contextmanager >.<
94         # contextlib.closing fixes this.
95         # See: https://stackoverflow.com/a/28032829/812183
96         with contextlib.closing(sqlite3.connect(db_path)) as db:
97             # this creates a transaction
98             with db:
99                 yield db
100 
101     @classmethod
102     def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str:
103         if deps:
104             return f'{repo}:{",".join(sorted(deps))}'
105         else:
106             return repo
107 
108     def _new_repo(
109             self,
110             repo: str,
111             ref: str,
112             deps: Sequence[str],
113             make_strategy: Callable[[str], None],
114     ) -> str:
115         repo = self.db_repo_name(repo, deps)
116 
117         def _get_result() -> Optional[str]:
118             # Check if we already exist
119             with self.connect() as db:
120                 result = db.execute(
121                     'SELECT path FROM repos WHERE repo = ? AND ref = ?',
122                     (repo, ref),
123                 ).fetchone()
124                 return result[0] if result else None
125 
126         result = _get_result()
127         if result:
128             return result
129         with self.exclusive_lock():
130             # Another process may have already completed this work
131             result = _get_result()
132             if result:  # pragma: no cover (race)
133                 return result
134 
135             logger.info(f'Initializing environment for {repo}.')
136 
137             directory = tempfile.mkdtemp(prefix='repo', dir=self.directory)
138             with clean_path_on_failure(directory):
139                 make_strategy(directory)
140 
141             # Update our db with the created repo
142             with self.connect() as db:
143                 db.execute(
144                     'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',
145                     [repo, ref, directory],
146                 )
147         return directory
148 
149     def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:
150         """Perform a complete clone of a repository and its submodules """
151 
152         git_cmd('fetch', 'origin', '--tags')
153         git_cmd('checkout', ref)
154         git_cmd('submodule', 'update', '--init', '--recursive')
155 
156     def _shallow_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:
157         """Perform a shallow clone of a repository and its submodules """
158 
159         git_config = 'protocol.version=2'
160         git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1')
161         git_cmd('checkout', 'FETCH_HEAD')
162         git_cmd(
163             '-c', git_config, 'submodule', 'update', '--init', '--recursive',
164             '--depth=1',
165         )
166 
167     def clone(self, repo: str, ref: str, deps: Sequence[str] = ()) -> str:
168         """Clone the given url and checkout the specific ref."""
169 
170         def clone_strategy(directory: str) -> None:
171             git.init_repo(directory, repo)
172             env = git.no_git_env()
173 
174             def _git_cmd(*args: str) -> None:
175                 cmd_output_b('git', *args, cwd=directory, env=env)
176 
177             try:
178                 self._shallow_clone(ref, _git_cmd)
179             except CalledProcessError:
180                 self._complete_clone(ref, _git_cmd)
181 
182         return self._new_repo(repo, ref, deps, clone_strategy)
183 
184     LOCAL_RESOURCES = (
185         'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
186         'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py',
187         'environment.yml', 'Makefile.PL',
188     )
189 
190     def make_local(self, deps: Sequence[str]) -> str:
191         def make_local_strategy(directory: str) -> None:
192             for resource in self.LOCAL_RESOURCES:
193                 contents = resource_text(f'empty_template_{resource}')
194                 with open(os.path.join(directory, resource), 'w') as f:
195                     f.write(contents)
196 
197             env = git.no_git_env()
198 
199             # initialize the git repository so it looks more like cloned repos
200             def _git_cmd(*args: str) -> None:
201                 cmd_output_b('git', *args, cwd=directory, env=env)
202 
203             git.init_repo(directory, '<<unknown>>')
204             _git_cmd('add', '.')
205             git.commit(repo=directory)
206 
207         return self._new_repo(
208             'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy,
209         )
210 
211     def _create_config_table(self, db: sqlite3.Connection) -> None:
212         db.executescript(
213             'CREATE TABLE IF NOT EXISTS configs ('
214             '   path TEXT NOT NULL,'
215             '   PRIMARY KEY (path)'
216             ');',
217         )
218 
219     def mark_config_used(self, path: str) -> None:
220         path = os.path.realpath(path)
221         # don't insert config files that do not exist
222         if not os.path.exists(path):
223             return
224         with self.connect() as db:
225             # TODO: eventually remove this and only create in _create
226             self._create_config_table(db)
227             db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))
228 
229     def select_all_configs(self) -> List[str]:
230         with self.connect() as db:
231             self._create_config_table(db)
232             rows = db.execute('SELECT path FROM configs').fetchall()
233             return [path for path, in rows]
234 
235     def delete_configs(self, configs: List[str]) -> None:
236         with self.connect() as db:
237             rows = [(path,) for path in configs]
238             db.executemany('DELETE FROM configs WHERE path = ?', rows)
239 
240     def select_all_repos(self) -> List[Tuple[str, str, str]]:
241         with self.connect() as db:
242             return db.execute('SELECT repo, ref, path from repos').fetchall()
243 
244     def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None:
245         with self.connect() as db:
246             db.execute(
247                 'DELETE FROM repos WHERE repo = ? and ref = ?',
248                 (db_repo_name, ref),
249             )
250         rmtree(path)
251
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/pre_commit/store.py b/pre_commit/store.py
--- a/pre_commit/store.py
+++ b/pre_commit/store.py
@@ -30,10 +30,11 @@
     `Store.get_default_directory` can be mocked in tests and
     `_get_default_directory` can be tested.
     """
-    return os.environ.get('PRE_COMMIT_HOME') or os.path.join(
+    ret = os.environ.get('PRE_COMMIT_HOME') or os.path.join(
         os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),
         'pre-commit',
     )
+    return os.path.realpath(ret)
 
 
 class Store:

{"golden_diff": "diff --git a/pre_commit/store.py b/pre_commit/store.py\n--- a/pre_commit/store.py\n+++ b/pre_commit/store.py\n@@ -30,10 +30,11 @@\n     `Store.get_default_directory` can be mocked in tests and\n     `_get_default_directory` can be tested.\n     \"\"\"\n-    return os.environ.get('PRE_COMMIT_HOME') or os.path.join(\n+    ret = os.environ.get('PRE_COMMIT_HOME') or os.path.join(\n         os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),\n         'pre-commit',\n     )\n+    return os.path.realpath(ret)\n \n \n class Store:\n", "issue": "hook using node doesn't work on FreeBSD\nWe have a hook using doctoc, the node package. I cannot install the hook, this is the output:\r\n\r\n```\r\ndesktop1% pre-commit install --install-hooks                                                                                                                                                                                                  \r\npre-commit installed at .git/hooks/pre-commit\r\n[INFO] Initializing environment for git@github.com:jockej/pre-commit-hooks.git.\r\n[INFO] Initializing environment for https://github.com/thlorenz/doctoc.\r\n[INFO] Initializing environment for https://gitlab.com/pycqa/flake8.\r\n[INFO] Initializing environment for https://github.com/jorisroovers/gitlint.\r\n[INFO] Installing environment for https://github.com/thlorenz/doctoc.\r\n[INFO] Once installed this environment will be reused.\r\n[INFO] This may take a few minutes...\r\nAn unexpected error has occurred: CalledProcessError: command: ('/home/joakim/.cache/pre-commit/repoizbdwm4g/node_env-system/bin/node', '/usr/local/bin/npm', 'install', '-g', '.')\r\nreturn code: 254\r\nexpected return code: 0\r\nstdout: (none)\r\nstderr:\r\n    npm ERR! code ENOENT\r\n    npm ERR! syscall chmod\r\n    npm ERR! path /home/joakim/.cache/pre-commit/repoizbdwm4g/node_env-system/lib/node_modules/doctoc/doctoc.js\r\n    npm ERR! errno -2\r\n    npm ERR! enoent ENOENT: no such file or directory, chmod '/home/joakim/.cache/pre-commit/repoizbdwm4g/node_env-system/lib/node_modules/doctoc/doctoc.js'\r\n    npm ERR! enoent This is related to npm not being able to find a file.\r\n    npm ERR! enoent \r\n    \r\n    npm ERR! A complete log of this run can be found in:\r\n    npm ERR!     /home/joakim/.npm/_logs/2020-05-07T10_30_16_218Z-debug.log\r\n    \r\nCheck the log at /home/joakim/.cache/pre-commit/pre-commit.log\r\n```\r\nThese hooks all install and run perfectly on GNU/Linux.\r\n\r\nI believe it happens because pre-commit tries to use the --prebuilt flag to nodeenv, however this doesn't work for FreeBSD since there are no prebuilt packages (the URL constructed gives a 404). I have node installed so it should be possible to use --system, but I haven't been able to make it work myself.\r\n\r\nNodeenv itself seems to work fine with --system.\r\n\r\nPlease let me know if I can help with trying things on FreeBSD.\r\n\n", "before_files": [{"content": "import contextlib\nimport logging\nimport os.path\nimport sqlite3\nimport tempfile\nfrom typing import Callable\nfrom typing import Generator\nfrom typing import List\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import Tuple\n\nimport pre_commit.constants as C\nfrom pre_commit import file_lock\nfrom pre_commit import git\nfrom pre_commit.util import CalledProcessError\nfrom pre_commit.util import clean_path_on_failure\nfrom pre_commit.util import cmd_output_b\nfrom pre_commit.util import resource_text\nfrom pre_commit.util import rmtree\n\n\nlogger = logging.getLogger('pre_commit')\n\n\ndef _get_default_directory() -> str:\n    \"\"\"Returns the default directory for the Store.  This is intentionally\n    underscored to indicate that `Store.get_default_directory` is the intended\n    way to get this information.  This is also done so\n    `Store.get_default_directory` can be mocked in tests and\n    `_get_default_directory` can be tested.\n    \"\"\"\n    return os.environ.get('PRE_COMMIT_HOME') or os.path.join(\n        os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),\n        'pre-commit',\n    )\n\n\nclass Store:\n    get_default_directory = staticmethod(_get_default_directory)\n\n    def __init__(self, directory: Optional[str] = None) -> None:\n        self.directory = directory or Store.get_default_directory()\n        self.db_path = os.path.join(self.directory, 'db.db')\n\n        if not os.path.exists(self.directory):\n            os.makedirs(self.directory, exist_ok=True)\n            with open(os.path.join(self.directory, 'README'), 'w') as f:\n                f.write(\n                    'This directory is maintained by the pre-commit project.\\n'\n                    'Learn more: https://github.com/pre-commit/pre-commit\\n',\n                )\n\n        if os.path.exists(self.db_path):\n            return\n        with self.exclusive_lock():\n            # Another process may have already completed this work\n            if os.path.exists(self.db_path):  # pragma: no cover (race)\n                return\n            # To avoid a race where someone ^Cs between db creation and\n            # execution of the CREATE TABLE statement\n            fd, tmpfile = tempfile.mkstemp(dir=self.directory)\n            # We'll be managing this file ourselves\n            os.close(fd)\n            with self.connect(db_path=tmpfile) as db:\n                db.executescript(\n                    'CREATE TABLE repos ('\n                    '    repo TEXT NOT NULL,'\n                    '    ref TEXT NOT NULL,'\n                    '    path TEXT NOT NULL,'\n                    '    PRIMARY KEY (repo, ref)'\n                    ');',\n                )\n                self._create_config_table(db)\n\n            # Atomic file move\n            os.rename(tmpfile, self.db_path)\n\n    @contextlib.contextmanager\n    def exclusive_lock(self) -> Generator[None, None, None]:\n        def blocked_cb() -> None:  # pragma: no cover (tests are in-process)\n            logger.info('Locking pre-commit directory')\n\n        with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb):\n            yield\n\n    @contextlib.contextmanager\n    def connect(\n            self,\n            db_path: Optional[str] = None,\n    ) -> Generator[sqlite3.Connection, None, None]:\n        db_path = db_path or self.db_path\n        # sqlite doesn't close its fd with its contextmanager >.<\n        # contextlib.closing fixes this.\n        # See: https://stackoverflow.com/a/28032829/812183\n        with contextlib.closing(sqlite3.connect(db_path)) as db:\n            # this creates a transaction\n            with db:\n                yield db\n\n    @classmethod\n    def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str:\n        if deps:\n            return f'{repo}:{\",\".join(sorted(deps))}'\n        else:\n            return repo\n\n    def _new_repo(\n            self,\n            repo: str,\n            ref: str,\n            deps: Sequence[str],\n            make_strategy: Callable[[str], None],\n    ) -> str:\n        repo = self.db_repo_name(repo, deps)\n\n        def _get_result() -> Optional[str]:\n            # Check if we already exist\n            with self.connect() as db:\n                result = db.execute(\n                    'SELECT path FROM repos WHERE repo = ? AND ref = ?',\n                    (repo, ref),\n                ).fetchone()\n                return result[0] if result else None\n\n        result = _get_result()\n        if result:\n            return result\n        with self.exclusive_lock():\n            # Another process may have already completed this work\n            result = _get_result()\n            if result:  # pragma: no cover (race)\n                return result\n\n            logger.info(f'Initializing environment for {repo}.')\n\n            directory = tempfile.mkdtemp(prefix='repo', dir=self.directory)\n            with clean_path_on_failure(directory):\n                make_strategy(directory)\n\n            # Update our db with the created repo\n            with self.connect() as db:\n                db.execute(\n                    'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',\n                    [repo, ref, directory],\n                )\n        return directory\n\n    def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:\n        \"\"\"Perform a complete clone of a repository and its submodules \"\"\"\n\n        git_cmd('fetch', 'origin', '--tags')\n        git_cmd('checkout', ref)\n        git_cmd('submodule', 'update', '--init', '--recursive')\n\n    def _shallow_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:\n        \"\"\"Perform a shallow clone of a repository and its submodules \"\"\"\n\n        git_config = 'protocol.version=2'\n        git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1')\n        git_cmd('checkout', 'FETCH_HEAD')\n        git_cmd(\n            '-c', git_config, 'submodule', 'update', '--init', '--recursive',\n            '--depth=1',\n        )\n\n    def clone(self, repo: str, ref: str, deps: Sequence[str] = ()) -> str:\n        \"\"\"Clone the given url and checkout the specific ref.\"\"\"\n\n        def clone_strategy(directory: str) -> None:\n            git.init_repo(directory, repo)\n            env = git.no_git_env()\n\n            def _git_cmd(*args: str) -> None:\n                cmd_output_b('git', *args, cwd=directory, env=env)\n\n            try:\n                self._shallow_clone(ref, _git_cmd)\n            except CalledProcessError:\n                self._complete_clone(ref, _git_cmd)\n\n        return self._new_repo(repo, ref, deps, clone_strategy)\n\n    LOCAL_RESOURCES = (\n        'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',\n        'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py',\n        'environment.yml', 'Makefile.PL',\n    )\n\n    def make_local(self, deps: Sequence[str]) -> str:\n        def make_local_strategy(directory: str) -> None:\n            for resource in self.LOCAL_RESOURCES:\n                contents = resource_text(f'empty_template_{resource}')\n                with open(os.path.join(directory, resource), 'w') as f:\n                    f.write(contents)\n\n            env = git.no_git_env()\n\n            # initialize the git repository so it looks more like cloned repos\n            def _git_cmd(*args: str) -> None:\n                cmd_output_b('git', *args, cwd=directory, env=env)\n\n            git.init_repo(directory, '<<unknown>>')\n            _git_cmd('add', '.')\n            git.commit(repo=directory)\n\n        return self._new_repo(\n            'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy,\n        )\n\n    def _create_config_table(self, db: sqlite3.Connection) -> None:\n        db.executescript(\n            'CREATE TABLE IF NOT EXISTS configs ('\n            '   path TEXT NOT NULL,'\n            '   PRIMARY KEY (path)'\n            ');',\n        )\n\n    def mark_config_used(self, path: str) -> None:\n        path = os.path.realpath(path)\n        # don't insert config files that do not exist\n        if not os.path.exists(path):\n            return\n        with self.connect() as db:\n            # TODO: eventually remove this and only create in _create\n            self._create_config_table(db)\n            db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))\n\n    def select_all_configs(self) -> List[str]:\n        with self.connect() as db:\n            self._create_config_table(db)\n            rows = db.execute('SELECT path FROM configs').fetchall()\n            return [path for path, in rows]\n\n    def delete_configs(self, configs: List[str]) -> None:\n        with self.connect() as db:\n            rows = [(path,) for path in configs]\n            db.executemany('DELETE FROM configs WHERE path = ?', rows)\n\n    def select_all_repos(self) -> List[Tuple[str, str, str]]:\n        with self.connect() as db:\n            return db.execute('SELECT repo, ref, path from repos').fetchall()\n\n    def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None:\n        with self.connect() as db:\n            db.execute(\n                'DELETE FROM repos WHERE repo = ? and ref = ?',\n                (db_repo_name, ref),\n            )\n        rmtree(path)\n", "path": "pre_commit/store.py"}], "after_files": [{"content": "import contextlib\nimport logging\nimport os.path\nimport sqlite3\nimport tempfile\nfrom typing import Callable\nfrom typing import Generator\nfrom typing import List\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import Tuple\n\nimport pre_commit.constants as C\nfrom pre_commit import file_lock\nfrom pre_commit import git\nfrom pre_commit.util import CalledProcessError\nfrom pre_commit.util import clean_path_on_failure\nfrom pre_commit.util import cmd_output_b\nfrom pre_commit.util import resource_text\nfrom pre_commit.util import rmtree\n\n\nlogger = logging.getLogger('pre_commit')\n\n\ndef _get_default_directory() -> str:\n    \"\"\"Returns the default directory for the Store.  This is intentionally\n    underscored to indicate that `Store.get_default_directory` is the intended\n    way to get this information.  This is also done so\n    `Store.get_default_directory` can be mocked in tests and\n    `_get_default_directory` can be tested.\n    \"\"\"\n    ret = os.environ.get('PRE_COMMIT_HOME') or os.path.join(\n        os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),\n        'pre-commit',\n    )\n    return os.path.realpath(ret)\n\n\nclass Store:\n    get_default_directory = staticmethod(_get_default_directory)\n\n    def __init__(self, directory: Optional[str] = None) -> None:\n        self.directory = directory or Store.get_default_directory()\n        self.db_path = os.path.join(self.directory, 'db.db')\n\n        if not os.path.exists(self.directory):\n            os.makedirs(self.directory, exist_ok=True)\n            with open(os.path.join(self.directory, 'README'), 'w') as f:\n                f.write(\n                    'This directory is maintained by the pre-commit project.\\n'\n                    'Learn more: https://github.com/pre-commit/pre-commit\\n',\n                )\n\n        if os.path.exists(self.db_path):\n            return\n        with self.exclusive_lock():\n            # Another process may have already completed this work\n            if os.path.exists(self.db_path):  # pragma: no cover (race)\n                return\n            # To avoid a race where someone ^Cs between db creation and\n            # execution of the CREATE TABLE statement\n            fd, tmpfile = tempfile.mkstemp(dir=self.directory)\n            # We'll be managing this file ourselves\n            os.close(fd)\n            with self.connect(db_path=tmpfile) as db:\n                db.executescript(\n                    'CREATE TABLE repos ('\n                    '    repo TEXT NOT NULL,'\n                    '    ref TEXT NOT NULL,'\n                    '    path TEXT NOT NULL,'\n                    '    PRIMARY KEY (repo, ref)'\n                    ');',\n                )\n                self._create_config_table(db)\n\n            # Atomic file move\n            os.rename(tmpfile, self.db_path)\n\n    @contextlib.contextmanager\n    def exclusive_lock(self) -> Generator[None, None, None]:\n        def blocked_cb() -> None:  # pragma: no cover (tests are in-process)\n            logger.info('Locking pre-commit directory')\n\n        with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb):\n            yield\n\n    @contextlib.contextmanager\n    def connect(\n            self,\n            db_path: Optional[str] = None,\n    ) -> Generator[sqlite3.Connection, None, None]:\n        db_path = db_path or self.db_path\n        # sqlite doesn't close its fd with its contextmanager >.<\n        # contextlib.closing fixes this.\n        # See: https://stackoverflow.com/a/28032829/812183\n        with contextlib.closing(sqlite3.connect(db_path)) as db:\n            # this creates a transaction\n            with db:\n                yield db\n\n    @classmethod\n    def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str:\n        if deps:\n            return f'{repo}:{\",\".join(sorted(deps))}'\n        else:\n            return repo\n\n    def _new_repo(\n            self,\n            repo: str,\n            ref: str,\n            deps: Sequence[str],\n            make_strategy: Callable[[str], None],\n    ) -> str:\n        repo = self.db_repo_name(repo, deps)\n\n        def _get_result() -> Optional[str]:\n            # Check if we already exist\n            with self.connect() as db:\n                result = db.execute(\n                    'SELECT path FROM repos WHERE repo = ? AND ref = ?',\n                    (repo, ref),\n                ).fetchone()\n                return result[0] if result else None\n\n        result = _get_result()\n        if result:\n            return result\n        with self.exclusive_lock():\n            # Another process may have already completed this work\n            result = _get_result()\n            if result:  # pragma: no cover (race)\n                return result\n\n            logger.info(f'Initializing environment for {repo}.')\n\n            directory = tempfile.mkdtemp(prefix='repo', dir=self.directory)\n            with clean_path_on_failure(directory):\n                make_strategy(directory)\n\n            # Update our db with the created repo\n            with self.connect() as db:\n                db.execute(\n                    'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',\n                    [repo, ref, directory],\n                )\n        return directory\n\n    def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:\n        \"\"\"Perform a complete clone of a repository and its submodules \"\"\"\n\n        git_cmd('fetch', 'origin', '--tags')\n        git_cmd('checkout', ref)\n        git_cmd('submodule', 'update', '--init', '--recursive')\n\n    def _shallow_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:\n        \"\"\"Perform a shallow clone of a repository and its submodules \"\"\"\n\n        git_config = 'protocol.version=2'\n        git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1')\n        git_cmd('checkout', 'FETCH_HEAD')\n        git_cmd(\n            '-c', git_config, 'submodule', 'update', '--init', '--recursive',\n            '--depth=1',\n        )\n\n    def clone(self, repo: str, ref: str, deps: Sequence[str] = ()) -> str:\n        \"\"\"Clone the given url and checkout the specific ref.\"\"\"\n\n        def clone_strategy(directory: str) -> None:\n            git.init_repo(directory, repo)\n            env = git.no_git_env()\n\n            def _git_cmd(*args: str) -> None:\n                cmd_output_b('git', *args, cwd=directory, env=env)\n\n            try:\n                self._shallow_clone(ref, _git_cmd)\n            except CalledProcessError:\n                self._complete_clone(ref, _git_cmd)\n\n        return self._new_repo(repo, ref, deps, clone_strategy)\n\n    LOCAL_RESOURCES = (\n        'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',\n        'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py',\n        'environment.yml', 'Makefile.PL',\n    )\n\n    def make_local(self, deps: Sequence[str]) -> str:\n        def make_local_strategy(directory: str) -> None:\n            for resource in self.LOCAL_RESOURCES:\n                contents = resource_text(f'empty_template_{resource}')\n                with open(os.path.join(directory, resource), 'w') as f:\n                    f.write(contents)\n\n            env = git.no_git_env()\n\n            # initialize the git repository so it looks more like cloned repos\n            def _git_cmd(*args: str) -> None:\n                cmd_output_b('git', *args, cwd=directory, env=env)\n\n            git.init_repo(directory, '<<unknown>>')\n            _git_cmd('add', '.')\n            git.commit(repo=directory)\n\n        return self._new_repo(\n            'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy,\n        )\n\n    def _create_config_table(self, db: sqlite3.Connection) -> None:\n        db.executescript(\n            'CREATE TABLE IF NOT EXISTS configs ('\n            '   path TEXT NOT NULL,'\n            '   PRIMARY KEY (path)'\n            ');',\n        )\n\n    def mark_config_used(self, path: str) -> None:\n        path = os.path.realpath(path)\n        # don't insert config files that do not exist\n        if not os.path.exists(path):\n            return\n        with self.connect() as db:\n            # TODO: eventually remove this and only create in _create\n            self._create_config_table(db)\n            db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))\n\n    def select_all_configs(self) -> List[str]:\n        with self.connect() as db:\n            self._create_config_table(db)\n            rows = db.execute('SELECT path FROM configs').fetchall()\n            return [path for path, in rows]\n\n    def delete_configs(self, configs: List[str]) -> None:\n        with self.connect() as db:\n            rows = [(path,) for path in configs]\n            db.executemany('DELETE FROM configs WHERE path = ?', rows)\n\n    def select_all_repos(self) -> List[Tuple[str, str, str]]:\n        with self.connect() as db:\n            return db.execute('SELECT repo, ref, path from repos').fetchall()\n\n    def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None:\n        with self.connect() as db:\n            db.execute(\n                'DELETE FROM repos WHERE repo = ? and ref = ?',\n                (db_repo_name, ref),\n            )\n        rmtree(path)\n", "path": "pre_commit/store.py"}]}