git-format-staged 3.1.2 → 4.0.1
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 +40 -15
- package/git-format-staged +289 -162
- package/package.json +2 -8
package/README.md
CHANGED
|
@@ -128,30 +128,55 @@ option to tell it to read content from `stdin` instead of reading files from
|
|
|
128
128
|
disk, and messages from `eslint` are redirected to `stderr` (using the `>&2`
|
|
129
129
|
notation) so that you can see them.
|
|
130
130
|
|
|
131
|
-
### Set up a pre-commit hook
|
|
131
|
+
### Set up a pre-commit hook
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
a
|
|
133
|
+
A git pre-commit hook runs automatically whenever you make a commit, before your
|
|
134
|
+
editor opens to write a commit message. If formatting fails with a non-zero exit
|
|
135
|
+
status it will cancel the commit, requiring you to fix the problem before trying
|
|
136
|
+
again. (You can always skip a pre-commit hook by running `git commit
|
|
137
|
+
--no-verify`.) But in most cases you will have correctly formatted files at all
|
|
138
|
+
times, without having to think about it again after setup. Whenever a file is
|
|
139
|
+
changed as a result of formatting on commit you will see a message in the output
|
|
140
|
+
from `git commit`.
|
|
135
141
|
|
|
136
|
-
|
|
142
|
+
There are a number of options for setting up a git pre-commit hook depending on
|
|
143
|
+
the software ecosystem you are working with.
|
|
137
144
|
|
|
138
|
-
|
|
145
|
+
#### Write a pre-commit hook manually
|
|
139
146
|
|
|
140
|
-
|
|
147
|
+
You can write a hook manually by creating the file `.git/hooks/pre-commit`, and
|
|
148
|
+
making it executable. A hook set up this way will _not_ automatically propagate
|
|
149
|
+
to other clones, so every collaborator will have to do this themselves.
|
|
141
150
|
|
|
142
|
-
|
|
143
|
-
$ npm run prepare
|
|
151
|
+
#### Nix devShell
|
|
144
152
|
|
|
145
|
-
|
|
153
|
+
If you use a Nix devShell you can use [nix-git-hooks][] to help set up
|
|
154
|
+
a pre-commit hook automatically. You can either automatically install hooks for
|
|
155
|
+
everyone who uses the devShell, or provide a command that collaborators can run
|
|
156
|
+
to easily opt in to hooks. Take a look at how this repo uses nix-git-hooks in
|
|
157
|
+
[flake.nix](./flake.nix).
|
|
146
158
|
|
|
147
|
-
|
|
148
|
-
$ git add .husky/pre-commit
|
|
159
|
+
[nix-git-hooks]: https://github.com/ysndr/nix-git-hooks
|
|
149
160
|
|
|
150
|
-
|
|
151
|
-
patterns are quoted!
|
|
161
|
+
#### Npm, yarn, pnpm, or bun
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
[Husky][] will automatically install git hooks for all project collaborators
|
|
164
|
+
after running package manager commands.
|
|
165
|
+
|
|
166
|
+
[Husky]: https://typicode.github.io/husky/get-started.html
|
|
167
|
+
|
|
168
|
+
#### Rust with Cargo
|
|
169
|
+
|
|
170
|
+
[cargo-husky][] is like Husky, but for Rust projects.
|
|
171
|
+
|
|
172
|
+
[cargo-husky]: https://crates.io/crates/cargo-husky
|
|
173
|
+
|
|
174
|
+
#### and more!
|
|
175
|
+
|
|
176
|
+
See more hook management options in [awesome-git][] and [awesome-git-hooks][].
|
|
177
|
+
|
|
178
|
+
[awesome-git]: https://github.com/dictcp/awesome-git?tab=readme-ov-file#hook-management.
|
|
179
|
+
[awesome-git-hooks]: https://github.com/compscilauren/awesome-git-hooks?tab=readme-ov-file#tools
|
|
155
180
|
|
|
156
181
|
## Comparisons to similar utilities
|
|
157
182
|
|
package/git-format-staged
CHANGED
|
@@ -13,67 +13,105 @@
|
|
|
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
22
|
import shlex
|
|
23
|
+
import signal
|
|
23
24
|
import subprocess
|
|
24
25
|
import sys
|
|
26
|
+
from typing import NoReturn, Protocol, cast
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
|
|
29
|
+
# The string 4.0.1 is replaced during the publish process.
|
|
30
|
+
VERSION = "4.0.1"
|
|
28
31
|
PROG = sys.argv[0]
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
|
|
34
|
+
def info(msg: str):
|
|
31
35
|
print(msg, file=sys.stdout)
|
|
32
36
|
|
|
33
|
-
def warn(msg):
|
|
34
|
-
print('{}: warning: {}'.format(PROG, msg), file=sys.stderr)
|
|
35
37
|
|
|
36
|
-
def
|
|
37
|
-
print(
|
|
38
|
+
def info_stderr(msg: str):
|
|
39
|
+
print(msg, file=sys.stderr)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def warn(msg: str):
|
|
43
|
+
print("{}: warning: {}".format(PROG, msg), file=sys.stderr)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def fatal(msg: str):
|
|
47
|
+
print("{}: error: {}".format(PROG, msg), file=sys.stderr)
|
|
38
48
|
exit(1)
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
|
|
51
|
+
def format_staged_files(
|
|
52
|
+
file_patterns: Sequence[str],
|
|
53
|
+
formatter: str,
|
|
54
|
+
update_working_tree: bool = True,
|
|
55
|
+
write: bool = True,
|
|
56
|
+
verbose: bool = False,
|
|
57
|
+
):
|
|
41
58
|
common_opts = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
"--cached",
|
|
60
|
+
"--diff-filter=AM", # select only file additions and modifications
|
|
61
|
+
"--no-renames",
|
|
62
|
+
"HEAD",
|
|
46
63
|
]
|
|
47
64
|
|
|
48
65
|
try:
|
|
49
66
|
staged_files = {
|
|
50
|
-
path.decode(
|
|
51
|
-
for path in subprocess.check_output(
|
|
67
|
+
path.decode("utf-8")
|
|
68
|
+
for path in subprocess.check_output(
|
|
69
|
+
["git", "diff", "--name-only"] + common_opts
|
|
70
|
+
).splitlines()
|
|
52
71
|
}
|
|
53
72
|
|
|
54
|
-
output = subprocess.check_output([
|
|
73
|
+
output = subprocess.check_output(["git", "diff-index"] + common_opts)
|
|
55
74
|
for line in output.splitlines():
|
|
56
|
-
entry =
|
|
57
|
-
|
|
58
|
-
if entry['dst_mode'] == '120000':
|
|
75
|
+
entry = DiffIndexEntry(line.decode("utf-8"))
|
|
76
|
+
if entry.dst_mode == "120000":
|
|
59
77
|
# Do not process symlinks
|
|
60
78
|
continue
|
|
61
|
-
if not (matches_some_path(file_patterns,
|
|
79
|
+
if not (matches_some_path(file_patterns, entry.src_path)):
|
|
62
80
|
continue
|
|
63
|
-
if
|
|
81
|
+
if (
|
|
82
|
+
entry.src_mode is None
|
|
83
|
+
and (not entry.dst_hash or object_is_empty(entry.dst_hash))
|
|
84
|
+
and entry.src_path not in staged_files
|
|
85
|
+
):
|
|
64
86
|
# File is not staged, it's tracked only with `--intent-to-add` and won't get committed
|
|
65
87
|
continue
|
|
66
|
-
if format_file_in_index(
|
|
67
|
-
|
|
88
|
+
if format_file_in_index(
|
|
89
|
+
formatter,
|
|
90
|
+
entry,
|
|
91
|
+
update_working_tree=update_working_tree,
|
|
92
|
+
write=write,
|
|
93
|
+
verbose=verbose,
|
|
94
|
+
):
|
|
95
|
+
info("Reformatted {} with {}".format(entry.src_path, formatter))
|
|
68
96
|
except Exception as err:
|
|
69
97
|
fatal(str(err))
|
|
70
98
|
|
|
99
|
+
|
|
71
100
|
# Run formatter on file in the git index. Creates a new git object with the
|
|
72
101
|
# result, and replaces the content of the file in the index with that object.
|
|
73
102
|
# Returns hash of the new object if formatting produced any changes.
|
|
74
|
-
def format_file_in_index(
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
def format_file_in_index(
|
|
104
|
+
formatter: str,
|
|
105
|
+
diff_entry: "DiffIndexEntry",
|
|
106
|
+
update_working_tree: bool = True,
|
|
107
|
+
write: bool = True,
|
|
108
|
+
verbose: bool = False,
|
|
109
|
+
):
|
|
110
|
+
orig_hash = diff_entry.dst_hash
|
|
111
|
+
if not orig_hash:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
new_hash = format_object(formatter, orig_hash, diff_entry.src_path, verbose=verbose)
|
|
77
115
|
|
|
78
116
|
# If the new hash is the same then the formatter did not make any changes.
|
|
79
117
|
if not write or new_hash == orig_hash:
|
|
@@ -89,203 +127,292 @@ def format_file_in_index(formatter, diff_entry, update_working_tree=True, write=
|
|
|
89
127
|
|
|
90
128
|
if update_working_tree:
|
|
91
129
|
try:
|
|
92
|
-
patch_working_file(diff_entry
|
|
130
|
+
patch_working_file(diff_entry.src_path, orig_hash, new_hash)
|
|
93
131
|
except Exception as err:
|
|
94
132
|
# Errors patching working tree files are not fatal
|
|
95
133
|
warn(str(err))
|
|
96
134
|
|
|
97
135
|
return new_hash
|
|
98
136
|
|
|
137
|
+
|
|
99
138
|
# Match {}, and to avoid breaking quoting from shlex also match and remove surrounding quotes. This
|
|
100
139
|
# is important for backward compatibility because previous version of git-format-staged did not use
|
|
101
140
|
# shlex quoting, and required manual quoting.
|
|
102
141
|
file_path_placeholder = re.compile(r"(['\"]?)\{\}(\1)")
|
|
103
142
|
|
|
143
|
+
|
|
104
144
|
# Run formatter on a git blob identified by its hash. Writes output to a new git
|
|
105
145
|
# blob, and returns the hash of the new blob.
|
|
106
|
-
def format_object(
|
|
146
|
+
def format_object(
|
|
147
|
+
formatter: str, object_hash: str, file_path: str, verbose: bool = False
|
|
148
|
+
) -> str:
|
|
107
149
|
get_content = subprocess.Popen(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
150
|
+
["git", "cat-file", "-p", object_hash], stdout=subprocess.PIPE
|
|
151
|
+
)
|
|
152
|
+
|
|
111
153
|
command = re.sub(file_path_placeholder, shlex.quote(file_path), formatter)
|
|
112
154
|
if verbose:
|
|
113
|
-
|
|
155
|
+
info_stderr(command)
|
|
114
156
|
format_content = subprocess.Popen(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
stdin=get_content.stdout,
|
|
118
|
-
stdout=subprocess.PIPE
|
|
119
|
-
)
|
|
120
|
-
write_object = subprocess.Popen(
|
|
121
|
-
['git', 'hash-object', '-w', '--stdin'],
|
|
122
|
-
stdin=format_content.stdout,
|
|
123
|
-
stdout=subprocess.PIPE
|
|
124
|
-
)
|
|
157
|
+
command, shell=True, stdin=get_content.stdout, stdout=subprocess.PIPE
|
|
158
|
+
)
|
|
125
159
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
160
|
+
write_object = subprocess.Popen(
|
|
161
|
+
["git", "hash-object", "-w", "--stdin"],
|
|
162
|
+
stdin=format_content.stdout,
|
|
163
|
+
stdout=subprocess.PIPE,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Close the parent process reference to stdout, leaving only references in the child processes.
|
|
167
|
+
# This way if the downstream process terminates while format_content is still running,
|
|
168
|
+
# format_content will be terminated with a SIGPIPE signal.
|
|
169
|
+
format_content.stdout.close() # pyright: ignore[reportOptionalMemberAccess]
|
|
170
|
+
|
|
171
|
+
# On the other hand we don't close get_content.stdout() so that we can check if there is unread
|
|
172
|
+
# data left after the formatter has finished.
|
|
173
|
+
|
|
174
|
+
# Read output from the last process in the pipe, and block until that process has completed.
|
|
175
|
+
# It's important to block on the last process completing before waiting for the other sub
|
|
176
|
+
# processes to finish.
|
|
177
|
+
new_hash, _err = write_object.communicate()
|
|
178
|
+
|
|
179
|
+
# The first two pipe processes should have completed by now. Block to verify that we have exit
|
|
180
|
+
# statuses from them.
|
|
181
|
+
try:
|
|
182
|
+
# Use communicate() to check for any unread output from get_content.
|
|
183
|
+
get_content_unread_stdout, _ = get_content.communicate(timeout=5)
|
|
184
|
+
get_content.stdout.close() # pyright: ignore[reportOptionalMemberAccess]
|
|
185
|
+
format_content_exit_status = format_content.wait(timeout=5)
|
|
186
|
+
except subprocess.TimeoutExpired as exception:
|
|
187
|
+
raise Exception(
|
|
188
|
+
"the formatter command did not terminate as expected"
|
|
189
|
+
) from exception
|
|
190
|
+
|
|
191
|
+
# An error from format_content is most relevant to the user, so prioritize displaying this error
|
|
192
|
+
# message in case multiple things went wrong.
|
|
193
|
+
if format_content_exit_status != 0:
|
|
194
|
+
raise Exception(
|
|
195
|
+
f"formatter exited with non-zero status ({format_content_exit_status}) while processing {file_path}"
|
|
196
|
+
)
|
|
129
197
|
|
|
130
|
-
|
|
131
|
-
|
|
198
|
+
# If the formatter exited before reading all input then get_content might have been terminated
|
|
199
|
+
# by a SIGPIPE signal. This is probably incorrect behavior from the formatter command, but is
|
|
200
|
+
# allowed by design (for now). So we emit a warning, but will not fail.
|
|
201
|
+
|
|
202
|
+
# If there was unread output from get_content that's an indication that the formatter command is
|
|
203
|
+
# probably not configured correctly. But program design allows the formatter command do do what
|
|
204
|
+
# it wants. So this is a warning, not a hard error.
|
|
205
|
+
#
|
|
206
|
+
# A SIGPIPE termination to get_content would also indicate unread output. This should not
|
|
207
|
+
# happen, but it doesn't hurt to check.
|
|
208
|
+
if len(get_content_unread_stdout) > 0 or get_content.returncode == -signal.SIGPIPE:
|
|
209
|
+
warn(
|
|
210
|
+
f"the formatter command exited before reading all content from {file_path}"
|
|
211
|
+
)
|
|
132
212
|
|
|
133
|
-
if
|
|
134
|
-
|
|
213
|
+
if get_content.returncode != 0 and get_content.returncode != -signal.SIGPIPE:
|
|
214
|
+
if verbose:
|
|
215
|
+
info_stderr(
|
|
216
|
+
f"non-zero exit status from `git cat-file -p {object_hash}`\n"
|
|
217
|
+
+ f"exit status: {get_content.returncode}\n"
|
|
218
|
+
+ f"file path: {file_path}\n"
|
|
219
|
+
)
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"unable to read file content for {file_path} from object database."
|
|
222
|
+
)
|
|
135
223
|
|
|
136
224
|
if write_object.returncode != 0:
|
|
137
|
-
|
|
225
|
+
if verbose:
|
|
226
|
+
info_stderr(
|
|
227
|
+
f"non-zero exit status from `git hash-object -w --stdin`\n"
|
|
228
|
+
+ f"exit status: {write_object.returncode}\n"
|
|
229
|
+
+ f"file path: {file_path}\n"
|
|
230
|
+
)
|
|
231
|
+
raise Exception("unable to write formatted content to object database")
|
|
138
232
|
|
|
139
|
-
return new_hash.decode(
|
|
233
|
+
return new_hash.decode("utf-8").rstrip()
|
|
140
234
|
|
|
141
|
-
|
|
235
|
+
|
|
236
|
+
def object_is_empty(object_hash: str) -> bool:
|
|
142
237
|
get_content = subprocess.Popen(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
content, err = get_content.communicate()
|
|
238
|
+
["git", "cat-file", "-p", object_hash], stdout=subprocess.PIPE
|
|
239
|
+
)
|
|
240
|
+
content, _err = get_content.communicate()
|
|
147
241
|
|
|
148
242
|
if get_content.returncode != 0:
|
|
149
|
-
raise Exception(
|
|
243
|
+
raise Exception("unable to verify content of formatted object")
|
|
150
244
|
|
|
151
245
|
return not content
|
|
152
246
|
|
|
153
|
-
def replace_file_in_index(diff_entry, new_object_hash):
|
|
154
|
-
subprocess.check_call(['git', 'update-index',
|
|
155
|
-
'--cacheinfo', '{},{},{}'.format(
|
|
156
|
-
diff_entry['dst_mode'],
|
|
157
|
-
new_object_hash,
|
|
158
|
-
diff_entry['src_path']
|
|
159
|
-
)])
|
|
160
247
|
|
|
161
|
-
def
|
|
248
|
+
def replace_file_in_index(diff_entry: "DiffIndexEntry", new_object_hash: str):
|
|
249
|
+
_ = subprocess.check_call(
|
|
250
|
+
[
|
|
251
|
+
"git",
|
|
252
|
+
"update-index",
|
|
253
|
+
"--cacheinfo",
|
|
254
|
+
"{},{},{}".format(
|
|
255
|
+
diff_entry.dst_mode, new_object_hash, diff_entry.src_path
|
|
256
|
+
),
|
|
257
|
+
]
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def patch_working_file(path: str, orig_object_hash: str, new_object_hash: str):
|
|
162
262
|
patch = subprocess.check_output(
|
|
163
|
-
|
|
164
|
-
|
|
263
|
+
[
|
|
264
|
+
"git",
|
|
265
|
+
"diff",
|
|
266
|
+
"--no-ext-diff",
|
|
267
|
+
"--color=never",
|
|
268
|
+
orig_object_hash,
|
|
269
|
+
new_object_hash,
|
|
270
|
+
]
|
|
271
|
+
)
|
|
165
272
|
|
|
166
273
|
# Substitute object hashes in patch header with path to working tree file
|
|
167
|
-
patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(
|
|
274
|
+
patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(
|
|
275
|
+
new_object_hash.encode(), path.encode()
|
|
276
|
+
)
|
|
168
277
|
|
|
169
278
|
apply_patch = subprocess.Popen(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
279
|
+
["git", "apply", "-"],
|
|
280
|
+
stdin=subprocess.PIPE,
|
|
281
|
+
stdout=subprocess.PIPE,
|
|
282
|
+
stderr=subprocess.PIPE,
|
|
283
|
+
)
|
|
175
284
|
|
|
176
|
-
|
|
285
|
+
_output, _err = apply_patch.communicate(input=patch_b)
|
|
177
286
|
|
|
178
287
|
if apply_patch.returncode != 0:
|
|
179
|
-
raise Exception(
|
|
288
|
+
raise Exception(
|
|
289
|
+
"could not apply formatting changes to working tree file {}".format(path)
|
|
290
|
+
)
|
|
291
|
+
|
|
180
292
|
|
|
181
293
|
# Format: src_mode dst_mode src_hash dst_hash status/score? src_path dst_path?
|
|
182
|
-
diff_pat = re.compile(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
294
|
+
diff_pat = re.compile(
|
|
295
|
+
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]+))?$"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class DiffIndexEntry:
|
|
300
|
+
src_mode: str | None
|
|
301
|
+
dst_mode: str | None
|
|
302
|
+
src_hash: str | None
|
|
303
|
+
dst_hash: str | None
|
|
304
|
+
status: str
|
|
305
|
+
score: int | None
|
|
306
|
+
src_path: str
|
|
307
|
+
dst_path: str | None
|
|
308
|
+
|
|
309
|
+
def __init__(self, diff_index_output_line: str):
|
|
310
|
+
"Parse a line of output from `git diff-index`"
|
|
311
|
+
m = diff_pat.match(diff_index_output_line)
|
|
312
|
+
if not m:
|
|
313
|
+
raise ValueError(
|
|
314
|
+
"Failed to parse diff-index line: " + diff_index_output_line
|
|
315
|
+
)
|
|
316
|
+
self.src_mode = unless_zeroed(m.group("src_mode"))
|
|
317
|
+
self.dst_mode = unless_zeroed(m.group("dst_mode"))
|
|
318
|
+
self.src_hash = unless_zeroed(m.group("src_hash"))
|
|
319
|
+
self.dst_hash = unless_zeroed(m.group("dst_hash"))
|
|
320
|
+
self.status = m.group("status")
|
|
321
|
+
self.score = int(m.group("score")) if m.group("score") else None
|
|
322
|
+
self.src_path = m.group("src_path")
|
|
323
|
+
self.dst_path = m.group("dst_path")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
zeroed_pat = re.compile(r"^0+$")
|
|
327
|
+
|
|
201
328
|
|
|
202
329
|
# Returns the argument unless the argument is a string of zeroes, in which case
|
|
203
330
|
# returns `None`
|
|
204
|
-
def unless_zeroed(s):
|
|
331
|
+
def unless_zeroed(s: str) -> str | None:
|
|
205
332
|
return s if not zeroed_pat.match(s) else None
|
|
206
333
|
|
|
207
|
-
def get_git_root():
|
|
208
|
-
return subprocess.check_output(
|
|
209
|
-
['git', 'rev-parse', '--show-toplevel']
|
|
210
|
-
).decode('utf-8').rstrip()
|
|
211
334
|
|
|
212
|
-
def
|
|
213
|
-
return os.path.abspath(
|
|
214
|
-
os.path.join(relative_to, p) if relative_to else p
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
def matches_some_path(patterns, target):
|
|
335
|
+
def matches_some_path(patterns: Sequence[str], target: str) -> bool:
|
|
218
336
|
is_match = False
|
|
219
337
|
for signed_pattern in patterns:
|
|
220
338
|
(is_pattern_positive, pattern) = from_signed_pattern(signed_pattern)
|
|
221
|
-
if fnmatch(target,
|
|
339
|
+
if fnmatch(target, pattern):
|
|
222
340
|
is_match = is_pattern_positive
|
|
223
341
|
return is_match
|
|
224
342
|
|
|
343
|
+
|
|
225
344
|
# Checks for a '!' as the first character of a pattern, returns the rest of the
|
|
226
345
|
# pattern in a tuple. The tuple takes the form (is_pattern_positive, pattern).
|
|
227
346
|
# For example:
|
|
228
347
|
# from_signed_pattern('!pat') == (False, 'pat')
|
|
229
348
|
# from_signed_pattern('pat') == (True, 'pat')
|
|
230
|
-
def from_signed_pattern(pattern):
|
|
231
|
-
if pattern[0] ==
|
|
349
|
+
def from_signed_pattern(pattern: str) -> tuple[bool, str]:
|
|
350
|
+
if pattern[0] == "!":
|
|
232
351
|
return (False, pattern[1:])
|
|
233
352
|
else:
|
|
234
353
|
return (True, pattern)
|
|
235
354
|
|
|
355
|
+
|
|
236
356
|
class CustomArgumentParser(argparse.ArgumentParser):
|
|
237
|
-
def
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
357
|
+
def error( # pyright: ignore[reportImplicitOverride]
|
|
358
|
+
self, message: str
|
|
359
|
+
) -> NoReturn:
|
|
360
|
+
if message.startswith("unrecognized arguments:"):
|
|
361
|
+
message += " Do you need to quote your formatter command?"
|
|
362
|
+
super().error(message)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class Args(Protocol):
|
|
366
|
+
formatter: str
|
|
367
|
+
no_update_working_tree: bool
|
|
368
|
+
no_write: bool
|
|
369
|
+
verbose: bool
|
|
370
|
+
files: Sequence[str]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == "__main__":
|
|
247
374
|
parser = CustomArgumentParser(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
parser.add_argument(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
375
|
+
description="Transform staged files using a formatting command that accepts content via stdin and produces a result via stdout.",
|
|
376
|
+
epilog='Example: %(prog)s --formatter "prettier --stdin-filepath {}" "src/*.js" "test/*.js"',
|
|
377
|
+
)
|
|
378
|
+
_ = parser.add_argument(
|
|
379
|
+
"--formatter",
|
|
380
|
+
"-f",
|
|
381
|
+
required=True,
|
|
382
|
+
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 {}")',
|
|
383
|
+
)
|
|
384
|
+
_ = parser.add_argument(
|
|
385
|
+
"--no-update-working-tree",
|
|
386
|
+
action="store_true",
|
|
387
|
+
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.",
|
|
388
|
+
)
|
|
389
|
+
_ = parser.add_argument(
|
|
390
|
+
"--no-write",
|
|
391
|
+
action="store_true",
|
|
392
|
+
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"',
|
|
393
|
+
)
|
|
394
|
+
_ = parser.add_argument(
|
|
395
|
+
"--version",
|
|
396
|
+
action="version",
|
|
397
|
+
version="%(prog)s version {}".format(VERSION),
|
|
398
|
+
help="Display version of %(prog)s",
|
|
399
|
+
)
|
|
400
|
+
_ = parser.add_argument(
|
|
401
|
+
"--verbose",
|
|
402
|
+
help="Show the formatting commands that are running",
|
|
403
|
+
action="store_true",
|
|
404
|
+
)
|
|
405
|
+
_ = parser.add_argument(
|
|
406
|
+
"files",
|
|
407
|
+
nargs="+",
|
|
408
|
+
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/*")',
|
|
409
|
+
)
|
|
410
|
+
args = cast(Args, parser.parse_args()) # pyright: ignore[reportInvalidCast]
|
|
411
|
+
files = args.files
|
|
284
412
|
format_staged_files(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
)
|
|
413
|
+
file_patterns=files,
|
|
414
|
+
formatter=args.formatter,
|
|
415
|
+
update_working_tree=not args.no_update_working_tree,
|
|
416
|
+
write=not args.no_write,
|
|
417
|
+
verbose=args.verbose,
|
|
418
|
+
)
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-format-staged",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
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",
|
|
7
|
+
"prepare": "update-npm-deps-hash || true",
|
|
7
8
|
"prepublishOnly": "sed -i \"s/\\$VERSION/$npm_package_version/\" git-format-staged",
|
|
8
9
|
"semantic-release": "semantic-release"
|
|
9
10
|
},
|
|
@@ -36,7 +37,6 @@
|
|
|
36
37
|
"ava": "^3.8.1",
|
|
37
38
|
"eslint": "^5.16.0",
|
|
38
39
|
"fs-extra": "^9.0.0",
|
|
39
|
-
"husky": "^9.0.11",
|
|
40
40
|
"micromatch": "^4.0.2",
|
|
41
41
|
"prettier-standard": "^9.1.1",
|
|
42
42
|
"semantic-release": "^23.0.2",
|
|
@@ -55,11 +55,5 @@
|
|
|
55
55
|
"files": [
|
|
56
56
|
"test/**/*_test.ts"
|
|
57
57
|
]
|
|
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
58
|
}
|
|
65
59
|
}
|