git-format-staged 4.0.0 → 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 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 with Husky
131
+ ### Set up a pre-commit hook
132
132
 
133
- Follow these steps to automatically format all Javascript files on commit in
134
- a project that uses npm.
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
- Install git-format-staged, husky, and a formatter (I use `prettier`):
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
- $ npm install --save-dev git-format-staged husky prettier
145
+ #### Write a pre-commit hook manually
139
146
 
140
- Add a `prepare` script to install husky when running `npm install`:
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
- $ npm set-script prepare "husky install"
143
- $ npm run prepare
151
+ #### Nix devShell
144
152
 
145
- Add the pre-commit hook:
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
- $ npx husky add .husky/pre-commit "git-format-staged --formatter 'prettier --stdin-filepath {}' '*.js' '*.ts'"
148
- $ git add .husky/pre-commit
159
+ [nix-git-hooks]: https://github.com/ysndr/nix-git-hooks
149
160
 
150
- Once again note that the formatter command and the `'*.js'` and `'*.ts'`
151
- patterns are quoted!
161
+ #### Npm, yarn, pnpm, or bun
152
162
 
153
- That's it! Whenever a file is changed as a result of formatting on commit you
154
- will see a message in the output from `git commit`.
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
@@ -20,13 +20,14 @@ from fnmatch import fnmatch
20
20
  from gettext import gettext as _
21
21
  import re
22
22
  import shlex
23
+ import signal
23
24
  import subprocess
24
25
  import sys
25
26
  from typing import NoReturn, Protocol, cast
26
27
 
27
28
 
28
- # The string 4.0.0 is replaced during the publish process.
29
- VERSION = "4.0.0"
29
+ # The string 4.0.1 is replaced during the publish process.
30
+ VERSION = "4.0.1"
30
31
  PROG = sys.argv[0]
31
32
 
32
33
 
@@ -34,6 +35,10 @@ def info(msg: str):
34
35
  print(msg, file=sys.stdout)
35
36
 
36
37
 
38
+ def info_stderr(msg: str):
39
+ print(msg, file=sys.stderr)
40
+
41
+
37
42
  def warn(msg: str):
38
43
  print("{}: warning: {}".format(PROG, msg), file=sys.stderr)
39
44
 
@@ -144,31 +149,85 @@ def format_object(
144
149
  get_content = subprocess.Popen(
145
150
  ["git", "cat-file", "-p", object_hash], stdout=subprocess.PIPE
146
151
  )
152
+
147
153
  command = re.sub(file_path_placeholder, shlex.quote(file_path), formatter)
148
154
  if verbose:
149
- info(command)
155
+ info_stderr(command)
150
156
  format_content = subprocess.Popen(
151
157
  command, shell=True, stdin=get_content.stdout, stdout=subprocess.PIPE
152
158
  )
159
+
153
160
  write_object = subprocess.Popen(
154
161
  ["git", "hash-object", "-w", "--stdin"],
155
162
  stdin=format_content.stdout,
156
163
  stdout=subprocess.PIPE,
157
164
  )
158
165
 
159
- # Read output from the final process first to avoid deadlock if intermediate processes produce
160
- # large outputs that would fill pipe buffers
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.
161
177
  new_hash, _err = write_object.communicate()
162
178
 
163
- if get_content.wait() != 0:
164
- raise ValueError(
165
- "unable to read file content from object database: " + object_hash
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
+ )
197
+
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}"
166
211
  )
167
212
 
168
- if format_content.wait() != 0:
169
- raise Exception("formatter exited with non-zero status")
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
+ )
170
223
 
171
224
  if write_object.returncode != 0:
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
+ )
172
231
  raise Exception("unable to write formatted content to object database")
173
232
 
174
233
  return new_hash.decode("utf-8").rstrip()
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "git-format-staged",
3
- "version": "4.0.0",
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
  },