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.
Files changed (3) hide show
  1. package/README.md +12 -14
  2. package/git-format-staged +242 -162
  3. package/package.json +4 -11
package/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # git-format-staged
2
2
 
3
- [![Build Status](https://travis-ci.org/hallettj/git-format-staged.svg?branch=master)](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 add github:hallettj/git-format-staged
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 version 3 or 2.7.
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 version 3 or 2.7.
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 "{}"' 'src/*.js'
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 "{}"' 'src/*.js' 'src/*.jsx' 'src/*.ts'
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 "{}"' '*.js' '!flow-typed/*'
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. This is useful if your
108
- formatter needs to know the file extension to determine how to format or to
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 "{}"' '*.js' '*.css'
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 "{}" >&2' 'src/*.js'
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 \"{}\"' '*.js' '*.ts'"
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 "{}"' '*.js'
10
+ # Example: git-format-staged --formatter 'prettier --stdin-filepath {}' '*.js'
11
11
  #
12
- # Tested with Python 3.10 and Python 2.7.
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 print_function
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
- # The string 3.1.1 is replaced during the publish process.
26
- VERSION = '3.1.1'
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
- def info(msg):
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 fatal(msg):
36
- print('{}: error: {}'.format(PROG, msg), file=sys.stderr)
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
- def format_staged_files(file_patterns, formatter, git_root, update_working_tree=True, write=True, verbose=False):
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
- output = subprocess.check_output([
42
- 'git', 'diff-index',
43
- '--cached',
44
- '--diff-filter=AM', # select only file additions and modifications
45
- '--no-renames',
46
- 'HEAD'
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 = parse_diff(line.decode('utf-8'))
50
- entry_path = normalize_path(entry['src_path'], relative_to=git_root)
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, entry_path)):
74
+ if not (matches_some_path(file_patterns, entry.src_path)):
55
75
  continue
56
- if format_file_in_index(formatter, entry, update_working_tree=update_working_tree, write=write, verbose=verbose):
57
- info('Reformatted {} with {}'.format(entry['src_path'], formatter))
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(formatter, diff_entry, update_working_tree=True, write=True, verbose=False):
65
- orig_hash = diff_entry['dst_hash']
66
- new_hash = format_object(formatter, orig_hash, diff_entry['src_path'], verbose=verbose)
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['src_path'], orig_hash, new_hash)
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
- file_path_placeholder = re.compile(r'\{\}')
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(formatter, object_hash, file_path, verbose=False):
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
- ['git', 'cat-file', '-p', object_hash],
96
- stdout=subprocess.PIPE
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
- command,
103
- shell=True,
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
- ['git', 'hash-object', '-w', '--stdin'],
109
- stdin=format_content.stdout,
110
- stdout=subprocess.PIPE
111
- )
154
+ ["git", "hash-object", "-w", "--stdin"],
155
+ stdin=format_content.stdout,
156
+ stdout=subprocess.PIPE,
157
+ )
112
158
 
113
- get_content.stdout.close()
114
- format_content.stdout.close()
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('unable to read file content from object database: ' + object_hash)
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('formatter exited with non-zero status') # TODO: capture stderr from format command
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('unable to write formatted content to object database')
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
- ['git', 'cat-file', '-p', object_hash],
132
- stdout=subprocess.PIPE
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('unable to verify content of formatted object')
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 patch_working_file(path, orig_object_hash, new_object_hash):
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
- ['git', 'diff', '--no-ext-diff', '--color=never', orig_object_hash, new_object_hash]
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(new_object_hash.encode(), path.encode())
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
- ['git', 'apply', '-'],
159
- stdin=subprocess.PIPE,
160
- stdout=subprocess.PIPE,
161
- stderr=subprocess.PIPE
162
- )
220
+ ["git", "apply", "-"],
221
+ stdin=subprocess.PIPE,
222
+ stdout=subprocess.PIPE,
223
+ stderr=subprocess.PIPE,
224
+ )
163
225
 
164
- output, err = apply_patch.communicate(input=patch_b)
226
+ _output, _err = apply_patch.communicate(input=patch_b)
165
227
 
166
228
  if apply_patch.returncode != 0:
167
- raise Exception('could not apply formatting changes to working tree file {}'.format(path))
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(r'^:(\d+) (\d+) ([a-f0-9]+) ([a-f0-9]+) ([A-Z])(\d+)?\t([^\t]+)(?:\t([^\t]+))?$')
171
-
172
- # Parse output from `git diff-index`
173
- def parse_diff(diff):
174
- m = diff_pat.match(diff)
175
- if not m:
176
- raise ValueError('Failed to parse diff-index line: ' + diff)
177
- return {
178
- 'src_mode': unless_zeroed(m.group(1)),
179
- 'dst_mode': unless_zeroed(m.group(2)),
180
- 'src_hash': unless_zeroed(m.group(3)),
181
- 'dst_hash': unless_zeroed(m.group(4)),
182
- 'status': m.group(5),
183
- 'score': int(m.group(6)) if m.group(6) else None,
184
- 'src_path': m.group(7),
185
- 'dst_path': m.group(8)
186
- }
187
-
188
- zeroed_pat = re.compile(r'^0+$')
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, normalize_path(pattern)):
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 parse_args(self, args=None, namespace=None):
226
- args, argv = self.parse_known_args(args, namespace)
227
- if argv:
228
- msg = argparse._(
229
- 'unrecognized arguments: %s. Do you need to quote your formatter command?'
230
- )
231
- self.error(msg % ' '.join(argv))
232
- return args
233
-
234
- if __name__ == '__main__':
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
- description='Transform staged files using a formatting command that accepts content via stdin and produces a result via stdout.',
237
- epilog='Example: %(prog)s --formatter "prettier --stdin-filepath \'{}\'" "src/*.js" "test/*.js"'
238
- )
239
- parser.add_argument(
240
- '--formatter', '-f',
241
- required=True,
242
- 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 \'{}\'")'
243
- )
244
- parser.add_argument(
245
- '--no-update-working-tree',
246
- action='store_true',
247
- 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.'
248
- )
249
- parser.add_argument(
250
- '--no-write',
251
- action='store_true',
252
- 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"'
253
- )
254
- parser.add_argument(
255
- '--version',
256
- action='version',
257
- version='%(prog)s version {}'.format(VERSION),
258
- help='Display version of %(prog)s'
259
- )
260
- parser.add_argument(
261
- '--verbose',
262
- help='Show the formatting commands that are running',
263
- action='store_true'
264
- )
265
- parser.add_argument(
266
- 'files',
267
- nargs='+',
268
- 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/*")'
269
- )
270
- args = parser.parse_args()
271
- files = vars(args)['files']
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
- file_patterns=files,
274
- formatter=vars(args)['formatter'],
275
- git_root=get_git_root(),
276
- update_working_tree=not vars(args)['no_update_working_tree'],
277
- write=not vars(args)['no_write'],
278
- verbose=vars(args)['verbose']
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.1.1",
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": "^8.3.5",
33
- "@commitlint/config-conventional": "^8.3.4",
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": "^19.0.2",
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
  }