1
- #!/usr/bin/env python
1
+ #!/usr/bin/env python3
2
2
#
3
3
# Git command to transform staged files according to a command that accepts file
4
4
# content on stdin and produces output on stdout. This command is useful in
7
7
# ignoring unstaged changes.
8
8
#
9
9
# Usage: git-format-staged [OPTION]... [FILE]...
10
- # Example: git-format-staged --formatter 'prettier --stdin' '*.js'
10
+ # Example: git-format-staged --formatter 'prettier --stdin-filepath "{}" ' '*.js'
11
11
#
12
- # Tested with Python 3.6 and Python 2.7 .
12
+ # Tested with Python versions 3.8 - 3.13 .
13
13
#
14
14
# Original author: Jesse Hallett <[email protected] >
15
15
16
16
from __future__ import print_function
17
+
17
18
import argparse
18
- from fnmatch import fnmatch
19
- from gettext import gettext as _
20
19
import os
21
20
import re
22
21
import subprocess
23
22
import sys
23
+ from fnmatch import fnmatch
24
+ from gettext import gettext as _
24
25
25
26
# The string $VERSION is replaced during the publish process.
26
27
VERSION = '$VERSION'
27
28
PROG = sys .argv [0 ]
28
29
29
30
def info (msg ):
30
- print (msg , file = sys .stderr )
31
+ print (msg , file = sys .stdout )
31
32
32
33
def warn (msg ):
33
34
print ('{}: warning: {}' .format (PROG , msg ), file = sys .stderr )
@@ -36,7 +37,7 @@ def fatal(msg):
36
37
print ('{}: error: {}' .format (PROG , msg ), file = sys .stderr )
37
38
exit (1 )
38
39
39
- def format_staged_files (file_patterns , formatter , git_root , update_working_tree = True , write = True ):
40
+ def format_staged_files (file_patterns , formatter , git_root , update_working_tree = True , write = True , verbose = False ):
40
41
try :
41
42
output = subprocess .check_output ([
42
43
'git' , 'diff-index' ,
@@ -48,19 +49,22 @@ def format_staged_files(file_patterns, formatter, git_root, update_working_tree=
48
49
for line in output .splitlines ():
49
50
entry = parse_diff (line .decode ('utf-8' ))
50
51
entry_path = normalize_path (entry ['src_path' ], relative_to = git_root )
52
+ if entry ['dst_mode' ] == '120000' :
53
+ # Do not process symlinks
54
+ continue
51
55
if not (matches_some_path (file_patterns , entry_path )):
52
56
continue
53
- if format_file_in_index (formatter , entry , update_working_tree = update_working_tree , write = write ):
57
+ if format_file_in_index (formatter , entry , update_working_tree = update_working_tree , write = write , verbose = verbose ):
54
58
info ('Reformatted {} with {}' .format (entry ['src_path' ], formatter ))
55
59
except Exception as err :
56
60
fatal (str (err ))
57
61
58
62
# Run formatter on file in the git index. Creates a new git object with the
59
63
# result, and replaces the content of the file in the index with that object.
60
64
# Returns hash of the new object if formatting produced any changes.
61
- def format_file_in_index (formatter , diff_entry , update_working_tree = True , write = True ):
65
+ def format_file_in_index (formatter , diff_entry , update_working_tree = True , write = True , verbose = False ):
62
66
orig_hash = diff_entry ['dst_hash' ]
63
- new_hash = format_object (formatter , orig_hash , diff_entry ['src_path' ])
67
+ new_hash = format_object (formatter , orig_hash , diff_entry ['src_path' ], verbose = verbose )
64
68
65
69
# If the new hash is the same then the formatter did not make any changes.
66
70
if not write or new_hash == orig_hash :
@@ -83,17 +87,20 @@ def format_file_in_index(formatter, diff_entry, update_working_tree=True, write=
83
87
84
88
return new_hash
85
89
86
- file_path_placeholder = re .compile ('\{\}' )
90
+ file_path_placeholder = re .compile (r '\{\}' )
87
91
88
92
# Run formatter on a git blob identified by its hash. Writes output to a new git
89
93
# blob, and returns the hash of the new blob.
90
- def format_object (formatter , object_hash , file_path ):
94
+ def format_object (formatter , object_hash , file_path , verbose = False ):
91
95
get_content = subprocess .Popen (
92
96
['git' , 'cat-file' , '-p' , object_hash ],
93
97
stdout = subprocess .PIPE
94
98
)
99
+ command = re .sub (file_path_placeholder , file_path , formatter )
100
+ if verbose :
101
+ info (command )
95
102
format_content = subprocess .Popen (
96
- re . sub ( file_path_placeholder , file_path , formatter ) ,
103
+ command ,
97
104
shell = True ,
98
105
stdin = get_content .stdout ,
99
106
stdout = subprocess .PIPE
@@ -142,7 +149,7 @@ def replace_file_in_index(diff_entry, new_object_hash):
142
149
143
150
def patch_working_file (path , orig_object_hash , new_object_hash ):
144
151
patch = subprocess .check_output (
145
- ['git' , 'diff' , orig_object_hash , new_object_hash ]
152
+ ['git' , 'diff' , '--no-ext-diff' , '--color=never' , orig_object_hash , new_object_hash ]
146
153
)
147
154
148
155
# Substitute object hashes in patch header with path to working tree file
@@ -161,7 +168,7 @@ def patch_working_file(path, orig_object_hash, new_object_hash):
161
168
raise Exception ('could not apply formatting changes to working tree file {}' .format (path ))
162
169
163
170
# Format: src_mode dst_mode src_hash dst_hash status/score? src_path dst_path?
164
- diff_pat = re .compile ('^:(\d+) (\d+) ([a-f0-9]+) ([a-f0-9]+) ([A-Z])(\d+)?\t ([^\t ]+)(?:\t ([^\t ]+))?$' )
171
+ diff_pat = re .compile (r '^:(\d+) (\d+) ([a-f0-9]+) ([a-f0-9]+) ([A-Z])(\d+)?\t([^\t]+)(?:\t([^\t]+))?$' )
165
172
166
173
# Parse output from `git diff-index`
167
174
def parse_diff (diff ):
@@ -179,7 +186,7 @@ def parse_diff(diff):
179
186
'dst_path' : m .group (8 )
180
187
}
181
188
182
- zeroed_pat = re .compile ('^0+$' )
189
+ zeroed_pat = re .compile (r '^0+$' )
183
190
184
191
# Returns the argument unless the argument is a string of zeroes, in which case
185
192
# returns `None`
@@ -228,12 +235,12 @@ def parse_args(self, args=None, namespace=None):
228
235
if __name__ == '__main__' :
229
236
parser = CustomArgumentParser (
230
237
description = 'Transform staged files using a formatting command that accepts content via stdin and produces a result via stdout.' ,
231
- epilog = 'Example: %(prog)s --formatter "prettier --stdin" "src/*.js" "test/*.js"'
238
+ epilog = 'Example: %(prog)s --formatter "prettier --stdin-filepath \' {} \' " "src/*.js" "test/*.js"'
232
239
)
233
240
parser .add_argument (
234
241
'--formatter' , '-f' ,
235
242
required = True ,
236
- help = 'Shell command to format files, will run once per file. Occurrences of the placeholder `{}` will be replaced with a path to the file being formatted. (Example: "prettier --stdin --stdin -filepath \' {}\' ")'
243
+ help = 'Shell command to format files, will run once per file. Occurrences of the placeholder `{}` will be replaced with a path to the file being formatted. (Example: "prettier --stdin-filepath \' {}\' ")'
237
244
)
238
245
parser .add_argument (
239
246
'--no-update-working-tree' ,
@@ -251,6 +258,11 @@ def parse_args(self, args=None, namespace=None):
251
258
version = '%(prog)s version {}' .format (VERSION ),
252
259
help = 'Display version of %(prog)s'
253
260
)
261
+ parser .add_argument (
262
+ '--verbose' ,
263
+ help = 'Show the formatting commands that are running' ,
264
+ action = 'store_true'
265
+ )
254
266
parser .add_argument (
255
267
'files' ,
256
268
nargs = '+' ,
@@ -263,5 +275,6 @@ def parse_args(self, args=None, namespace=None):
263
275
formatter = vars (args )['formatter' ],
264
276
git_root = get_git_root (),
265
277
update_working_tree = not vars (args )['no_update_working_tree' ],
266
- write = not vars (args )['no_write' ]
278
+ write = not vars (args )['no_write' ],
279
+ verbose = vars (args )['verbose' ]
267
280
)
0 commit comments