Skip to content

Commit

Permalink
feat: add --no-write option which skips applying formatting changes
Browse files Browse the repository at this point in the history
With the `--no-write` option git-format-staged runs the given formatter
command, but ignores its output and does not make changes to staged
files or to working tree files. This can be used as a dry-run option for
testing, or it can be used to lint staged versions of files without
formatting them. For example:

    git-format-staged --no-write -f 'eslint --stdin >&2' '*.js'
  • Loading branch information
hallettj committed Jun 3, 2018
1 parent ded6e95 commit 418c847
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 8 deletions.
26 changes: 23 additions & 3 deletions README.md
Expand Up @@ -68,9 +68,12 @@ file names.

[`fnmatch`]: https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch

Note that both the formatter command and the file pattern are quoted. If you
prefer you may let your shell expand a file glob for you. This command is
equivalent if your shell supports [globstar][] notation:
The formatter command must read file content from `stdin`, and output formatted
content to `stdout`.

Note that both the formatter command and the file pattern
are quoted. If you prefer you may let your shell expand a file glob for you.
This command is equivalent if your shell supports [globstar][] notation:

$ git-format-staged --formatter 'prettier --stdin' src/**/*.js

Expand All @@ -79,6 +82,23 @@ shell option is set. Do not rely on globstar in npm scripts!

[globstar]: https://www.linuxjournal.com/content/globstar-new-bash-globbing-option

### Check staged changes with a linter without formatting

Perhaps you do not want to reformat files automatically; but you do want to
prevent files from being committed if they do not conform to style rules. You
can use git-format-staged with the `--no-write` option, and supply a lint
command instead of a format command. Here is an example using ESLint:

$ git-format-staged --no-write -f 'eslint --stdin >&2' 'src/*.js'

If this command is run in a pre-commit hook, and the lint command fails the
commit will be aborted and error messages will be displayed. The lint command
must read file content via `stdin`. Anything that the lint command outputs to
`stdout` will be ignored. In the example above `eslint` is given the `--stdin`
option to tell it to read content from `stdin` instead of reading files from
disk, and messages from `eslint` are redirected to `stderr` (using the `>&2`
notation) so that you can see them.

### Set up a pre-commit hook with Husky

Follow these steps to automatically format all Javascript files on commit in
Expand Down
16 changes: 11 additions & 5 deletions git-format-staged
Expand Up @@ -36,7 +36,7 @@ def fatal(msg):
print('{}: error: {}'.format(PROG, msg), file=sys.stderr)
exit(1)

def format_staged_files(files, formatter, git_root, update_working_tree=False):
def format_staged_files(files, formatter, git_root, update_working_tree=True, write=True):
try:
output = subprocess.check_output([
'git', 'diff-index',
Expand All @@ -50,20 +50,20 @@ def format_staged_files(files, formatter, git_root, update_working_tree=False):
entry_path = normalize_path(entry['src_path'], relative_to=git_root)
if not (matches_some_path(files, entry_path)):
continue
if format_file_in_index(formatter, entry, update_working_tree=update_working_tree):
if format_file_in_index(formatter, entry, update_working_tree=update_working_tree, write=write):
info('Reformatted {} with {}'.format(entry['src_path'], formatter))
except Exception as err:
fatal(str(err))

# Run formatter on file in the git index. Creates a new git object with the
# result, and replaces the content of the file in the index with that object.
# Returns hash of the new object if formatting produced any changes.
def format_file_in_index(formatter, diff_entry, update_working_tree=False):
def format_file_in_index(formatter, diff_entry, update_working_tree=True, write=True):
orig_hash = diff_entry['dst_hash']
new_hash = format_object(formatter, orig_hash)

# If the new hash is the same then the formatter did not make any changes.
if new_hash == orig_hash:
if not write or new_hash == orig_hash:
return None

replace_file_in_index(diff_entry, new_hash)
Expand Down Expand Up @@ -211,6 +211,11 @@ if __name__ == '__main__':
action='store_true',
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.'
)
parser.add_argument(
'--no-write',
action='store_true',
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 >&2" "*.js"'
)
parser.add_argument(
'--version',
action='version',
Expand All @@ -228,5 +233,6 @@ if __name__ == '__main__':
files=normalize_paths(files),
formatter=vars(args)['formatter'],
git_root=get_git_root(),
update_working_tree=not vars(args)['no_update_working_tree']
update_working_tree=not vars(args)['no_update_working_tree'],
write=not vars(args)['no_write']
)
48 changes: 48 additions & 0 deletions test/git-format-staged_test.js
Expand Up @@ -83,6 +83,18 @@ test('formats a file', async t => {
)
})

test('fails with non-zero exit status if formatter fails', async t => {
const r = repo(t)
await setContent(r, 'index.js', 'function foo{} ( return "foo" )')
await stage(r, 'index.js')
const { exitCode, stderr } = await formatStagedCaptureError(
r,
'-f prettier-standard "*.js"'
)
t.true(exitCode > 0)
t.regex(stderr, /SyntaxError: Unexpected token/)
})

test('fails if no formatter command is given', async t => {
const r = repo(t)
const { exitCode, stderr } = await formatStagedCaptureError(r, '*.js')
Expand Down Expand Up @@ -403,6 +415,42 @@ test('ignores files that are not listed on command line', async t => {
contentIs(t, await getStagedContent(r, 'README.md'), readmeContent)
})

test('does not write changes if `--no-write` option is set', async t => {
const r = repo(t)
await setContent(r, 'index.js', `function foo() { return "foo"; }`)
await stage(r, 'index.js')
await formatStaged(r, '--no-write -f prettier-standard "*.js"')
contentIs(
t,
await getStagedContent(r, 'index.js'),
`function foo() { return "foo"; }`
)
})

test('fails with non-zero exit status if formatter fails and `--no-write` is set', async t => {
const r = repo(t)
await setContent(r, 'index.js', 'function foo{} ( return "foo" )')
await stage(r, 'index.js')
const { exitCode, stderr } = await formatStagedCaptureError(
r,
'--no-write -f prettier-standard "*.js"'
)
t.true(exitCode > 0)
t.regex(stderr, /SyntaxError: Unexpected token/)
})

test('messages from formatter command can be redirected to stderr', async t => {
const r = repo(t)
await setContent(r, 'index.js', 'function foo{} ( return "foo" )')
await stage(r, 'index.js')
const { exitCode, stderr } = await formatStagedCaptureError(
r,
'--no-write -f "eslint --stdin --no-eslintrc >&2" "*.js"'
)
t.true(exitCode > 0)
t.regex(stderr, /Parsing error: Unexpected token/)
})

function contentIs (t: ExecutionContext<>, actual: string, expected: string) {
t.is(trim(actual), trim(expected))
}
Expand Down

0 comments on commit 418c847

Please sign in to comment.