git-format-staged 3.1.1 → 3.1.3
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 +258 -129
- package/package.json +5 -5
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,9 +7,9 @@
|
|
|
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
|
|
|
@@ -19,51 +19,94 @@ from fnmatch import fnmatch
|
|
|
19
19
|
from gettext import gettext as _
|
|
20
20
|
import os
|
|
21
21
|
import re
|
|
22
|
+
import shlex
|
|
23
|
+
import signal
|
|
22
24
|
import subprocess
|
|
23
25
|
import sys
|
|
24
26
|
|
|
25
|
-
# The string 3.1.
|
|
26
|
-
VERSION =
|
|
27
|
+
# The string 3.1.3 is replaced during the publish process.
|
|
28
|
+
VERSION = "3.1.3"
|
|
27
29
|
PROG = sys.argv[0]
|
|
28
30
|
|
|
31
|
+
|
|
29
32
|
def info(msg):
|
|
30
33
|
print(msg, file=sys.stdout)
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
|
|
36
|
+
def info_stderr(msg: str):
|
|
37
|
+
print(msg, file=sys.stderr)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def warn(msg: str):
|
|
41
|
+
print("{}: warning: {}".format(PROG, msg), file=sys.stderr)
|
|
42
|
+
|
|
34
43
|
|
|
35
44
|
def fatal(msg):
|
|
36
|
-
print(
|
|
45
|
+
print("{}: error: {}".format(PROG, msg), file=sys.stderr)
|
|
37
46
|
exit(1)
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
|
|
49
|
+
def format_staged_files(
|
|
50
|
+
file_patterns,
|
|
51
|
+
formatter,
|
|
52
|
+
git_root,
|
|
53
|
+
update_working_tree=True,
|
|
54
|
+
write=True,
|
|
55
|
+
verbose=False,
|
|
56
|
+
):
|
|
57
|
+
common_opts = [
|
|
58
|
+
"--cached",
|
|
59
|
+
"--diff-filter=AM", # select only file additions and modifications
|
|
60
|
+
"--no-renames",
|
|
61
|
+
"HEAD",
|
|
62
|
+
]
|
|
63
|
+
|
|
40
64
|
try:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
staged_files = {
|
|
66
|
+
path.decode("utf-8")
|
|
67
|
+
for path in subprocess.check_output(
|
|
68
|
+
["git", "diff", "--name-only"] + common_opts
|
|
69
|
+
).splitlines()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
output = subprocess.check_output(["git", "diff-index"] + common_opts)
|
|
48
73
|
for line in output.splitlines():
|
|
49
|
-
entry = parse_diff(line.decode(
|
|
50
|
-
entry_path = normalize_path(entry[
|
|
51
|
-
if entry[
|
|
74
|
+
entry = parse_diff(line.decode("utf-8"))
|
|
75
|
+
entry_path = normalize_path(entry["src_path"], relative_to=git_root)
|
|
76
|
+
if entry["dst_mode"] == "120000":
|
|
52
77
|
# Do not process symlinks
|
|
53
78
|
continue
|
|
54
79
|
if not (matches_some_path(file_patterns, entry_path)):
|
|
55
80
|
continue
|
|
56
|
-
if
|
|
57
|
-
|
|
81
|
+
if (
|
|
82
|
+
entry["src_mode"] is None
|
|
83
|
+
and object_is_empty(entry["dst_hash"])
|
|
84
|
+
and entry["src_path"] not in staged_files
|
|
85
|
+
):
|
|
86
|
+
# File is not staged, it's tracked only with `--intent-to-add` and won't get committed
|
|
87
|
+
continue
|
|
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))
|
|
58
96
|
except Exception as err:
|
|
59
97
|
fatal(str(err))
|
|
60
98
|
|
|
99
|
+
|
|
61
100
|
# Run formatter on file in the git index. Creates a new git object with the
|
|
62
101
|
# result, and replaces the content of the file in the index with that object.
|
|
63
102
|
# Returns hash of the new object if formatting produced any changes.
|
|
64
|
-
def format_file_in_index(
|
|
65
|
-
|
|
66
|
-
|
|
103
|
+
def format_file_in_index(
|
|
104
|
+
formatter, diff_entry, update_working_tree=True, write=True, verbose=False
|
|
105
|
+
):
|
|
106
|
+
orig_hash = diff_entry["dst_hash"]
|
|
107
|
+
new_hash = format_object(
|
|
108
|
+
formatter, orig_hash, diff_entry["src_path"], verbose=verbose
|
|
109
|
+
)
|
|
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,128 +122,210 @@ 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
141
|
def format_object(formatter, object_hash, file_path, verbose=False):
|
|
94
142
|
get_content = subprocess.Popen(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
command = re.sub(file_path_placeholder, file_path, formatter)
|
|
143
|
+
["git", "cat-file", "-p", object_hash], stdout=subprocess.PIPE
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
command = re.sub(file_path_placeholder, shlex.quote(file_path), formatter)
|
|
99
147
|
if verbose:
|
|
100
|
-
|
|
148
|
+
info_stderr(command)
|
|
101
149
|
format_content = subprocess.Popen(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
stdin=get_content.stdout,
|
|
105
|
-
stdout=subprocess.PIPE
|
|
106
|
-
)
|
|
107
|
-
write_object = subprocess.Popen(
|
|
108
|
-
['git', 'hash-object', '-w', '--stdin'],
|
|
109
|
-
stdin=format_content.stdout,
|
|
110
|
-
stdout=subprocess.PIPE
|
|
111
|
-
)
|
|
150
|
+
command, shell=True, stdin=get_content.stdout, stdout=subprocess.PIPE
|
|
151
|
+
)
|
|
112
152
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
153
|
+
write_object = subprocess.Popen(
|
|
154
|
+
["git", "hash-object", "-w", "--stdin"],
|
|
155
|
+
stdin=format_content.stdout,
|
|
156
|
+
stdout=subprocess.PIPE,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Close the parent process reference to stdout, leaving only references in the child processes.
|
|
160
|
+
# This way if the downstream process terminates while format_content is still running,
|
|
161
|
+
# format_content will be terminated with a SIGPIPE signal.
|
|
162
|
+
format_content.stdout.close() # pyright: ignore[reportOptionalMemberAccess]
|
|
163
|
+
|
|
164
|
+
# On the other hand we don't close get_content.stdout() so that we can check if there is unread
|
|
165
|
+
# data left after the formatter has finished.
|
|
166
|
+
|
|
167
|
+
# Read output from the last process in the pipe, and block until that process has completed.
|
|
168
|
+
# It's important to block on the last process completing before waiting for the other sub
|
|
169
|
+
# processes to finish.
|
|
170
|
+
new_hash, _err = write_object.communicate()
|
|
171
|
+
|
|
172
|
+
# The first two pipe processes should have completed by now. Block to verify that we have exit
|
|
173
|
+
# statuses from them.
|
|
174
|
+
try:
|
|
175
|
+
# Use communicate() to check for any unread output from get_content.
|
|
176
|
+
get_content_unread_stdout, _ = get_content.communicate(timeout=5)
|
|
177
|
+
get_content.stdout.close() # pyright: ignore[reportOptionalMemberAccess]
|
|
178
|
+
format_content_exit_status = format_content.wait(timeout=5)
|
|
179
|
+
except subprocess.TimeoutExpired as exception:
|
|
180
|
+
raise Exception(
|
|
181
|
+
"the formatter command did not terminate as expected"
|
|
182
|
+
) from exception
|
|
183
|
+
|
|
184
|
+
# An error from format_content is most relevant to the user, so prioritize displaying this error
|
|
185
|
+
# message in case multiple things went wrong.
|
|
186
|
+
if format_content_exit_status != 0:
|
|
187
|
+
raise Exception(
|
|
188
|
+
f"formatter exited with non-zero status ({format_content_exit_status}) while processing {file_path}"
|
|
189
|
+
)
|
|
118
190
|
|
|
119
|
-
|
|
120
|
-
|
|
191
|
+
# If the formatter exited before reading all input then get_content might have been terminated
|
|
192
|
+
# by a SIGPIPE signal. This is probably incorrect behavior from the formatter command, but is
|
|
193
|
+
# allowed by design (for now). So we emit a warning, but will not fail.
|
|
194
|
+
|
|
195
|
+
# If there was unread output from get_content that's an indication that the formatter command is
|
|
196
|
+
# probably not configured correctly. But program design allows the formatter command do do what
|
|
197
|
+
# it wants. So this is a warning, not a hard error.
|
|
198
|
+
#
|
|
199
|
+
# A SIGPIPE termination to get_content would also indicate unread output. This should not
|
|
200
|
+
# happen, but it doesn't hurt to check.
|
|
201
|
+
if len(get_content_unread_stdout) > 0 or get_content.returncode == -signal.SIGPIPE:
|
|
202
|
+
warn(
|
|
203
|
+
f"the formatter command exited before reading all content from {file_path}"
|
|
204
|
+
)
|
|
121
205
|
|
|
122
|
-
|
|
206
|
+
if get_content.returncode != 0 and get_content.returncode != -signal.SIGPIPE:
|
|
207
|
+
if verbose:
|
|
208
|
+
info_stderr(
|
|
209
|
+
f"non-zero exit status from `git cat-file -p {object_hash}`\n"
|
|
210
|
+
+ f"exit status: {get_content.returncode}\n"
|
|
211
|
+
+ f"file path: {file_path}\n"
|
|
212
|
+
)
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"unable to read file content for {file_path} from object database."
|
|
215
|
+
)
|
|
123
216
|
|
|
124
217
|
if write_object.returncode != 0:
|
|
125
|
-
|
|
218
|
+
if verbose:
|
|
219
|
+
info_stderr(
|
|
220
|
+
f"non-zero exit status from `git hash-object -w --stdin`\n"
|
|
221
|
+
+ f"exit status: {write_object.returncode}\n"
|
|
222
|
+
+ f"file path: {file_path}\n"
|
|
223
|
+
)
|
|
224
|
+
raise Exception("unable to write formatted content to object database")
|
|
225
|
+
|
|
226
|
+
return new_hash.decode("utf-8").rstrip()
|
|
126
227
|
|
|
127
|
-
return new_hash.decode('utf-8').rstrip()
|
|
128
228
|
|
|
129
229
|
def object_is_empty(object_hash):
|
|
130
230
|
get_content = subprocess.Popen(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
231
|
+
["git", "cat-file", "-p", object_hash], stdout=subprocess.PIPE
|
|
232
|
+
)
|
|
134
233
|
content, err = get_content.communicate()
|
|
135
234
|
|
|
136
235
|
if get_content.returncode != 0:
|
|
137
|
-
raise Exception(
|
|
236
|
+
raise Exception("unable to verify content of formatted object")
|
|
138
237
|
|
|
139
238
|
return not content
|
|
140
239
|
|
|
240
|
+
|
|
141
241
|
def replace_file_in_index(diff_entry, new_object_hash):
|
|
142
|
-
subprocess.check_call(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
242
|
+
subprocess.check_call(
|
|
243
|
+
[
|
|
244
|
+
"git",
|
|
245
|
+
"update-index",
|
|
246
|
+
"--cacheinfo",
|
|
247
|
+
"{},{},{}".format(
|
|
248
|
+
diff_entry["dst_mode"], new_object_hash, diff_entry["src_path"]
|
|
249
|
+
),
|
|
250
|
+
]
|
|
251
|
+
)
|
|
252
|
+
|
|
148
253
|
|
|
149
254
|
def patch_working_file(path, orig_object_hash, new_object_hash):
|
|
150
255
|
patch = subprocess.check_output(
|
|
151
|
-
|
|
152
|
-
|
|
256
|
+
[
|
|
257
|
+
"git",
|
|
258
|
+
"diff",
|
|
259
|
+
"--no-ext-diff",
|
|
260
|
+
"--color=never",
|
|
261
|
+
orig_object_hash,
|
|
262
|
+
new_object_hash,
|
|
263
|
+
]
|
|
264
|
+
)
|
|
153
265
|
|
|
154
266
|
# Substitute object hashes in patch header with path to working tree file
|
|
155
|
-
patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(
|
|
267
|
+
patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(
|
|
268
|
+
new_object_hash.encode(), path.encode()
|
|
269
|
+
)
|
|
156
270
|
|
|
157
271
|
apply_patch = subprocess.Popen(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
272
|
+
["git", "apply", "-"],
|
|
273
|
+
stdin=subprocess.PIPE,
|
|
274
|
+
stdout=subprocess.PIPE,
|
|
275
|
+
stderr=subprocess.PIPE,
|
|
276
|
+
)
|
|
163
277
|
|
|
164
278
|
output, err = apply_patch.communicate(input=patch_b)
|
|
165
279
|
|
|
166
280
|
if apply_patch.returncode != 0:
|
|
167
|
-
raise Exception(
|
|
281
|
+
raise Exception(
|
|
282
|
+
"could not apply formatting changes to working tree file {}".format(path)
|
|
283
|
+
)
|
|
284
|
+
|
|
168
285
|
|
|
169
286
|
# Format: src_mode dst_mode src_hash dst_hash status/score? src_path dst_path?
|
|
170
|
-
diff_pat = re.compile(
|
|
287
|
+
diff_pat = re.compile(
|
|
288
|
+
r"^:(\d+) (\d+) ([a-f0-9]+) ([a-f0-9]+) ([A-Z])(\d+)?\t([^\t]+)(?:\t([^\t]+))?$"
|
|
289
|
+
)
|
|
290
|
+
|
|
171
291
|
|
|
172
292
|
# Parse output from `git diff-index`
|
|
173
293
|
def parse_diff(diff):
|
|
174
294
|
m = diff_pat.match(diff)
|
|
175
295
|
if not m:
|
|
176
|
-
raise ValueError(
|
|
296
|
+
raise ValueError("Failed to parse diff-index line: " + diff)
|
|
177
297
|
return {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
298
|
+
"src_mode": unless_zeroed(m.group(1)),
|
|
299
|
+
"dst_mode": unless_zeroed(m.group(2)),
|
|
300
|
+
"src_hash": unless_zeroed(m.group(3)),
|
|
301
|
+
"dst_hash": unless_zeroed(m.group(4)),
|
|
302
|
+
"status": m.group(5),
|
|
303
|
+
"score": int(m.group(6)) if m.group(6) else None,
|
|
304
|
+
"src_path": m.group(7),
|
|
305
|
+
"dst_path": m.group(8),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
zeroed_pat = re.compile(r"^0+$")
|
|
310
|
+
|
|
189
311
|
|
|
190
312
|
# Returns the argument unless the argument is a string of zeroes, in which case
|
|
191
313
|
# returns `None`
|
|
192
314
|
def unless_zeroed(s):
|
|
193
315
|
return s if not zeroed_pat.match(s) else None
|
|
194
316
|
|
|
317
|
+
|
|
195
318
|
def get_git_root():
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
319
|
+
return (
|
|
320
|
+
subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
|
|
321
|
+
.decode("utf-8")
|
|
322
|
+
.rstrip()
|
|
323
|
+
)
|
|
324
|
+
|
|
199
325
|
|
|
200
326
|
def normalize_path(p, relative_to=None):
|
|
201
|
-
return os.path.abspath(
|
|
202
|
-
|
|
203
|
-
)
|
|
327
|
+
return os.path.abspath(os.path.join(relative_to, p) if relative_to else p)
|
|
328
|
+
|
|
204
329
|
|
|
205
330
|
def matches_some_path(patterns, target):
|
|
206
331
|
is_match = False
|
|
@@ -210,70 +335,74 @@ def matches_some_path(patterns, target):
|
|
|
210
335
|
is_match = is_pattern_positive
|
|
211
336
|
return is_match
|
|
212
337
|
|
|
338
|
+
|
|
213
339
|
# Checks for a '!' as the first character of a pattern, returns the rest of the
|
|
214
340
|
# pattern in a tuple. The tuple takes the form (is_pattern_positive, pattern).
|
|
215
341
|
# For example:
|
|
216
342
|
# from_signed_pattern('!pat') == (False, 'pat')
|
|
217
343
|
# from_signed_pattern('pat') == (True, 'pat')
|
|
218
344
|
def from_signed_pattern(pattern):
|
|
219
|
-
if pattern[0] ==
|
|
345
|
+
if pattern[0] == "!":
|
|
220
346
|
return (False, pattern[1:])
|
|
221
347
|
else:
|
|
222
348
|
return (True, pattern)
|
|
223
349
|
|
|
350
|
+
|
|
224
351
|
class CustomArgumentParser(argparse.ArgumentParser):
|
|
225
352
|
def parse_args(self, args=None, namespace=None):
|
|
226
353
|
args, argv = self.parse_known_args(args, namespace)
|
|
227
354
|
if argv:
|
|
228
355
|
msg = argparse._(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
self.error(msg %
|
|
356
|
+
"unrecognized arguments: %s. Do you need to quote your formatter command?"
|
|
357
|
+
)
|
|
358
|
+
self.error(msg % " ".join(argv))
|
|
232
359
|
return args
|
|
233
360
|
|
|
234
|
-
|
|
361
|
+
|
|
362
|
+
if __name__ == "__main__":
|
|
235
363
|
parser = CustomArgumentParser(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
364
|
+
description="Transform staged files using a formatting command that accepts content via stdin and produces a result via stdout.",
|
|
365
|
+
epilog='Example: %(prog)s --formatter "prettier --stdin-filepath {}" "src/*.js" "test/*.js"',
|
|
366
|
+
)
|
|
239
367
|
parser.add_argument(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
368
|
+
"--formatter",
|
|
369
|
+
"-f",
|
|
370
|
+
required=True,
|
|
371
|
+
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 {}")',
|
|
372
|
+
)
|
|
244
373
|
parser.add_argument(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
374
|
+
"--no-update-working-tree",
|
|
375
|
+
action="store_true",
|
|
376
|
+
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.",
|
|
377
|
+
)
|
|
249
378
|
parser.add_argument(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
379
|
+
"--no-write",
|
|
380
|
+
action="store_true",
|
|
381
|
+
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"',
|
|
382
|
+
)
|
|
254
383
|
parser.add_argument(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
384
|
+
"--version",
|
|
385
|
+
action="version",
|
|
386
|
+
version="%(prog)s version {}".format(VERSION),
|
|
387
|
+
help="Display version of %(prog)s",
|
|
388
|
+
)
|
|
260
389
|
parser.add_argument(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
390
|
+
"--verbose",
|
|
391
|
+
help="Show the formatting commands that are running",
|
|
392
|
+
action="store_true",
|
|
393
|
+
)
|
|
265
394
|
parser.add_argument(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
395
|
+
"files",
|
|
396
|
+
nargs="+",
|
|
397
|
+
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. 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/*")',
|
|
398
|
+
)
|
|
270
399
|
args = parser.parse_args()
|
|
271
|
-
files = vars(args)[
|
|
400
|
+
files = vars(args)["files"]
|
|
272
401
|
format_staged_files(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
402
|
+
file_patterns=files,
|
|
403
|
+
formatter=vars(args)["formatter"],
|
|
404
|
+
git_root=get_git_root(),
|
|
405
|
+
update_working_tree=not vars(args)["no_update_working_tree"],
|
|
406
|
+
write=not vars(args)["no_write"],
|
|
407
|
+
verbose=vars(args)["verbose"],
|
|
408
|
+
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-format-staged",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
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,17 @@
|
|
|
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": "^
|
|
39
|
+
"husky": "^9.0.11",
|
|
40
40
|
"micromatch": "^4.0.2",
|
|
41
41
|
"prettier-standard": "^9.1.1",
|
|
42
|
-
"semantic-release": "^
|
|
42
|
+
"semantic-release": "^23.0.2",
|
|
43
43
|
"strip-indent": "^3.0.0",
|
|
44
44
|
"tmp": "0.2.0",
|
|
45
45
|
"ts-node": "^8.9.1",
|