git-format-staged 3.1.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/git-format-staged +242 -162
- package/package.json +4 -11
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# git-format-staged
|
|
2
2
|
|
|
3
|
-
[](https://travis-ci.org/hallettj/git-format-staged)
|
|
4
|
-
|
|
5
3
|
Consider a project where you want all code formatted consistently. So you use
|
|
6
4
|
a formatting command. (For example I use [prettier][] in my Javascript and
|
|
7
5
|
Typescript projects.) You want to make sure that everyone working on the project
|
|
@@ -39,13 +37,13 @@ patch step can be disabled with the `--no-update-working-tree` option.
|
|
|
39
37
|
|
|
40
38
|
Install via the CLI:
|
|
41
39
|
|
|
42
|
-
$ nix profile
|
|
40
|
+
$ nix profile install github:hallettj/git-format-staged
|
|
43
41
|
|
|
44
42
|
Or add to your flake imports, and use the `default` package output.
|
|
45
43
|
|
|
46
44
|
### Install with NPM
|
|
47
45
|
|
|
48
|
-
Requires Python
|
|
46
|
+
Requires Python 3.8 or later.
|
|
49
47
|
|
|
50
48
|
Install as a development dependency in a project that uses npm packages:
|
|
51
49
|
|
|
@@ -57,7 +55,7 @@ Or install globally:
|
|
|
57
55
|
|
|
58
56
|
### Or just copy the script
|
|
59
57
|
|
|
60
|
-
Requires Python
|
|
58
|
+
Requires Python 3.8 or later.
|
|
61
59
|
|
|
62
60
|
If you do not use the above methods you can copy the
|
|
63
61
|
[`git-format-staged`](./git-format-staged) script from this repository and
|
|
@@ -73,7 +71,7 @@ For detailed information run:
|
|
|
73
71
|
The command expects a shell command to run a formatter, and one or more file
|
|
74
72
|
patterns to identify which files should be formatted. For example:
|
|
75
73
|
|
|
76
|
-
$ git-format-staged --formatter 'prettier --stdin-filepath
|
|
74
|
+
$ git-format-staged --formatter 'prettier --stdin-filepath {}' 'src/*.js'
|
|
77
75
|
|
|
78
76
|
That will format all files under `src/` and its subdirectories using
|
|
79
77
|
`prettier`. The file pattern is tested against staged files using Python's
|
|
@@ -90,11 +88,11 @@ normal shell globbing. So if you need to match multiple patterns, you should
|
|
|
90
88
|
pass multiple arguments with different patterns, and they will be grouped.
|
|
91
89
|
So instead of e.g. `'src/**/*.{js,jsx,ts}'`, you would use:
|
|
92
90
|
|
|
93
|
-
$ git-format-staged --formatter 'prettier --stdin-filepath
|
|
91
|
+
$ git-format-staged --formatter 'prettier --stdin-filepath {}' 'src/*.js' 'src/*.jsx' 'src/*.ts'
|
|
94
92
|
|
|
95
93
|
Files can be excluded by prefixing a pattern with `!`. For example:
|
|
96
94
|
|
|
97
|
-
$ git-format-staged --formatter 'prettier --stdin-filepath
|
|
95
|
+
$ git-format-staged --formatter 'prettier --stdin-filepath {}' '*.js' '!flow-typed/*'
|
|
98
96
|
|
|
99
97
|
Patterns are evaluated from left-to-right: if a file matches multiple patterns
|
|
100
98
|
the right-most pattern determines whether the file is included or excluded.
|
|
@@ -104,11 +102,11 @@ control. So it is not necessary to explicitly exclude stuff like
|
|
|
104
102
|
`node_modules/`.
|
|
105
103
|
|
|
106
104
|
The formatter command may include a placeholder, `{}`, which will be replaced
|
|
107
|
-
with the path of the file that is being formatted
|
|
108
|
-
formatter needs to know the file extension to determine
|
|
109
|
-
lint each file. For example:
|
|
105
|
+
with the path of the file that is being formatted (with appropriate quoting).
|
|
106
|
+
This is useful if your formatter needs to know the file extension to determine
|
|
107
|
+
how to format or to lint each file. For example:
|
|
110
108
|
|
|
111
|
-
$ git-format-staged -f 'prettier --stdin-filepath
|
|
109
|
+
$ git-format-staged -f 'prettier --stdin-filepath {}' '*.js' '*.css'
|
|
112
110
|
|
|
113
111
|
Do not attempt to read or write to `{}` in your formatter command! The
|
|
114
112
|
placeholder exists only for referencing the file name and path.
|
|
@@ -120,7 +118,7 @@ prevent files from being committed if they do not conform to style rules. You
|
|
|
120
118
|
can use git-format-staged with the `--no-write` option, and supply a lint
|
|
121
119
|
command instead of a format command. Here is an example using ESLint:
|
|
122
120
|
|
|
123
|
-
$ git-format-staged --no-write -f 'eslint --stdin --stdin-filename
|
|
121
|
+
$ git-format-staged --no-write -f 'eslint --stdin --stdin-filename {} >&2' 'src/*.js'
|
|
124
122
|
|
|
125
123
|
If this command is run in a pre-commit hook, and the lint command fails the
|
|
126
124
|
commit will be aborted and error messages will be displayed. The lint command
|
|
@@ -146,7 +144,7 @@ Add a `prepare` script to install husky when running `npm install`:
|
|
|
146
144
|
|
|
147
145
|
Add the pre-commit hook:
|
|
148
146
|
|
|
149
|
-
$ npx husky add .husky/pre-commit "git-format-staged --formatter 'prettier --stdin-filepath
|
|
147
|
+
$ npx husky add .husky/pre-commit "git-format-staged --formatter 'prettier --stdin-filepath {}' '*.js' '*.ts'"
|
|
150
148
|
$ git add .husky/pre-commit
|
|
151
149
|
|
|
152
150
|
Once again note that the formatter command and the `'*.js'` and `'*.ts'`
|
package/git-format-staged
CHANGED
|
@@ -7,63 +7,106 @@
|
|
|
7
7
|
# ignoring unstaged changes.
|
|
8
8
|
#
|
|
9
9
|
# Usage: git-format-staged [OPTION]... [FILE]...
|
|
10
|
-
# Example: git-format-staged --formatter 'prettier --stdin-filepath
|
|
10
|
+
# Example: git-format-staged --formatter 'prettier --stdin-filepath {}' '*.js'
|
|
11
11
|
#
|
|
12
|
-
# Tested with Python 3.
|
|
12
|
+
# Tested with Python versions 3.8 - 3.15.
|
|
13
13
|
#
|
|
14
14
|
# Original author: Jesse Hallett <jesse@sitr.us>
|
|
15
15
|
|
|
16
|
-
from __future__ import
|
|
16
|
+
from __future__ import annotations
|
|
17
17
|
import argparse
|
|
18
|
+
from collections.abc import Sequence
|
|
18
19
|
from fnmatch import fnmatch
|
|
19
20
|
from gettext import gettext as _
|
|
20
|
-
import os
|
|
21
21
|
import re
|
|
22
|
+
import shlex
|
|
22
23
|
import subprocess
|
|
23
24
|
import sys
|
|
25
|
+
from typing import NoReturn, Protocol, cast
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
# The string 4.0.0 is replaced during the publish process.
|
|
29
|
+
VERSION = "4.0.0"
|
|
27
30
|
PROG = sys.argv[0]
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
|
|
33
|
+
def info(msg: str):
|
|
30
34
|
print(msg, file=sys.stdout)
|
|
31
35
|
|
|
32
|
-
def warn(msg):
|
|
33
|
-
print('{}: warning: {}'.format(PROG, msg), file=sys.stderr)
|
|
34
36
|
|
|
35
|
-
def
|
|
36
|
-
print(
|
|
37
|
+
def warn(msg: str):
|
|
38
|
+
print("{}: warning: {}".format(PROG, msg), file=sys.stderr)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fatal(msg: str):
|
|
42
|
+
print("{}: error: {}".format(PROG, msg), file=sys.stderr)
|
|
37
43
|
exit(1)
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
|
|
46
|
+
def format_staged_files(
|
|
47
|
+
file_patterns: Sequence[str],
|
|
48
|
+
formatter: str,
|
|
49
|
+
update_working_tree: bool = True,
|
|
50
|
+
write: bool = True,
|
|
51
|
+
verbose: bool = False,
|
|
52
|
+
):
|
|
53
|
+
common_opts = [
|
|
54
|
+
"--cached",
|
|
55
|
+
"--diff-filter=AM", # select only file additions and modifications
|
|
56
|
+
"--no-renames",
|
|
57
|
+
"HEAD",
|
|
58
|
+
]
|
|
59
|
+
|
|
40
60
|
try:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
staged_files = {
|
|
62
|
+
path.decode("utf-8")
|
|
63
|
+
for path in subprocess.check_output(
|
|
64
|
+
["git", "diff", "--name-only"] + common_opts
|
|
65
|
+
).splitlines()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
output = subprocess.check_output(["git", "diff-index"] + common_opts)
|
|
48
69
|
for line in output.splitlines():
|
|
49
|
-
entry =
|
|
50
|
-
|
|
51
|
-
if entry['dst_mode'] == '120000':
|
|
70
|
+
entry = DiffIndexEntry(line.decode("utf-8"))
|
|
71
|
+
if entry.dst_mode == "120000":
|
|
52
72
|
# Do not process symlinks
|
|
53
73
|
continue
|
|
54
|
-
if not (matches_some_path(file_patterns,
|
|
74
|
+
if not (matches_some_path(file_patterns, entry.src_path)):
|
|
55
75
|
continue
|
|
56
|
-
if
|
|
57
|
-
|
|
76
|
+
if (
|
|
77
|
+
entry.src_mode is None
|
|
78
|
+
and (not entry.dst_hash or object_is_empty(entry.dst_hash))
|
|
79
|
+
and entry.src_path not in staged_files
|
|
80
|
+
):
|
|
81
|
+
# File is not staged, it's tracked only with `--intent-to-add` and won't get committed
|
|
82
|
+
continue
|
|
83
|
+
if format_file_in_index(
|
|
84
|
+
formatter,
|
|
85
|
+
entry,
|
|
86
|
+
update_working_tree=update_working_tree,
|
|
87
|
+
write=write,
|
|
88
|
+
verbose=verbose,
|
|
89
|
+
):
|
|
90
|
+
info("Reformatted {} with {}".format(entry.src_path, formatter))
|
|
58
91
|
except Exception as err:
|
|
59
92
|
fatal(str(err))
|
|
60
93
|
|
|
94
|
+
|
|
61
95
|
# Run formatter on file in the git index. Creates a new git object with the
|
|
62
96
|
# result, and replaces the content of the file in the index with that object.
|
|
63
97
|
# Returns hash of the new object if formatting produced any changes.
|
|
64
|
-
def format_file_in_index(
|
|
65
|
-
|
|
66
|
-
|
|
98
|
+
def format_file_in_index(
|
|
99
|
+
formatter: str,
|
|
100
|
+
diff_entry: "DiffIndexEntry",
|
|
101
|
+
update_working_tree: bool = True,
|
|
102
|
+
write: bool = True,
|
|
103
|
+
verbose: bool = False,
|
|
104
|
+
):
|
|
105
|
+
orig_hash = diff_entry.dst_hash
|
|
106
|
+
if not orig_hash:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
new_hash = format_object(formatter, orig_hash, diff_entry.src_path, verbose=verbose)
|
|
67
110
|
|
|
68
111
|
# If the new hash is the same then the formatter did not make any changes.
|
|
69
112
|
if not write or new_hash == orig_hash:
|
|
@@ -79,201 +122,238 @@ def format_file_in_index(formatter, diff_entry, update_working_tree=True, write=
|
|
|
79
122
|
|
|
80
123
|
if update_working_tree:
|
|
81
124
|
try:
|
|
82
|
-
patch_working_file(diff_entry
|
|
125
|
+
patch_working_file(diff_entry.src_path, orig_hash, new_hash)
|
|
83
126
|
except Exception as err:
|
|
84
127
|
# Errors patching working tree files are not fatal
|
|
85
128
|
warn(str(err))
|
|
86
129
|
|
|
87
130
|
return new_hash
|
|
88
131
|
|
|
89
|
-
|
|
132
|
+
|
|
133
|
+
# Match {}, and to avoid breaking quoting from shlex also match and remove surrounding quotes. This
|
|
134
|
+
# is important for backward compatibility because previous version of git-format-staged did not use
|
|
135
|
+
# shlex quoting, and required manual quoting.
|
|
136
|
+
file_path_placeholder = re.compile(r"(['\"]?)\{\}(\1)")
|
|
137
|
+
|
|
90
138
|
|
|
91
139
|
# Run formatter on a git blob identified by its hash. Writes output to a new git
|
|
92
140
|
# blob, and returns the hash of the new blob.
|
|
93
|
-
def format_object(
|
|
141
|
+
def format_object(
|
|
142
|
+
formatter: str, object_hash: str, file_path: str, verbose: bool = False
|
|
143
|
+
) -> str:
|
|
94
144
|
get_content = subprocess.Popen(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
command = re.sub(file_path_placeholder, file_path, formatter)
|
|
145
|
+
["git", "cat-file", "-p", object_hash], stdout=subprocess.PIPE
|
|
146
|
+
)
|
|
147
|
+
command = re.sub(file_path_placeholder, shlex.quote(file_path), formatter)
|
|
99
148
|
if verbose:
|
|
100
149
|
info(command)
|
|
101
150
|
format_content = subprocess.Popen(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
stdin=get_content.stdout,
|
|
105
|
-
stdout=subprocess.PIPE
|
|
106
|
-
)
|
|
151
|
+
command, shell=True, stdin=get_content.stdout, stdout=subprocess.PIPE
|
|
152
|
+
)
|
|
107
153
|
write_object = subprocess.Popen(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
154
|
+
["git", "hash-object", "-w", "--stdin"],
|
|
155
|
+
stdin=format_content.stdout,
|
|
156
|
+
stdout=subprocess.PIPE,
|
|
157
|
+
)
|
|
112
158
|
|
|
113
|
-
|
|
114
|
-
|
|
159
|
+
# Read output from the final process first to avoid deadlock if intermediate processes produce
|
|
160
|
+
# large outputs that would fill pipe buffers
|
|
161
|
+
new_hash, _err = write_object.communicate()
|
|
115
162
|
|
|
116
163
|
if get_content.wait() != 0:
|
|
117
|
-
raise ValueError(
|
|
164
|
+
raise ValueError(
|
|
165
|
+
"unable to read file content from object database: " + object_hash
|
|
166
|
+
)
|
|
118
167
|
|
|
119
168
|
if format_content.wait() != 0:
|
|
120
|
-
raise Exception(
|
|
121
|
-
|
|
122
|
-
new_hash, err = write_object.communicate()
|
|
169
|
+
raise Exception("formatter exited with non-zero status")
|
|
123
170
|
|
|
124
171
|
if write_object.returncode != 0:
|
|
125
|
-
raise Exception(
|
|
172
|
+
raise Exception("unable to write formatted content to object database")
|
|
173
|
+
|
|
174
|
+
return new_hash.decode("utf-8").rstrip()
|
|
126
175
|
|
|
127
|
-
return new_hash.decode('utf-8').rstrip()
|
|
128
176
|
|
|
129
|
-
def object_is_empty(object_hash):
|
|
177
|
+
def object_is_empty(object_hash: str) -> bool:
|
|
130
178
|
get_content = subprocess.Popen(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
content, err = get_content.communicate()
|
|
179
|
+
["git", "cat-file", "-p", object_hash], stdout=subprocess.PIPE
|
|
180
|
+
)
|
|
181
|
+
content, _err = get_content.communicate()
|
|
135
182
|
|
|
136
183
|
if get_content.returncode != 0:
|
|
137
|
-
raise Exception(
|
|
184
|
+
raise Exception("unable to verify content of formatted object")
|
|
138
185
|
|
|
139
186
|
return not content
|
|
140
187
|
|
|
141
|
-
def replace_file_in_index(diff_entry, new_object_hash):
|
|
142
|
-
subprocess.check_call(['git', 'update-index',
|
|
143
|
-
'--cacheinfo', '{},{},{}'.format(
|
|
144
|
-
diff_entry['dst_mode'],
|
|
145
|
-
new_object_hash,
|
|
146
|
-
diff_entry['src_path']
|
|
147
|
-
)])
|
|
148
188
|
|
|
149
|
-
def
|
|
189
|
+
def replace_file_in_index(diff_entry: "DiffIndexEntry", new_object_hash: str):
|
|
190
|
+
_ = subprocess.check_call(
|
|
191
|
+
[
|
|
192
|
+
"git",
|
|
193
|
+
"update-index",
|
|
194
|
+
"--cacheinfo",
|
|
195
|
+
"{},{},{}".format(
|
|
196
|
+
diff_entry.dst_mode, new_object_hash, diff_entry.src_path
|
|
197
|
+
),
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def patch_working_file(path: str, orig_object_hash: str, new_object_hash: str):
|
|
150
203
|
patch = subprocess.check_output(
|
|
151
|
-
|
|
152
|
-
|
|
204
|
+
[
|
|
205
|
+
"git",
|
|
206
|
+
"diff",
|
|
207
|
+
"--no-ext-diff",
|
|
208
|
+
"--color=never",
|
|
209
|
+
orig_object_hash,
|
|
210
|
+
new_object_hash,
|
|
211
|
+
]
|
|
212
|
+
)
|
|
153
213
|
|
|
154
214
|
# Substitute object hashes in patch header with path to working tree file
|
|
155
|
-
patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(
|
|
215
|
+
patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(
|
|
216
|
+
new_object_hash.encode(), path.encode()
|
|
217
|
+
)
|
|
156
218
|
|
|
157
219
|
apply_patch = subprocess.Popen(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
220
|
+
["git", "apply", "-"],
|
|
221
|
+
stdin=subprocess.PIPE,
|
|
222
|
+
stdout=subprocess.PIPE,
|
|
223
|
+
stderr=subprocess.PIPE,
|
|
224
|
+
)
|
|
163
225
|
|
|
164
|
-
|
|
226
|
+
_output, _err = apply_patch.communicate(input=patch_b)
|
|
165
227
|
|
|
166
228
|
if apply_patch.returncode != 0:
|
|
167
|
-
raise Exception(
|
|
229
|
+
raise Exception(
|
|
230
|
+
"could not apply formatting changes to working tree file {}".format(path)
|
|
231
|
+
)
|
|
232
|
+
|
|
168
233
|
|
|
169
234
|
# Format: src_mode dst_mode src_hash dst_hash status/score? src_path dst_path?
|
|
170
|
-
diff_pat = re.compile(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
235
|
+
diff_pat = re.compile(
|
|
236
|
+
r"^:(?P<src_mode>\d+) (?P<dst_mode>\d+) (?P<src_hash>[a-f0-9]+) (?P<dst_hash>[a-f0-9]+) (?P<status>[A-Z])(?P<score>\d+)?\t(?P<src_path>[^\t]+)(?:\t(?P<dst_path>[^\t]+))?$"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class DiffIndexEntry:
|
|
241
|
+
src_mode: str | None
|
|
242
|
+
dst_mode: str | None
|
|
243
|
+
src_hash: str | None
|
|
244
|
+
dst_hash: str | None
|
|
245
|
+
status: str
|
|
246
|
+
score: int | None
|
|
247
|
+
src_path: str
|
|
248
|
+
dst_path: str | None
|
|
249
|
+
|
|
250
|
+
def __init__(self, diff_index_output_line: str):
|
|
251
|
+
"Parse a line of output from `git diff-index`"
|
|
252
|
+
m = diff_pat.match(diff_index_output_line)
|
|
253
|
+
if not m:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
"Failed to parse diff-index line: " + diff_index_output_line
|
|
256
|
+
)
|
|
257
|
+
self.src_mode = unless_zeroed(m.group("src_mode"))
|
|
258
|
+
self.dst_mode = unless_zeroed(m.group("dst_mode"))
|
|
259
|
+
self.src_hash = unless_zeroed(m.group("src_hash"))
|
|
260
|
+
self.dst_hash = unless_zeroed(m.group("dst_hash"))
|
|
261
|
+
self.status = m.group("status")
|
|
262
|
+
self.score = int(m.group("score")) if m.group("score") else None
|
|
263
|
+
self.src_path = m.group("src_path")
|
|
264
|
+
self.dst_path = m.group("dst_path")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
zeroed_pat = re.compile(r"^0+$")
|
|
268
|
+
|
|
189
269
|
|
|
190
270
|
# Returns the argument unless the argument is a string of zeroes, in which case
|
|
191
271
|
# returns `None`
|
|
192
|
-
def unless_zeroed(s):
|
|
272
|
+
def unless_zeroed(s: str) -> str | None:
|
|
193
273
|
return s if not zeroed_pat.match(s) else None
|
|
194
274
|
|
|
195
|
-
def get_git_root():
|
|
196
|
-
return subprocess.check_output(
|
|
197
|
-
['git', 'rev-parse', '--show-toplevel']
|
|
198
|
-
).decode('utf-8').rstrip()
|
|
199
|
-
|
|
200
|
-
def normalize_path(p, relative_to=None):
|
|
201
|
-
return os.path.abspath(
|
|
202
|
-
os.path.join(relative_to, p) if relative_to else p
|
|
203
|
-
)
|
|
204
275
|
|
|
205
|
-
def matches_some_path(patterns, target):
|
|
276
|
+
def matches_some_path(patterns: Sequence[str], target: str) -> bool:
|
|
206
277
|
is_match = False
|
|
207
278
|
for signed_pattern in patterns:
|
|
208
279
|
(is_pattern_positive, pattern) = from_signed_pattern(signed_pattern)
|
|
209
|
-
if fnmatch(target,
|
|
280
|
+
if fnmatch(target, pattern):
|
|
210
281
|
is_match = is_pattern_positive
|
|
211
282
|
return is_match
|
|
212
283
|
|
|
284
|
+
|
|
213
285
|
# Checks for a '!' as the first character of a pattern, returns the rest of the
|
|
214
286
|
# pattern in a tuple. The tuple takes the form (is_pattern_positive, pattern).
|
|
215
287
|
# For example:
|
|
216
288
|
# from_signed_pattern('!pat') == (False, 'pat')
|
|
217
289
|
# from_signed_pattern('pat') == (True, 'pat')
|
|
218
|
-
def from_signed_pattern(pattern):
|
|
219
|
-
if pattern[0] ==
|
|
290
|
+
def from_signed_pattern(pattern: str) -> tuple[bool, str]:
|
|
291
|
+
if pattern[0] == "!":
|
|
220
292
|
return (False, pattern[1:])
|
|
221
293
|
else:
|
|
222
294
|
return (True, pattern)
|
|
223
295
|
|
|
296
|
+
|
|
224
297
|
class CustomArgumentParser(argparse.ArgumentParser):
|
|
225
|
-
def
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
298
|
+
def error( # pyright: ignore[reportImplicitOverride]
|
|
299
|
+
self, message: str
|
|
300
|
+
) -> NoReturn:
|
|
301
|
+
if message.startswith("unrecognized arguments:"):
|
|
302
|
+
message += " Do you need to quote your formatter command?"
|
|
303
|
+
super().error(message)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class Args(Protocol):
|
|
307
|
+
formatter: str
|
|
308
|
+
no_update_working_tree: bool
|
|
309
|
+
no_write: bool
|
|
310
|
+
verbose: bool
|
|
311
|
+
files: Sequence[str]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
235
315
|
parser = CustomArgumentParser(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
parser.add_argument(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
316
|
+
description="Transform staged files using a formatting command that accepts content via stdin and produces a result via stdout.",
|
|
317
|
+
epilog='Example: %(prog)s --formatter "prettier --stdin-filepath {}" "src/*.js" "test/*.js"',
|
|
318
|
+
)
|
|
319
|
+
_ = parser.add_argument(
|
|
320
|
+
"--formatter",
|
|
321
|
+
"-f",
|
|
322
|
+
required=True,
|
|
323
|
+
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 (with appropriate quoting). (Example: "prettier --stdin-filepath {}")',
|
|
324
|
+
)
|
|
325
|
+
_ = parser.add_argument(
|
|
326
|
+
"--no-update-working-tree",
|
|
327
|
+
action="store_true",
|
|
328
|
+
help="By default formatting changes made to staged file content will also be applied to working tree files via a patch. This option disables that behavior, leaving working tree files untouched.",
|
|
329
|
+
)
|
|
330
|
+
_ = parser.add_argument(
|
|
331
|
+
"--no-write",
|
|
332
|
+
action="store_true",
|
|
333
|
+
help='Prevents %(prog)s from modifying staged or working tree files. You can use this option to check staged changes with a linter instead of formatting. With this option stdout from the formatter command is ignored. Example: %(prog)s --no-write -f "eslint --stdin --stdin-filename {} >&2" "*.js"',
|
|
334
|
+
)
|
|
335
|
+
_ = parser.add_argument(
|
|
336
|
+
"--version",
|
|
337
|
+
action="version",
|
|
338
|
+
version="%(prog)s version {}".format(VERSION),
|
|
339
|
+
help="Display version of %(prog)s",
|
|
340
|
+
)
|
|
341
|
+
_ = parser.add_argument(
|
|
342
|
+
"--verbose",
|
|
343
|
+
help="Show the formatting commands that are running",
|
|
344
|
+
action="store_true",
|
|
345
|
+
)
|
|
346
|
+
_ = parser.add_argument(
|
|
347
|
+
"files",
|
|
348
|
+
nargs="+",
|
|
349
|
+
help='Patterns that specify files to format. The formatter will only transform staged files that are given here. Patterns may be literal file paths, or globs which will be tested against staged file paths using Python\'s fnmatch function. Patterns must be relative to the git repository root. For example "src/*.js" will match all files with a .js extension in src/ and its subdirectories. Patterns may be negated to exclude files using a "!" character. Patterns are evaluated left-to-right. (Example: "main.js" "src/*.js" "test/*.js" "!test/todo/*")',
|
|
350
|
+
)
|
|
351
|
+
args = cast(Args, parser.parse_args()) # pyright: ignore[reportInvalidCast]
|
|
352
|
+
files = args.files
|
|
272
353
|
format_staged_files(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
)
|
|
354
|
+
file_patterns=files,
|
|
355
|
+
formatter=args.formatter,
|
|
356
|
+
update_working_tree=not args.no_update_working_tree,
|
|
357
|
+
write=not args.no_write,
|
|
358
|
+
verbose=args.verbose,
|
|
359
|
+
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-format-staged",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Git command to transform staged files according to a command that accepts file content on stdin and produces output on stdout.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "ava",
|
|
@@ -29,17 +29,16 @@
|
|
|
29
29
|
"git-format-staged"
|
|
30
30
|
],
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@commitlint/cli": "^
|
|
33
|
-
"@commitlint/config-conventional": "^
|
|
32
|
+
"@commitlint/cli": "^19.0.3",
|
|
33
|
+
"@commitlint/config-conventional": "^19.0.3",
|
|
34
34
|
"@types/fs-extra": "^8.1.0",
|
|
35
35
|
"@types/tmp": "^0.1.0",
|
|
36
36
|
"ava": "^3.8.1",
|
|
37
37
|
"eslint": "^5.16.0",
|
|
38
38
|
"fs-extra": "^9.0.0",
|
|
39
|
-
"husky": "^4.2.5",
|
|
40
39
|
"micromatch": "^4.0.2",
|
|
41
40
|
"prettier-standard": "^9.1.1",
|
|
42
|
-
"semantic-release": "^
|
|
41
|
+
"semantic-release": "^23.0.2",
|
|
43
42
|
"strip-indent": "^3.0.0",
|
|
44
43
|
"tmp": "0.2.0",
|
|
45
44
|
"ts-node": "^8.9.1",
|
|
@@ -55,11 +54,5 @@
|
|
|
55
54
|
"files": [
|
|
56
55
|
"test/**/*_test.ts"
|
|
57
56
|
]
|
|
58
|
-
},
|
|
59
|
-
"husky": {
|
|
60
|
-
"hooks": {
|
|
61
|
-
"commit-msg": "commitlint -e $HUSKY_GIT_PARAMS",
|
|
62
|
-
"pre-commit": "./git-format-staged --formatter prettier-standard '*.js'"
|
|
63
|
-
}
|
|
64
57
|
}
|
|
65
58
|
}
|