git_stage_formatter 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +28 -0
- data/Gemfile +6 -0
- data/LICENSE +7 -0
- data/README.md +194 -0
- data/bin/git_stage_formatter +5 -0
- data/commitlint.config.js +5 -0
- data/git-format-staged +267 -0
- data/lib/git_stage_formatter.rb +9 -0
- data/lib/git_stage_formatter/version.rb +5 -0
- data/no-main.js +5 -0
- data/package-lock.json +10537 -0
- data/package.json +68 -0
- data/tsconfig.json +15 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e24f5f48a24491053f2e2220ce055df6f61c6207a750f5a8ff6098afbde4120a
|
4
|
+
data.tar.gz: 54553233ebdb102c78ab79d8a660c12f8df10647f3cfb65c22397516d8f0b965
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 20911e08c4169554122387169899c2d93f9b0be2ef7d305370c059dba9be6ab88a8553efdbf0fbb74598a31ea62cc7945a04dcb416379fb7abef8f1d677700f4
|
7
|
+
data.tar.gz: d86c46ed60800b49fd8ee4132226e97483a1fb15060d08642984d55fc23096f2d8147ea04a835763d48ef8c25ff1e05d7afd87472f6a8a745039b41872bc0b9c
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
language: python
|
2
|
+
python:
|
3
|
+
- 3.8
|
4
|
+
- 3.6
|
5
|
+
- 2.7
|
6
|
+
|
7
|
+
cache:
|
8
|
+
directories:
|
9
|
+
- ~/.npm
|
10
|
+
|
11
|
+
before_install:
|
12
|
+
- nvm install v14
|
13
|
+
|
14
|
+
install:
|
15
|
+
- npm install
|
16
|
+
|
17
|
+
script:
|
18
|
+
- npm test
|
19
|
+
|
20
|
+
jobs:
|
21
|
+
include:
|
22
|
+
- stage: release
|
23
|
+
deploy:
|
24
|
+
provider: script
|
25
|
+
skip_cleanup: true
|
26
|
+
script: "npm run semantic-release"
|
27
|
+
on:
|
28
|
+
all_branches: true
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright 2018 Jesse Hallett
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
# git-format-staged
|
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
|
+
Consider a project where you want all code formatted consistently. So you use
|
6
|
+
a formatting command. (For example I use [prettier-standard][] in my
|
7
|
+
Javascript projects.) You want to make sure that everyone working on the
|
8
|
+
project runs the formatter, so you use a tool like [husky][] to install a git
|
9
|
+
pre-commit hook. The naive way to write that hook would be to:
|
10
|
+
|
11
|
+
- get a list of staged files
|
12
|
+
- run the formatter on those files
|
13
|
+
- run `git add` to stage the results of formatting
|
14
|
+
|
15
|
+
The problem with that solution is it forces you to commit entire files. At
|
16
|
+
worst this will lead to contributors to unwittingly committing changes. At
|
17
|
+
best it disrupts workflow for contributors who use `git add -p`.
|
18
|
+
|
19
|
+
git-format-staged tackles this problem by running the formatter on the staged
|
20
|
+
version of the file. Staging changes to a file actually produces a new file
|
21
|
+
that exists in the git object database. git-format-staged uses some git
|
22
|
+
plumbing commands to send content from that file to your formatter. The command
|
23
|
+
replaces file content in the git index. The process bypasses the working tree,
|
24
|
+
so any unstaged changes are ignored by the formatter, and remain unstaged.
|
25
|
+
|
26
|
+
After formatting a staged file git-format-staged computes a patch which it
|
27
|
+
attempts to apply to the working tree file to keep the working tree in sync
|
28
|
+
with staged changes. If patching fails you will see a warning message. The
|
29
|
+
version of the file that is committed will be formatted properly - the warning
|
30
|
+
just means that working tree copy of the file has been left unformatted. The
|
31
|
+
patch step can be disabled with the `--no-update-working-tree` option.
|
32
|
+
|
33
|
+
[prettier-standard]: https://www.npmjs.com/package/prettier-standard
|
34
|
+
[husky]: https://www.npmjs.com/package/husky
|
35
|
+
|
36
|
+
|
37
|
+
## How to install
|
38
|
+
|
39
|
+
Requires Python version 3 or 2.7.
|
40
|
+
|
41
|
+
Install as a development dependency in a project that uses npm packages:
|
42
|
+
|
43
|
+
$ npm install --save-dev git-format-staged
|
44
|
+
|
45
|
+
Or install globally:
|
46
|
+
|
47
|
+
$ npm install --global git-format-staged
|
48
|
+
|
49
|
+
If you do not use npm you can copy the
|
50
|
+
[`git-format-staged`](./git-format-staged) script from this repository and
|
51
|
+
place it in your executable path. The script is MIT-licensed - so you can check
|
52
|
+
the script into version control in your own open source project if you wish.
|
53
|
+
|
54
|
+
|
55
|
+
## How to use
|
56
|
+
|
57
|
+
For detailed information run:
|
58
|
+
|
59
|
+
$ git-format-staged --help
|
60
|
+
|
61
|
+
The command expects a shell command to run a formatter, and one or more file
|
62
|
+
patterns to identify which files should be formatted. For example:
|
63
|
+
|
64
|
+
$ git-format-staged --formatter 'prettier --stdin --stdin-filepath "{}"' 'src/*.js'
|
65
|
+
|
66
|
+
That will format all files under `src/` and its subdirectories using
|
67
|
+
`prettier`. The file pattern is tested against staged files using Python's
|
68
|
+
[`fnmatch`][] function: each `*` will match nested directories in addition to
|
69
|
+
file names.
|
70
|
+
|
71
|
+
[`fnmatch`]: https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch
|
72
|
+
|
73
|
+
The formatter command must read file content from `stdin`, and output formatted
|
74
|
+
content to `stdout`.
|
75
|
+
|
76
|
+
Files can be excluded by prefixing a pattern with `!`. For example:
|
77
|
+
|
78
|
+
$ git-format-staged --formatter 'prettier --stdin' '*.js' '!flow-typed/*'
|
79
|
+
|
80
|
+
Patterns are evaluated from left-to-right: if a file matches multiple patterns
|
81
|
+
the right-most pattern determines whether the file is included or excluded.
|
82
|
+
|
83
|
+
git-format-staged never operates on files that are excluded from version
|
84
|
+
control. So it is not necessary to explicitly exclude stuff like
|
85
|
+
`node_modules/`.
|
86
|
+
|
87
|
+
The formatter command may include a placeholder, `{}`, which will be replaced
|
88
|
+
with the path of the file that is being formatted. This is useful if your
|
89
|
+
formatter needs to know the file extension to determine how to format or to
|
90
|
+
lint each file. For example:
|
91
|
+
|
92
|
+
$ git-format-staged -f 'prettier --stdin --stdin-filepath "{}"' '*.js' '*.css'
|
93
|
+
|
94
|
+
Do not attempt to read or write to `{}` in your formatter command! The
|
95
|
+
placeholder exists only for referencing the file name and path.
|
96
|
+
|
97
|
+
### Check staged changes with a linter without formatting
|
98
|
+
|
99
|
+
Perhaps you do not want to reformat files automatically; but you do want to
|
100
|
+
prevent files from being committed if they do not conform to style rules. You
|
101
|
+
can use git-format-staged with the `--no-write` option, and supply a lint
|
102
|
+
command instead of a format command. Here is an example using ESLint:
|
103
|
+
|
104
|
+
$ git-format-staged --no-write -f 'eslint --stdin --stdin-filename "{}" >&2' 'src/*.js'
|
105
|
+
|
106
|
+
If this command is run in a pre-commit hook, and the lint command fails the
|
107
|
+
commit will be aborted and error messages will be displayed. The lint command
|
108
|
+
must read file content via `stdin`. Anything that the lint command outputs to
|
109
|
+
`stdout` will be ignored. In the example above `eslint` is given the `--stdin`
|
110
|
+
option to tell it to read content from `stdin` instead of reading files from
|
111
|
+
disk, and messages from `eslint` are redirected to `stderr` (using the `>&2`
|
112
|
+
notation) so that you can see them.
|
113
|
+
|
114
|
+
### Set up a pre-commit hook with Husky
|
115
|
+
|
116
|
+
Follow these steps to automatically format all Javascript files on commit in
|
117
|
+
a project that uses npm.
|
118
|
+
|
119
|
+
Install git-format-staged, husky, and a formatter (I use prettier-standard):
|
120
|
+
|
121
|
+
$ npm install --save-dev git-format-staged husky prettier-standard
|
122
|
+
|
123
|
+
Add a `"precommit"` script in `package.json`:
|
124
|
+
|
125
|
+
"scripts": {
|
126
|
+
"precommit": "git-format-staged -f prettier-standard '*.js'"
|
127
|
+
}
|
128
|
+
|
129
|
+
Once again note that the `'*.js'` pattern is quoted! If the formatter command
|
130
|
+
included arguments it would also need to be quoted.
|
131
|
+
|
132
|
+
That's it! Whenever a file is changed as a result of formatting on commit you
|
133
|
+
will see a message in the output from `git commit`.
|
134
|
+
|
135
|
+
|
136
|
+
## Comparisons to similar utilities
|
137
|
+
|
138
|
+
There are other tools that will format or lint staged files. What distinguishes
|
139
|
+
git-format-staged is that when a file has both staged and unstaged changes
|
140
|
+
git-format-staged ignores the unstaged changes; and it leaves unstaged changes
|
141
|
+
unstaged when applying formatting.
|
142
|
+
|
143
|
+
Some linters (such as [precise-commits][]) have an option to restrict linting
|
144
|
+
to certain lines or character ranges in files, which is one way to ignore
|
145
|
+
unstaged changes while linting. The author is not aware of a utility other than
|
146
|
+
git-format-staged that can apply any arbitrary linter so that it ignores
|
147
|
+
unstaged changes.
|
148
|
+
|
149
|
+
Some other formatting utilities (such as [pre-commit][])
|
150
|
+
use a different strategy to keep unstaged changes unstaged:
|
151
|
+
|
152
|
+
1. stash unstaged changes
|
153
|
+
2. apply the formatter to working tree files
|
154
|
+
3. stage any resulting changes
|
155
|
+
4. reapply stashed changes to the working tree.
|
156
|
+
|
157
|
+
The problem is that you may get a conflict where stashed changes cannot be
|
158
|
+
automatically merged after formatting has been applied. In those cases the user
|
159
|
+
has to do some manual fixing to retrieve unstaged changes. As far as the author
|
160
|
+
is aware git-format-staged is the only utility that applies a formatter without
|
161
|
+
touching working tree files, and then merges formatting changes to the working
|
162
|
+
tree. The advantage of merging formatting changes into unstaged changes (as
|
163
|
+
opposed to merging unstaged changes into formatting changes) is that
|
164
|
+
git-format-staged is non-lossy: if there are conflicts between unstaged changes
|
165
|
+
and formatting the unstaged changes win, and are kept in the working tree,
|
166
|
+
while staged/committed files are formatted properly.
|
167
|
+
|
168
|
+
Another advantage of git-format-staged is that it has no dependencies beyond
|
169
|
+
Python and git, and can be dropped into any programming language ecosystem.
|
170
|
+
|
171
|
+
Some more comparisons:
|
172
|
+
|
173
|
+
- [lint-staged][] lints and formats staged files. At the time of this writing
|
174
|
+
it does not have an official strategy for ignoring unstaged changes when
|
175
|
+
linting, or for keeping unstaged changes unstaged when formatting. But
|
176
|
+
lint-staged does provide powerful configuration options around which files
|
177
|
+
should be linted or formatted, what should happen before and after linting,
|
178
|
+
and so on.
|
179
|
+
- [pretty-quick][] formats staged files with prettier. By default pretty-quick
|
180
|
+
will abort the commit if files are partially staged to allow the user to
|
181
|
+
decide how to re-stage changes from formatting. The result is more manual
|
182
|
+
effort compared to git-format-staged.
|
183
|
+
- the one-liner
|
184
|
+
`git diff --diff-filter=d --cached | grep '^[+-]' | grep -Ev '^(--- a/|\+\+\+ b/)' | LINT_COMMAND`
|
185
|
+
(described [here][lint changed hunks]) extracts changed hunks and feeds them
|
186
|
+
to a linter. But linting will fail if the linter requires the entire file for
|
187
|
+
context. For example a linter might report errors if it cannot see import
|
188
|
+
lines.
|
189
|
+
|
190
|
+
[precise-commits]: https://github.com/nrwl/precise-commits
|
191
|
+
[pre-commit]: https://pre-commit.com/#pre-commit-during-commits
|
192
|
+
[pretty-quick]: https://www.npmjs.com/package/pretty-quick
|
193
|
+
[lint-staged]: https://github.com/okonet/lint-staged
|
194
|
+
[lint changed hunks]: https://github.com/okonet/lint-staged/issues/62#issuecomment-383217916
|
data/git-format-staged
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
#
|
3
|
+
# Git command to transform staged files according to a command that accepts file
|
4
|
+
# content on stdin and produces output on stdout. This command is useful in
|
5
|
+
# combination with `git add -p` which allows you to stage specific changes in
|
6
|
+
# a file. This command runs a formatter on the file with staged changes while
|
7
|
+
# ignoring unstaged changes.
|
8
|
+
#
|
9
|
+
# Usage: git-format-staged [OPTION]... [FILE]...
|
10
|
+
# Example: git-format-staged --formatter 'prettier --stdin' '*.js'
|
11
|
+
#
|
12
|
+
# Tested with Python 3.6 and Python 2.7.
|
13
|
+
#
|
14
|
+
# Original author: Jesse Hallett <jesse@sitr.us>
|
15
|
+
|
16
|
+
from __future__ import print_function
|
17
|
+
import argparse
|
18
|
+
from fnmatch import fnmatch
|
19
|
+
from gettext import gettext as _
|
20
|
+
import os
|
21
|
+
import re
|
22
|
+
import subprocess
|
23
|
+
import sys
|
24
|
+
|
25
|
+
# The string $VERSION is replaced during the publish process.
|
26
|
+
VERSION = '$VERSION'
|
27
|
+
PROG = sys.argv[0]
|
28
|
+
|
29
|
+
def info(msg):
|
30
|
+
print(msg, file=sys.stderr)
|
31
|
+
|
32
|
+
def warn(msg):
|
33
|
+
print('{}: warning: {}'.format(PROG, msg), file=sys.stderr)
|
34
|
+
|
35
|
+
def fatal(msg):
|
36
|
+
print('{}: error: {}'.format(PROG, msg), file=sys.stderr)
|
37
|
+
exit(1)
|
38
|
+
|
39
|
+
def format_staged_files(file_patterns, formatter, git_root, update_working_tree=True, write=True):
|
40
|
+
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
|
+
])
|
48
|
+
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 not (matches_some_path(file_patterns, entry_path)):
|
52
|
+
continue
|
53
|
+
if format_file_in_index(formatter, entry, update_working_tree=update_working_tree, write=write):
|
54
|
+
info('Reformatted {} with {}'.format(entry['src_path'], formatter))
|
55
|
+
except Exception as err:
|
56
|
+
fatal(str(err))
|
57
|
+
|
58
|
+
# Run formatter on file in the git index. Creates a new git object with the
|
59
|
+
# result, and replaces the content of the file in the index with that object.
|
60
|
+
# Returns hash of the new object if formatting produced any changes.
|
61
|
+
def format_file_in_index(formatter, diff_entry, update_working_tree=True, write=True):
|
62
|
+
orig_hash = diff_entry['dst_hash']
|
63
|
+
new_hash = format_object(formatter, orig_hash, diff_entry['src_path'])
|
64
|
+
|
65
|
+
# If the new hash is the same then the formatter did not make any changes.
|
66
|
+
if not write or new_hash == orig_hash:
|
67
|
+
return None
|
68
|
+
|
69
|
+
# If the content of the new object is empty then the formatter did not
|
70
|
+
# produce any output. We want to abort instead of replacing the file with an
|
71
|
+
# empty one.
|
72
|
+
if object_is_empty(new_hash):
|
73
|
+
return None
|
74
|
+
|
75
|
+
replace_file_in_index(diff_entry, new_hash)
|
76
|
+
|
77
|
+
if update_working_tree:
|
78
|
+
try:
|
79
|
+
patch_working_file(diff_entry['src_path'], orig_hash, new_hash)
|
80
|
+
except Exception as err:
|
81
|
+
# Errors patching working tree files are not fatal
|
82
|
+
warn(str(err))
|
83
|
+
|
84
|
+
return new_hash
|
85
|
+
|
86
|
+
file_path_placeholder = re.compile('\{\}')
|
87
|
+
|
88
|
+
# Run formatter on a git blob identified by its hash. Writes output to a new git
|
89
|
+
# blob, and returns the hash of the new blob.
|
90
|
+
def format_object(formatter, object_hash, file_path):
|
91
|
+
get_content = subprocess.Popen(
|
92
|
+
['git', 'cat-file', '-p', object_hash],
|
93
|
+
stdout=subprocess.PIPE
|
94
|
+
)
|
95
|
+
format_content = subprocess.Popen(
|
96
|
+
re.sub(file_path_placeholder, file_path, formatter),
|
97
|
+
shell=True,
|
98
|
+
stdin=get_content.stdout,
|
99
|
+
stdout=subprocess.PIPE
|
100
|
+
)
|
101
|
+
write_object = subprocess.Popen(
|
102
|
+
['git', 'hash-object', '-w', '--stdin'],
|
103
|
+
stdin=format_content.stdout,
|
104
|
+
stdout=subprocess.PIPE
|
105
|
+
)
|
106
|
+
|
107
|
+
get_content.stdout.close()
|
108
|
+
format_content.stdout.close()
|
109
|
+
|
110
|
+
if get_content.wait() != 0:
|
111
|
+
raise ValueError('unable to read file content from object database: ' + object_hash)
|
112
|
+
|
113
|
+
if format_content.wait() != 0:
|
114
|
+
raise Exception('formatter exited with non-zero status') # TODO: capture stderr from format command
|
115
|
+
|
116
|
+
new_hash, err = write_object.communicate()
|
117
|
+
|
118
|
+
if write_object.returncode != 0:
|
119
|
+
raise Exception('unable to write formatted content to object database')
|
120
|
+
|
121
|
+
return new_hash.decode('utf-8').rstrip()
|
122
|
+
|
123
|
+
def object_is_empty(object_hash):
|
124
|
+
get_content = subprocess.Popen(
|
125
|
+
['git', 'cat-file', '-p', object_hash],
|
126
|
+
stdout=subprocess.PIPE
|
127
|
+
)
|
128
|
+
content, err = get_content.communicate()
|
129
|
+
|
130
|
+
if get_content.returncode != 0:
|
131
|
+
raise Exception('unable to verify content of formatted object')
|
132
|
+
|
133
|
+
return not content
|
134
|
+
|
135
|
+
def replace_file_in_index(diff_entry, new_object_hash):
|
136
|
+
subprocess.check_call(['git', 'update-index',
|
137
|
+
'--cacheinfo', '{},{},{}'.format(
|
138
|
+
diff_entry['dst_mode'],
|
139
|
+
new_object_hash,
|
140
|
+
diff_entry['src_path']
|
141
|
+
)])
|
142
|
+
|
143
|
+
def patch_working_file(path, orig_object_hash, new_object_hash):
|
144
|
+
patch = subprocess.check_output(
|
145
|
+
['git', 'diff', orig_object_hash, new_object_hash]
|
146
|
+
)
|
147
|
+
|
148
|
+
# Substitute object hashes in patch header with path to working tree file
|
149
|
+
patch_b = patch.replace(orig_object_hash.encode(), path.encode()).replace(new_object_hash.encode(), path.encode())
|
150
|
+
|
151
|
+
apply_patch = subprocess.Popen(
|
152
|
+
['git', 'apply', '-'],
|
153
|
+
stdin=subprocess.PIPE,
|
154
|
+
stdout=subprocess.PIPE,
|
155
|
+
stderr=subprocess.PIPE
|
156
|
+
)
|
157
|
+
|
158
|
+
output, err = apply_patch.communicate(input=patch_b)
|
159
|
+
|
160
|
+
if apply_patch.returncode != 0:
|
161
|
+
raise Exception('could not apply formatting changes to working tree file {}'.format(path))
|
162
|
+
|
163
|
+
# Format: src_mode dst_mode src_hash dst_hash status/score? src_path dst_path?
|
164
|
+
diff_pat = re.compile('^:(\d+) (\d+) ([a-f0-9]+) ([a-f0-9]+) ([A-Z])(\d+)?\t([^\t]+)(?:\t([^\t]+))?$')
|
165
|
+
|
166
|
+
# Parse output from `git diff-index`
|
167
|
+
def parse_diff(diff):
|
168
|
+
m = diff_pat.match(diff)
|
169
|
+
if not m:
|
170
|
+
raise ValueError('Failed to parse diff-index line: ' + diff)
|
171
|
+
return {
|
172
|
+
'src_mode': unless_zeroed(m.group(1)),
|
173
|
+
'dst_mode': unless_zeroed(m.group(2)),
|
174
|
+
'src_hash': unless_zeroed(m.group(3)),
|
175
|
+
'dst_hash': unless_zeroed(m.group(4)),
|
176
|
+
'status': m.group(5),
|
177
|
+
'score': int(m.group(6)) if m.group(6) else None,
|
178
|
+
'src_path': m.group(7),
|
179
|
+
'dst_path': m.group(8)
|
180
|
+
}
|
181
|
+
|
182
|
+
zeroed_pat = re.compile('^0+$')
|
183
|
+
|
184
|
+
# Returns the argument unless the argument is a string of zeroes, in which case
|
185
|
+
# returns `None`
|
186
|
+
def unless_zeroed(s):
|
187
|
+
return s if not zeroed_pat.match(s) else None
|
188
|
+
|
189
|
+
def get_git_root():
|
190
|
+
return subprocess.check_output(
|
191
|
+
['git', 'rev-parse', '--show-toplevel']
|
192
|
+
).decode('utf-8').rstrip()
|
193
|
+
|
194
|
+
def normalize_path(p, relative_to=None):
|
195
|
+
return os.path.abspath(
|
196
|
+
os.path.join(relative_to, p) if relative_to else p
|
197
|
+
)
|
198
|
+
|
199
|
+
def matches_some_path(patterns, target):
|
200
|
+
is_match = False
|
201
|
+
for signed_pattern in patterns:
|
202
|
+
(is_pattern_positive, pattern) = from_signed_pattern(signed_pattern)
|
203
|
+
if fnmatch(target, normalize_path(pattern)):
|
204
|
+
is_match = is_pattern_positive
|
205
|
+
return is_match
|
206
|
+
|
207
|
+
# Checks for a '!' as the first character of a pattern, returns the rest of the
|
208
|
+
# pattern in a tuple. The tuple takes the form (is_pattern_positive, pattern).
|
209
|
+
# For example:
|
210
|
+
# from_signed_pattern('!pat') == (False, 'pat')
|
211
|
+
# from_signed_pattern('pat') == (True, 'pat')
|
212
|
+
def from_signed_pattern(pattern):
|
213
|
+
if pattern[0] == '!':
|
214
|
+
return (False, pattern[1:])
|
215
|
+
else:
|
216
|
+
return (True, pattern)
|
217
|
+
|
218
|
+
class CustomArgumentParser(argparse.ArgumentParser):
|
219
|
+
def parse_args(self, args=None, namespace=None):
|
220
|
+
args, argv = self.parse_known_args(args, namespace)
|
221
|
+
if argv:
|
222
|
+
msg = argparse._(
|
223
|
+
'unrecognized arguments: %s. Do you need to quote your formatter command?'
|
224
|
+
)
|
225
|
+
self.error(msg % ' '.join(argv))
|
226
|
+
return args
|
227
|
+
|
228
|
+
if __name__ == '__main__':
|
229
|
+
parser = CustomArgumentParser(
|
230
|
+
description='Transform staged files using a formatting command that accepts content via stdin and produces a result via stdout.',
|
231
|
+
epilog='Example: %(prog)s --formatter "prettier --stdin" "src/*.js" "test/*.js"'
|
232
|
+
)
|
233
|
+
parser.add_argument(
|
234
|
+
'--formatter', '-f',
|
235
|
+
required=True,
|
236
|
+
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 --stdin-filepath \'{}\'")'
|
237
|
+
)
|
238
|
+
parser.add_argument(
|
239
|
+
'--no-update-working-tree',
|
240
|
+
action='store_true',
|
241
|
+
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.'
|
242
|
+
)
|
243
|
+
parser.add_argument(
|
244
|
+
'--no-write',
|
245
|
+
action='store_true',
|
246
|
+
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"'
|
247
|
+
)
|
248
|
+
parser.add_argument(
|
249
|
+
'--version',
|
250
|
+
action='version',
|
251
|
+
version='%(prog)s version {}'.format(VERSION),
|
252
|
+
help='Display version of %(prog)s'
|
253
|
+
)
|
254
|
+
parser.add_argument(
|
255
|
+
'files',
|
256
|
+
nargs='+',
|
257
|
+
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/*")'
|
258
|
+
)
|
259
|
+
args = parser.parse_args()
|
260
|
+
files = vars(args)['files']
|
261
|
+
format_staged_files(
|
262
|
+
file_patterns=files,
|
263
|
+
formatter=vars(args)['formatter'],
|
264
|
+
git_root=get_git_root(),
|
265
|
+
update_working_tree=not vars(args)['no_update_working_tree'],
|
266
|
+
write=not vars(args)['no_write']
|
267
|
+
)
|