11from __future__ import print_function
22from __future__ import unicode_literals
33
4+ import collections
45import os .path
56import re
67
78import six
89from aspy .yaml import ordered_dump
910from aspy .yaml import ordered_load
10- from cfgv import remove_defaults
1111
1212import pre_commit .constants as C
1313from pre_commit import git
1414from pre_commit import output
15- from pre_commit .clientlib import CONFIG_SCHEMA
1615from pre_commit .clientlib import InvalidManifestError
1716from pre_commit .clientlib import load_config
1817from pre_commit .clientlib import load_manifest
2524from pre_commit .util import tmpdir
2625
2726
28- class RepositoryCannotBeUpdatedError (RuntimeError ):
29- pass
30-
27+ class RevInfo (collections .namedtuple ('RevInfo' , ('repo' , 'rev' , 'frozen' ))):
28+ __slots__ = ()
3129
32- def _update_repo (repo_config , store , tags_only ):
33- """Updates a repository to the tip of `master`. If the repository cannot
34- be updated because a hook that is configured does not exist in `master`,
35- this raises a RepositoryCannotBeUpdatedError
30+ @classmethod
31+ def from_config (cls , config ):
32+ return cls (config ['repo' ], config ['rev' ], None )
3633
37- Args:
38- repo_config - A config for a repository
39- """
40- with tmpdir () as repo_path :
41- git .init_repo (repo_path , repo_config ['repo' ])
42- cmd_output_b ('git' , 'fetch' , 'origin' , 'HEAD' , '--tags' , cwd = repo_path )
43-
44- tag_cmd = ('git' , 'describe' , 'FETCH_HEAD' , '--tags' )
34+ def update (self , tags_only , freeze ):
4535 if tags_only :
46- tag_cmd + = ('-- abbrev=0' , )
36+ tag_cmd = ('git' , 'describe' , 'FETCH_HEAD' , '--tags' , '-- abbrev=0' )
4737 else :
48- tag_cmd += ('--exact' ,)
49- try :
50- rev = cmd_output (* tag_cmd , cwd = repo_path )[1 ].strip ()
51- except CalledProcessError :
52- tag_cmd = ('git' , 'rev-parse' , 'FETCH_HEAD' )
53- rev = cmd_output (* tag_cmd , cwd = repo_path )[1 ].strip ()
38+ tag_cmd = ('git' , 'describe' , 'FETCH_HEAD' , '--tags' , '--exact' )
39+
40+ with tmpdir () as tmp :
41+ git .init_repo (tmp , self .repo )
42+ cmd_output_b ('git' , 'fetch' , 'origin' , 'HEAD' , '--tags' , cwd = tmp )
43+
44+ try :
45+ rev = cmd_output (* tag_cmd , cwd = tmp )[1 ].strip ()
46+ except CalledProcessError :
47+ cmd = ('git' , 'rev-parse' , 'FETCH_HEAD' )
48+ rev = cmd_output (* cmd , cwd = tmp )[1 ].strip ()
49+
50+ frozen = None
51+ if freeze :
52+ exact = cmd_output ('git' , 'rev-parse' , rev , cwd = tmp )[1 ].strip ()
53+ if exact != rev :
54+ rev , frozen = exact , rev
55+ return self ._replace (rev = rev , frozen = frozen )
56+
57+
58+ class RepositoryCannotBeUpdatedError (RuntimeError ):
59+ pass
5460
55- # Don't bother trying to update if our rev is the same
56- if rev == repo_config ['rev' ]:
57- return repo_config
5861
62+ def _check_hooks_still_exist_at_rev (repo_config , info , store ):
5963 try :
60- path = store .clone (repo_config ['repo' ], rev )
64+ path = store .clone (repo_config ['repo' ], info . rev )
6165 manifest = load_manifest (os .path .join (path , C .MANIFEST_FILE ))
6266 except InvalidManifestError as e :
6367 raise RepositoryCannotBeUpdatedError (six .text_type (e ))
@@ -71,94 +75,91 @@ def _update_repo(repo_config, store, tags_only):
7175 '{}' .format (', ' .join (sorted (hooks_missing ))),
7276 )
7377
74- # Construct a new config with the head rev
75- new_config = repo_config .copy ()
76- new_config ['rev' ] = rev
77- return new_config
78-
7978
80- REV_LINE_RE = re .compile (r'^(\s+)rev:(\s*)([^\s#]+)(.*)$' , re .DOTALL )
81- REV_LINE_FMT = '{}rev:{}{}{}'
79+ REV_LINE_RE = re .compile (r'^(\s+)rev:(\s*)([^\s#]+)(.*)(\r?\n) $' , re .DOTALL )
80+ REV_LINE_FMT = '{}rev:{}{}{}{} '
8281
8382
84- def _write_new_config_file (path , output ):
83+ def _original_lines (path , rev_infos , retry = False ):
84+ """detect `rev:` lines or reformat the file"""
8585 with open (path ) as f :
86- original_contents = f .read ()
87- output = remove_defaults (output , CONFIG_SCHEMA )
88- new_contents = ordered_dump (output , ** C .YAML_DUMP_KWARGS )
89-
90- lines = original_contents .splitlines (True )
91- rev_line_indices_reversed = list (
92- reversed ([
93- i for i , line in enumerate (lines ) if REV_LINE_RE .match (line )
94- ]),
95- )
96-
97- for line in new_contents .splitlines (True ):
98- if REV_LINE_RE .match (line ):
99- # It's possible we didn't identify the rev lines in the original
100- if not rev_line_indices_reversed :
101- break
102- line_index = rev_line_indices_reversed .pop ()
103- original_line = lines [line_index ]
104- orig_match = REV_LINE_RE .match (original_line )
105- new_match = REV_LINE_RE .match (line )
106- lines [line_index ] = REV_LINE_FMT .format (
107- orig_match .group (1 ), orig_match .group (2 ),
108- new_match .group (3 ), orig_match .group (4 ),
109- )
110-
111- # If we failed to intelligently rewrite the rev lines, fall back to the
112- # pretty-formatted yaml output
113- to_write = '' .join (lines )
114- if remove_defaults (ordered_load (to_write ), CONFIG_SCHEMA ) != output :
115- to_write = new_contents
86+ original = f .read ()
87+
88+ lines = original .splitlines (True )
89+ idxs = [i for i , line in enumerate (lines ) if REV_LINE_RE .match (line )]
90+ if len (idxs ) == len (rev_infos ):
91+ return lines , idxs
92+ elif retry :
93+ raise AssertionError ('could not find rev lines' )
94+ else :
95+ with open (path , 'w' ) as f :
96+ f .write (ordered_dump (ordered_load (original ), ** C .YAML_DUMP_KWARGS ))
97+ return _original_lines (path , rev_infos , retry = True )
98+
99+
100+ def _write_new_config (path , rev_infos ):
101+ lines , idxs = _original_lines (path , rev_infos )
102+
103+ for idx , rev_info in zip (idxs , rev_infos ):
104+ if rev_info is None :
105+ continue
106+ match = REV_LINE_RE .match (lines [idx ])
107+ assert match is not None
108+ new_rev_s = ordered_dump ({'rev' : rev_info .rev }, ** C .YAML_DUMP_KWARGS )
109+ new_rev = new_rev_s .split (':' , 1 )[1 ].strip ()
110+ if rev_info .frozen is not None :
111+ comment = ' # {}' .format (rev_info .frozen )
112+ else :
113+ comment = match .group (4 )
114+ lines [idx ] = REV_LINE_FMT .format (
115+ match .group (1 ), match .group (2 ), new_rev , comment , match .group (5 ),
116+ )
116117
117118 with open (path , 'w' ) as f :
118- f .write (to_write )
119+ f .write ('' . join ( lines ) )
119120
120121
121- def autoupdate (config_file , store , tags_only , repos = ()):
122+ def autoupdate (config_file , store , tags_only , freeze , repos = ()):
122123 """Auto-update the pre-commit config to the latest versions of repos."""
123124 migrate_config (config_file , quiet = True )
124125 retv = 0
125- output_repos = []
126+ rev_infos = []
126127 changed = False
127128
128- input_config = load_config (config_file )
129+ config = load_config (config_file )
130+ for repo_config in config ['repos' ]:
131+ if repo_config ['repo' ] in {LOCAL , META }:
132+ continue
129133
130- for repo_config in input_config ['repos' ]:
131- if (
132- repo_config ['repo' ] in {LOCAL , META } or
133- # Skip updating any repo_configs that aren't for the specified repo
134- repos and repo_config ['repo' ] not in repos
135- ):
136- output_repos .append (repo_config )
134+ info = RevInfo .from_config (repo_config )
135+ if repos and info .repo not in repos :
136+ rev_infos .append (None )
137137 continue
138- output .write ('Updating {}...' .format (repo_config ['repo' ]))
138+
139+ output .write ('Updating {}...' .format (info .repo ))
140+ new_info = info .update (tags_only = tags_only , freeze = freeze )
139141 try :
140- new_repo_config = _update_repo (repo_config , store , tags_only )
142+ _check_hooks_still_exist_at_rev (repo_config , new_info , store )
141143 except RepositoryCannotBeUpdatedError as error :
142144 output .write_line (error .args [0 ])
143- output_repos .append (repo_config )
145+ rev_infos .append (None )
144146 retv = 1
145147 continue
146148
147- if new_repo_config [ ' rev' ] != repo_config [ ' rev' ] :
149+ if new_info . rev != info . rev :
148150 changed = True
149- output .write_line (
150- 'updating {} -> {}.' .format (
151- repo_config ['rev' ], new_repo_config ['rev' ],
152- ),
153- )
154- output_repos .append (new_repo_config )
151+ if new_info .frozen :
152+ updated_to = '{} (frozen)' .format (new_info .frozen )
153+ else :
154+ updated_to = new_info .rev
155+ msg = 'updating {} -> {}.' .format (info .rev , updated_to )
156+ output .write_line (msg )
157+ rev_infos .append (new_info )
155158 else :
156159 output .write_line ('already up to date.' )
157- output_repos .append (repo_config )
160+ rev_infos .append (None )
158161
159162 if changed :
160- output_config = input_config .copy ()
161- output_config ['repos' ] = output_repos
162- _write_new_config_file (config_file , output_config )
163+ _write_new_config (config_file , rev_infos )
163164
164165 return retv
0 commit comments