Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: test

on:
pull_request:
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: extractions/setup-just@v2
- run: ./tests/run.sh
5 changes: 5 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
language: script
entry: pre-commit-just.sh
files: '\.?[jJ]ustfile'
- id: check-justfile
name: Check Justfiles
language: script
entry: pre-commit-just.sh --check
files: '\.?[jJ]ustfile'
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
This repository contains a pre-commit config for running `just --fmt` on any
discovered [just](https://github.com/casey/just) files. It will auto-fix the
files.
This repository contains pre-commit hooks for running `just --fmt` on any
discovered [just](https://github.com/casey/just) files.

Two hooks are provided:

- `format-justfile` — auto-formats justfiles in place (and fails so pre-commit
reports the change).
- `check-justfile` — verifies justfiles are formatted and valid without
modifying them, failing with `just`'s own error (e.g. a formatting diff or a
parse error such as "Extraneous attribute"). Prefer this in CI or when you'd
rather be told what's wrong than have files rewritten mid-commit.

## Usage

You must have `just` installed on your system for this hook to work.
You must have `just` installed on your system for these hooks to work. If it
isn't found, the hooks no-op so they don't block commits on machines without
`just`.

```yaml
- repo: https://github.com/instrumentl/pre-commit-just
rev: 'main'
hooks:
- id: format-justfile
- id: check-justfile
# or, to auto-format instead of failing:
# - id: format-justfile
```

## License
Expand Down
31 changes: 27 additions & 4 deletions pre-commit-just.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,42 @@

set -euo pipefail

if ! command -v just /dev/null 2>&1; then
if ! command -v just >/dev/null 2>&1; then
echo >&2 "no just binary found; not running"
exit 0
fi

# With --check, report unformatted or invalid justfiles and fail without
# modifying them (the check-justfile hook). Without it, auto-format in place
# (the format-justfile hook).
check_only=0
if [ "${1:-}" = "--check" ]; then
check_only=1
shift
fi

status=0

for file in "$@"; do
if ! just --fmt --unstable --check -f "$file" >/dev/null 2>&1; then
if just --fmt --unstable --check -f "$file" >/dev/null 2>&1; then
continue
fi

if [ "$check_only" -eq 1 ]; then
# Surface just's own output: a formatting diff, or a parse error such
# as "Extraneous attribute" that --fmt cannot fix.
echo >&2 "error: ${file} is not formatted or is invalid:"
just --fmt --unstable --check -f "$file" >&2 || true
else
echo >&2 "fixing ${file}"
just --fmt --unstable -f "$file" >/dev/null 2>&1
status=1
# If just cannot format the file (e.g. a parse error), let its message
# through instead of hiding it.
if ! just --fmt --unstable -f "$file"; then
echo >&2 "error: could not format ${file} (see above)"
fi
fi

status=1
done

exit $status
227 changes: 227 additions & 0 deletions tests/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
#!/bin/bash
#
# Test suite for pre-commit-just.sh.
#
# Covers both hooks (format and --check) against the three file states just
# cares about: already-formatted, formatted-but-not-canonically, and invalid
# (a parse error --fmt cannot fix). Sample justfiles are generated at runtime
# so this repo's own whitespace hooks can't mangle them.
#
# Run with: tests/run.sh (requires `just` on PATH for all but the first test)

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOOK="${SCRIPT_DIR}/../pre-commit-just.sh"

TMPROOT="$(mktemp -d)"
trap 'rm -rf "$TMPROOT"' EXIT

pass=0
fail=0

_ok() {
printf 'ok - %s\n' "$1"
pass=$((pass + 1))
}

_fail() {
printf 'FAIL - %s\n' "$1"
fail=$((fail + 1))
}

assert_eq() { # expected actual message
if [ "$1" = "$2" ]; then
_ok "$3"
else
_fail "$3 (expected [$1], got [$2])"
fi
}

assert_contains() { # haystack needle message
case "$1" in
*"$2"*) _ok "$3" ;;
*) _fail "$3 (output missing [$2])" ;;
esac
}

assert_unchanged() { # file backup message
if diff -q "$1" "$2" >/dev/null 2>&1; then
_ok "$3"
else
_fail "$3 (file was modified)"
fi
}

assert_changed() { # file backup message
if diff -q "$1" "$2" >/dev/null 2>&1; then
_fail "$3 (file was not modified)"
else
_ok "$3"
fi
}

# --- sample justfile generators -------------------------------------------

write_formatted() { # path
cat >"$1" <<'EOF'
default:
echo "hi"
EOF
}

write_unformatted() { # path
# Valid, but two-space indent and an extra blank line are not what
# `just --fmt` produces, so a fmt check reports a diff.
cat >"$1" <<'EOF'
default:
echo "hi"


build:
echo "build"
EOF
}

