1- #!/usr/bin/env python
1+ #!/usr/bin/env python3
22#
33# Git command to transform staged files according to a command that accepts file
44# content on stdin and produces output on stdout. This command is useful in
77# ignoring unstaged changes.
88#
99# 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'
1111#
12- # Tested with Python 3.6 and Python 2.7 .
12+ # Tested with Python versions 3.8 - 3.13 .
1313#
1414# Original author: Jesse Hallett <[email protected] > 1515
1616from __future__ import print_function
17+
1718import argparse
18- from fnmatch import fnmatch
19- from gettext import gettext as _
2019import os
2120import re
2221import subprocess
2322import sys
23+ from fnmatch import fnmatch
24+ from gettext import gettext as _
2425
2526# The string $VERSION is replaced during the publish process.
2627VERSION = '$VERSION'
2728PROG = sys .argv [0 ]
2829
2930def info (msg ):
30- print (msg , file = sys .stderr )
31+ print (msg , file = sys .stdout )
3132
3233def warn (msg ):
3334 print ('{}: warning: {}' .format (PROG , msg ), file = sys .stderr )
@@ -36,7 +37,7 @@ def fatal(msg):
3637 print ('{}: error: {}' .format (PROG , msg ), file = sys .stderr )
3738 exit (1 )
3839
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 ):
4041 try :
4142 output = subprocess .check_output ([
4243 'git' , 'diff-index' ,
@@ -48,19 +49,22 @@ def format_staged_files(file_patterns, formatter, git_root, update_working_tree=
4849 for line in output .splitlines ():
4950 entry = parse_diff (line .decode ('utf-8' ))
5051 entry_path = normalize_path (entry ['src_path' ], relative_to = git_root )
52+ if entry ['dst_mode' ] == '120000' :
53+ # Do not process symlinks
54+ continue
5155 if not (matches_some_path (file_patterns , entry_path )):
5256 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 ):
5458 info ('Reformatted {} with {}' .format (entry ['src_path' ], formatter ))
5559 except Exception as err :
5660 fatal (str (err ))
5761
5862# Run formatter on file in the git index. Creates a new git object with the
5963# result, and replaces the content of the file in the index with that object.
6064# 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 ):
6266 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 )
6468
6569 # If the new hash is the same then the formatter did not make any changes.
6670 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=
8387
8488 return new_hash
8589
86- file_path_placeholder = re .compile ('\{\}' )
90+ file_path_placeholder = re .compile (r '\{\}' )
8791
8892# Run formatter on a git blob identified by its hash. Writes output to a new git
8993# 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 ):
9195 get_content = subprocess .Popen (
9296 ['git' , 'cat-file' , '-p' , object_hash ],
9397 stdout = subprocess .PIPE
9498 )
99+ command = re .sub (file_path_placeholder , file_path , formatter )
100+ if verbose :
101+ info (command )
95102 format_content = subprocess .Popen (
96- re . sub ( file_path_placeholder , file_path , formatter ) ,
103+ command ,
97104 shell = True ,
98105 stdin = get_content .stdout ,
99106 stdout = subprocess .PIPE
@@ -142,7 +149,7 @@ def replace_file_in_index(diff_entry, new_object_hash):
142149
143150def patch_working_file (path , orig_object_hash , new_object_hash ):
144151 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 ]
146153 )
147154
148155 # 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):
161168 raise Exception ('could not apply formatting changes to working tree file {}' .format (path ))
162169
163170# 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]+))?$' )
165172
166173# Parse output from `git diff-index`
167174def parse_diff (diff ):
@@ -179,7 +186,7 @@ def parse_diff(diff):
179186 'dst_path' : m .group (8 )
180187 }
181188
182- zeroed_pat = re .compile ('^0+$' )
189+ zeroed_pat = re .compile (r '^0+$' )
183190
184191# Returns the argument unless the argument is a string of zeroes, in which case
185192# returns `None`
@@ -228,12 +235,12 @@ def parse_args(self, args=None, namespace=None):
228235if __name__ == '__main__' :
229236 parser = CustomArgumentParser (
230237 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"'
232239 )
233240 parser .add_argument (
234241 '--formatter' , '-f' ,
235242 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 \' {}\' ")'
237244 )
238245 parser .add_argument (
239246 '--no-update-working-tree' ,
@@ -251,6 +258,11 @@ def parse_args(self, args=None, namespace=None):
251258 version = '%(prog)s version {}' .format (VERSION ),
252259 help = 'Display version of %(prog)s'
253260 )
261+ parser .add_argument (
262+ '--verbose' ,
263+ help = 'Show the formatting commands that are running' ,
264+ action = 'store_true'
265+ )
254266 parser .add_argument (
255267 'files' ,
256268 nargs = '+' ,
@@ -263,5 +275,6 @@ def parse_args(self, args=None, namespace=None):
263275 formatter = vars (args )['formatter' ],
264276 git_root = get_git_root (),
265277 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' ]
267280 )
0 commit comments