11from __future__ import unicode_literals
22
3+ import io
4+ import json
35import logging
6+ import os
47import shutil
58from collections import defaultdict
69
710import pkg_resources
811from cached_property import cached_property
912
13+ from pre_commit import five
1014from pre_commit import git
1115from pre_commit .clientlib .validate_config import is_local_hooks
1216from pre_commit .clientlib .validate_manifest import MANIFEST_JSON_SCHEMA
2327 pkg_resources .get_distribution ('pre-commit' ).version
2428)
2529
30+ # Bump when installation changes in a backwards / forwards incompatible way
31+ INSTALLED_STATE_VERSION = '1'
32+
2633
2734class Repository (object ):
2835 def __init__ (self , repo_config , repo_path_getter ):
@@ -110,14 +117,45 @@ def require_installed(self):
110117
111118 def install (self ):
112119 """Install the hook repository."""
120+ def state (language_name , language_version ):
121+ return {
122+ 'additional_dependencies' : sorted (
123+ self .additional_dependencies [
124+ language_name
125+ ][language_version ],
126+ )
127+ }
128+
129+ def state_filename (venv , suffix = '' ):
130+ return self .cmd_runner .path (
131+ venv , '.install_state_v' + INSTALLED_STATE_VERSION + suffix ,
132+ )
133+
134+ def read_state (venv ):
135+ if not os .path .exists (state_filename (venv )):
136+ return None
137+ else :
138+ return json .loads (io .open (state_filename (venv )).read ())
139+
140+ def write_state (venv , language_name , language_version ):
141+ with io .open (
142+ state_filename (venv , suffix = 'staging' ), 'w' ,
143+ ) as state_file :
144+ state_file .write (five .to_text (json .dumps (
145+ state (language_name , language_version ),
146+ )))
147+ # Move the file into place atomically to indicate we've installed
148+ os .rename (
149+ state_filename (venv , suffix = 'staging' ),
150+ state_filename (venv ),
151+ )
152+
113153 def language_is_installed (language_name , language_version ):
114154 language = languages [language_name ]
115- directory = environment_dir (
116- language .ENVIRONMENT_DIR , language_version ,
117- )
155+ venv = environment_dir (language .ENVIRONMENT_DIR , language_version )
118156 return (
119- directory is None or
120- self . cmd_runner . exists ( directory , '.installed' )
157+ venv is None or
158+ read_state ( venv ) == state ( language_name , language_version )
121159 )
122160
123161 if not all (
@@ -131,24 +169,23 @@ def language_is_installed(language_name, language_version):
131169 logger .info ('This may take a few minutes...' )
132170
133171 for language_name , language_version in self .languages :
134- language = languages [language_name ]
135172 if language_is_installed (language_name , language_version ):
136173 continue
137174
138- directory = environment_dir (
139- language .ENVIRONMENT_DIR , language_version ,
140- )
175+ language = languages [ language_name ]
176+ venv = environment_dir ( language .ENVIRONMENT_DIR , language_version )
177+
141178 # There's potentially incomplete cleanup from previous runs
142179 # Clean it up!
143- if self .cmd_runner .exists (directory ):
144- shutil .rmtree (self .cmd_runner .path (directory ))
180+ if self .cmd_runner .exists (venv ):
181+ shutil .rmtree (self .cmd_runner .path (venv ))
145182
146183 language .install_environment (
147184 self .cmd_runner , language_version ,
148185 self .additional_dependencies [language_name ][language_version ],
149186 )
150- # Touch the .installed file (atomic) to indicate we've installed
151- open ( self . cmd_runner . path ( directory , '.installed' ), 'w' ). close ( )
187+ # Write our state to indicate we're installed
188+ write_state ( venv , language_name , language_version )
152189
153190 def run_hook (self , hook , file_args ):
154191 """Run a hook.
0 commit comments