write_invalid() { # path
# Duplicate recipe attribute: a parse error --fmt cannot repair.
cat >"$1" <<'EOF'
[private]
[private]
default:
echo "hi"
EOF
}

# Fresh temp file seeded by one of the writers above, plus a .orig backup so
# tests can assert whether the hook modified it. Echoes the file path.
seed() { # writer-fn
local f
f="$(mktemp "$TMPROOT/justfile.XXXXXX")"
"$1" "$f"
cp "$f" "$f.orig"
printf '%s' "$f"
}

# --- tests -----------------------------------------------------------------

test_no_just() {
# command -v just fails under an empty PATH; the hook should no-op.
local f out rc
f="$(seed write_formatted)"
out="$(PATH="" "$HOOK" "$f" 2>&1)"
rc=$?
assert_eq 0 "$rc" "no just binary: exits 0"
assert_contains "$out" "no just binary" "no just binary: prints notice"
assert_unchanged "$f" "$f.orig" "no just binary: leaves file alone"
}

test_check_formatted() {
local f out rc
f="$(seed write_formatted)"
out="$("$HOOK" --check "$f" 2>&1)"
rc=$?
assert_eq 0 "$rc" "check/formatted: exits 0"
assert_unchanged "$f" "$f.orig" "check/formatted: leaves file alone"
}

test_check_unformatted() {
local f out rc
f="$(seed write_unformatted)"
out="$("$HOOK" --check "$f" 2>&1)"
rc=$?
assert_eq 1 "$rc" "check/unformatted: exits 1"
assert_unchanged "$f" "$f.orig" "check/unformatted: does NOT modify the file"
assert_contains "$out" "is not formatted or is invalid" \
"check/unformatted: surfaces the error header"
}

test_check_invalid() {
local f out rc
f="$(seed write_invalid)"
out="$("$HOOK" --check "$f" 2>&1)"
rc=$?
assert_eq 1 "$rc" "check/invalid: exits 1"
assert_unchanged "$f" "$f.orig" "check/invalid: does NOT modify the file"
assert_contains "$out" "private" "check/invalid: surfaces just's parse error"
}

test_format_formatted() {
local f out rc
f="$(seed write_formatted)"
out="$("$HOOK" "$f" 2>&1)"
rc=$?
assert_eq 0 "$rc" "format/formatted: exits 0"
assert_unchanged "$f" "$f.orig" "format/formatted: leaves file alone"
}

test_format_unformatted() {
local f out rc
f="$(seed write_unformatted)"
out="$("$HOOK" "$f" 2>&1)"
rc=$?
# Non-zero on purpose: the file was rewritten, so pre-commit reports it.
assert_eq 1 "$rc" "format/unformatted: exits 1 to flag the change"
assert_changed "$f" "$f.orig" "format/unformatted: rewrites the file in place"
assert_contains "$out" "fixing" "format/unformatted: announces the fix"
if just --fmt --unstable --check -f "$f" >/dev/null 2>&1; then
_ok "format/unformatted: result is now canonically formatted"
else
_fail "format/unformatted: result still fails a fmt check"
fi
}

test_format_invalid() {
local f out rc
f="$(seed write_invalid)"
out="$("$HOOK" "$f" 2>&1)"
rc=$?
assert_eq 1 "$rc" "format/invalid: exits 1"
assert_unchanged "$f" "$f.orig" "format/invalid: cannot fix, leaves file alone"
assert_contains "$out" "could not format" "format/invalid: reports the failure"
}

test_multiple_mixed() {
# A good file alongside a bad one: overall failure, both processed.
local good bad out rc
good="$(seed write_formatted)"
bad="$(seed write_unformatted)"
out="$("$HOOK" --check "$good" "$bad" 2>&1)"
rc=$?
assert_eq 1 "$rc" "multiple/mixed: exits 1 when any file fails"
assert_unchanged "$good" "$good.orig" "multiple/mixed: good file untouched"
assert_contains "$out" "$bad" "multiple/mixed: names the failing file"
}

test_no_files() {
local out rc
out="$("$HOOK" 2>&1)"
rc=$?
assert_eq 0 "$rc" "no file args: exits 0"

out="$("$HOOK" --check 2>&1)"
rc=$?
assert_eq 0 "$rc" "--check with no file args: exits 0"
}

# --- runner ----------------------------------------------------------------

echo "# pre-commit-just.sh"

test_no_just

if command -v just >/dev/null 2>&1; then
test_check_formatted
test_check_unformatted
test_check_invalid
test_format_formatted
test_format_unformatted
test_format_invalid
test_multiple_mixed
test_no_files
else
echo "# skipping just-dependent tests: no just binary on PATH"
fi

echo "# ${pass} passed, ${fail} failed"
[ "$fail" -eq 0 ]
Loading