Add an opt-in pre-commit hook - #415
Conversation
Closes #383. Runs the same three checks CI runs — shellcheck, SwiftFormat, SwiftLint — against staged files only, so it stays fast and does not lint the whole tree on every commit. Opt-in via `git config core.hooksPath .githooks`, documented in CONTRIBUTING. Nothing installs it automatically: a repository that silently takes over your git config is worse than one that asks. It lints rather than formatting in place. A hook that rewrites files underneath you produces commits whose contents you never read. Each tool is skipped with a note if it is not installed, so the hook is usable before `brew install shellcheck swiftformat swiftlint` and never blocks a contributor who has not set the tooling up yet. Verified all four behaviours by running it: a staged file with a formatting violation is blocked, a clean file commits, --no-verify bypasses, and a docs-only commit skips both linters entirely. The hook also passes shellcheck itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe change adds an optional Git pre-commit hook. The hook checks staged shell and Swift files with available lint tools, reports failures, and documents setup, behavior, and bypass options. ChangesPre-commit validation
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant Git
participant PreCommitHook
participant ShellCheck
participant SwiftFormat
participant SwiftLint
Git->>PreCommitHook: Run pre-commit hook
PreCommitHook->>ShellCheck: Check staged .sh files
PreCommitHook->>SwiftFormat: Lint staged Swift files
PreCommitHook->>SwiftLint: Lint staged Swift files
PreCommitHook->>Git: Return success or failure status
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
CONTRIBUTING.md (1)
71-73: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy liftDocument the CI tool versions and sources.
CI installs unversioned
swiftformatandswiftlintwith Homebrew, but it relies on a runner-providedshellcheck.Package.swiftdoes not control the CI formatter. Pin the tools or document this provisioning policy.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CONTRIBUTING.md` around lines 71 - 73, Update the CI tooling documentation around the Homebrew install command to state the provisioning source and versions for swiftformat and swiftlint, and clarify that shellcheck is supplied by the runner rather than installed there. If versions cannot be pinned, explicitly document that these tools are intentionally provisioned unversioned by Homebrew and runner defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.githooks/pre-commit:
- Around line 15-18: Align the pre-commit hook’s staged Swift-file selector with
CI by excluding Sources/XCTestHTMLReportCore/Classes/HTMLTemplates.swift, while
preserving the existing *.sh ShellCheck selector. Update the relevant
staged-file filtering logic in the hook and retain the exclusion consistently
with .swiftformat and .swiftlint.yml.
- Line 43: Update the SwiftLint invocation in the pre-commit hook to include
--force-exclude so configured exclusions from .swiftlint.yml apply to staged
Swift paths. Preserve xargs handling for multiple paths, and treat SwiftLint’s
“No lintable files found” result as success when all staged files are excluded,
while retaining failure behavior for genuine lint errors.
- Line 10: Update the pre-commit lint flow around staged() so ShellCheck,
SwiftFormat, and SwiftLint analyze the staged index snapshot rather than
working-tree paths; materialize each index blob through temporary files or
stdin, or reject files that also have unstaged changes. Add a regression test
covering staged-clean content with conflicting bad working-tree content.
---
Nitpick comments:
In `@CONTRIBUTING.md`:
- Around line 71-73: Update the CI tooling documentation around the Homebrew
install command to state the provisioning source and versions for swiftformat
and swiftlint, and clarify that shellcheck is supplied by the runner rather than
installed there. If versions cannot be pinned, explicitly document that these
tools are intentionally provisioned unversioned by Homebrew and runner defaults.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e7fc47ba-4122-434f-875c-24c8486406eb
📒 Files selected for processing (2)
.githooks/pre-commitCONTRIBUTING.md
| # Skip a single commit with: git commit --no-verify | ||
| set -uo pipefail | ||
|
|
||
| staged() { git diff --cached --name-only --diff-filter=ACMR -z -- "$@"; } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- hook files ---'
git ls-files '.githooks/pre-commit' '.githooks/*' '.github/*' 2>/dev/null || true
printf '%s\n' '--- pre-commit outline ---'
ast-grep outline .githooks/pre-commit 2>/dev/null || true
printf '%s\n' '--- pre-commit contents ---'
cat -n .githooks/pre-commit
printf '%s\n' '--- related staged/path handling ---'
rg -n -C 4 'staged\(|swiftformat|swiftlint|SwiftFormat|SwiftLint|diff-filter|git diff --cached' .githooks .github . 2>/dev/null | head -n 500Repository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 8080
🏁 Script executed:
#!/bin/bash
set -eu
cat -n .githooks/pre-commit
rg -n -C 5 'staged\(|swiftformat|swiftlint|SwiftFormat|SwiftLint|git diff --cached' .githooks .github 2>/dev/null || trueRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 5881
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
p = Path(".githooks/pre-commit")
text = p.read_text()
checks = {
"staged names only": "git diff --cached --name-only -z" in text,
"shellcheck receives staged() output": bool(re.search(r"staged '\*\.sh'.*\|\s*xargs -0 shellcheck", text)),
"SwiftFormat receives staged() output": bool(re.search(r"staged '\*\.swift'.*\|\s*xargs -0 swiftformat", text)),
"SwiftLint receives staged() output": bool(re.search(r"staged '\*\.swift'.*\|\s*xargs -0 swiftlint", text)),
"index-content read mechanism": any(x in text for x in ("git show :", "git cat-file", "git checkout-index", "mktemp", "stdin")),
}
for name, value in checks.items():
print(f"{name}: {value}")
assert checks["staged names only"]
assert checks["shellcheck receives staged() output"]
assert checks["SwiftFormat receives staged() output"]
assert checks["SwiftLint receives staged() output"]
assert not checks["index-content read mechanism"]
PY
printf '%s\n' '--- current staged Swift paths (NUL-safe) ---'
git diff --cached --name-only --diff-filter=ACMR -z -- '*.swift' |
python3 -c 'import sys; print(repr(sys.stdin.buffer.read()))'
printf '%s\n' '--- staged versus working-tree entries, if any ---'
git status --short
git diff --cached --name-status --diff-filter=ACMR -- '*.swift' || true
git diff --name-status -- '*.swift' || trueRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 447
🌐 Web query:
SwiftFormat Git hook staged files working tree content official documentation
💡 Result:
The official documentation for SwiftFormat recommends using a Git pre-commit hook to format only staged files by utilizing the git-format-staged tool [1][2]. This approach prevents formatting unstaged changes or creating inconsistent states in the Git index [3]. To implement this, follow these steps: 1. Install the SwiftFormat command-line tool [1]. 2. Install git-format-staged [1]. 3. Create or edit the .git/hooks/pre-commit file in your repository [1]. 4. Add the following configuration to the file [1]: #!/bin/bash git-format-staged --formatter "swiftformat stdin --stdin-path '{}'" "*.swift" 5. Make the hook executable by running chmod +x .git/hooks/pre-commit in your terminal [1]. Important Considerations: - Stdin Usage: The use of --stdin-path is critical [4]. Because git-format-staged passes file content via stdin, SwiftFormat requires this argument to correctly identify the file path and apply project-specific configurations (such as ignores or exclusions) [4][5]. - Versioning: Git hooks are stored in the local .git directory and are not automatically checked into source control [1][2]. For collaborative teams, you may need a mechanism (like an install script) to ensure all developers have the hook configured, or consider running formatting checks on your CI server instead [1][6]. - GUI Clients: If you use a Git GUI client, you may need to perform additional configuration steps to ensure the hook is triggered correctly [1]. If you need to explicitly reference a configuration file within the hook, you can include the --config flag within the formatter string: git-format-staged --formatter "swiftformat stdin --stdin-path '{}' --config .swiftformat" "*.swift" [4][5].
Citations:
- 1: https://github.com/nicklockwood/swiftformat
- 2: https://github.com/nicklockwood/SwiftFormat/blob/master/README.md
- 3: Update instructions for git pre-commit hook nicklockwood/SwiftFormat#617
- 4: exclude option from config file is ignored when run for single file with githook nicklockwood/SwiftFormat#374
- 5: Running pre-commit hook from Xcode nicklockwood/SwiftFormat#670
- 6: https://blog.eidinger.info/automatically-format-your-swift-code-when-committing-your-work
🌐 Web query:
SwiftLint command line lint explicit file paths official documentation
💡 Result:
To lint explicit files with SwiftLint via the command line, you can provide the file paths directly as arguments to the lint command [1]. When no paths are specified, SwiftLint defaults to linting the current working directory [1]. Example command: swiftlint lint path/to/file1.swift path/to/file2.swift For specialized use cases, such as linting a list of files modified by Xcode or Git, SwiftLint provides options to read file paths from environment variables: --use-script-input-files: Uses environment variables (SCRIPT_INPUT_FILE_COUNT and SCRIPT_INPUT_FILE_0, etc.) to define the files to lint [2][3]. --use-script-input-file-lists: Uses environment variables (SCRIPT_INPUT_FILE_LIST_COUNT and SCRIPT_INPUT_FILE_LIST_0, etc.) to read file lists [3][4]. Note that when providing explicit paths, behavior can be influenced by your.swiftlint.yml configuration. Specifically, if you have an included property defined in your configuration file, SwiftLint may ignore directory paths passed on the command line [2][5]. Additionally, using the --force-exclude flag will cause SwiftLint to exclude files listed in your configuration's excluded property even if they are explicitly passed as arguments [6].
Citations:
- 1: https://github.com/realm/SwiftLint/blob/master/Source/swiftlint/Commands/Lint.swift
- 2: https://github.com/realm/Swiftlint
- 3: https://realm.github.io/SwiftLint/index.html
- 4: https://realm.github.io/SwiftLint/
- 5: https://context7.com/realm/swiftlint/llms.txt
- 6: https://github.com/realm/SwiftLint/blob/main/Source/swiftlint/Common/LintOrAnalyzeArguments.swift
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
text = Path(".githooks/pre-commit").read_text()
for label, pattern in {
"staged names only": r"git diff --cached --name-only -z",
"ShellCheck gets working-tree paths": r"staged '\*\.sh'.*\|\s*xargs -0 shellcheck",
"SwiftFormat gets working-tree paths": r"staged '\*\.swift'.*\|\s*xargs -0 swiftformat",
"SwiftLint gets working-tree paths": r"staged '\*\.swift'.*\|\s*xargs -0 swiftlint",
}.items():
print(f"{label}: {bool(re.search(pattern, text))}")
print("index-content handling:",
any(token in text for token in
("git show :", "git cat-file", "git checkout-index", "mktemp")))
PY
git status --short
git diff --cached --name-status --diff-filter=ACMR -- '*.swift' || true
git diff --name-status -- '*.swift' || trueRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 349
🌐 Web query:
SwiftFormat official Git hook staged files working tree content
💡 Result:
To run SwiftFormat on only your staged files as a Git pre-commit hook, the official documentation and community consensus recommend using the tool git-format-staged [1][2]. Using this tool prevents issues where formatting partially staged files results in committing both staged and unstaged changes [3]. Installation and Setup: 1. Ensure the SwiftFormat command-line tool is installed [1][2]. 2. Install git-format-staged (e.g., via npm or as a standalone script) [1][2][4]. 3. Navigate to your project folder and create or edit the file at.git/hooks/pre-commit [1][2]. 4. Add the following content to that file: #!/bin/bash git-format-staged --formatter "swiftformat stdin --stdin-path '{}'" "*.swift" 5. Make the hook executable by running: chmod +x.git/hooks/pre-commit [1][2]. This configuration ensures that only the staged portions of your.swift files are formatted [3][2]. When you run git commit, SwiftFormat will process the staged content; running git commit --no-verify will bypass this hook if necessary [1][2]. Note that Git hooks stored in.git/hooks are not automatically tracked by version control [1][2]. For collaborative projects, teams often use a script to install these hooks or rely on CI/CD pipelines to enforce formatting [1][2][4]. Alternative approaches, such as manually iterating over staged files with a loop and re-staging them, are possible but less robust compared to using git-format-staged, which is designed specifically to handle the complexities of the git index [5][6].
Citations:
- 1: https://github.com/nicklockwood/SwiftFormat?tab=readme-ov-file
- 2: https://github.com/nicklockwood/SwiftFormat/blob/master/README.md
- 3: Update instructions for git pre-commit hook nicklockwood/SwiftFormat#617
- 4: https://blog.eidinger.info/automatically-format-your-swift-code-when-committing-your-work
- 5: https://dev.to/theagilemonkeys/setting-up-a-code-formatter-in-a-ios-project-550g
- 6: https://useyourloaf.com/blog/format-swift-with-a-git-commit-hook/
🌐 Web query:
SwiftLint official command line explicit file paths lint
💡 Result:
To lint specific files or directories using SwiftLint from the command line, you can provide the file paths as arguments directly to the swiftlint or swiftlint lint command [1][2]. Usage example: swiftlint lint Path/To/File.swift Path/To/AnotherFile.swift Path/To/Directory/ If no paths are specified, SwiftLint defaults to linting the current working directory [1]. Key considerations: 1. Argument Handling: When you pass explicit file paths to the command line, SwiftLint typically lints those files directly [3][4]. 2. Configuration Files: If you provide a configuration file using the --config flag, it is intended to act as an override for the default.swiftlint.yml configuration [5][6]. 3. Script Input Files: For CI/CD workflows or complex build phases (such as when integrating with Xcode's file lists or git output), you can use the --use-script-input-files or --use-script-input-file-lists flags [5][2]. This allows you to provide a list of files via environment variables (e.g., SCRIPT_INPUT_FILE_0, SCRIPT_INPUT_FILE_1) instead of command-line arguments [5][7]. 4. Behavior with 'Included' paths: Be aware that if your.swiftlint.yml file contains an explicit included: key, some versions of SwiftLint may ignore directory paths provided on the command line in favor of the paths defined in the configuration file [5][8]. For common use cases like linting only changed files, you can use shell expansion to pass file paths dynamically: swiftlint lint $(git diff --name-only --diff-filter=ACM "*.swift") [4]
Citations:
- 1: https://github.com/realm/SwiftLint/blob/master/Source/swiftlint/Commands/Lint.swift
- 2: https://github.com/realm/SwiftLint/blob/5d6e25ae5fb25a24eafa7d2d3d8d0d2efcef46bf/README.md
- 3: Processing of command line paths is unintuitive and confusing realm/SwiftLint#4823
- 4: How to lint only changed files? realm/SwiftLint#5834
- 5: https://realm.github.io/SwiftLint/index.html
- 6: https://github.com/realm/SwiftLint/tree/refs/heads/main
- 7: https://github.com/realm/SwiftLint/
- 8: https://realm.github.io/SwiftLint/
Lint the staged snapshot, not the working tree.
staged() returns path names only. ShellCheck, SwiftFormat, and SwiftLint then read those paths from the working tree. Read each index blob through a temporary file or stdin path, or reject files with unstaged changes. Add a regression test for staged-clean and working-tree-bad content.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.githooks/pre-commit at line 10, Update the pre-commit lint flow around
staged() so ShellCheck, SwiftFormat, and SwiftLint analyze the staged index
snapshot rather than working-tree paths; materialize each index blob through
temporary files or stdin, or reject files that also have unstaged changes. Add a
regression test covering staged-clean content with conflicting bad working-tree
content.
| if [[ -n "$(staged '*.sh' | tr -d '\0')" ]]; then | ||
| if command -v shellcheck >/dev/null; then | ||
| note "shellcheck" | ||
| staged '*.sh' | xargs -0 shellcheck || fail=1 |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 6 'shellcheck|swiftformat|swiftlint|\.sh|\.swift' .Repository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 50391
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- tracked workflow and hook files ---'
git ls-files '.github/**' '.githooks/**' '*workflow*' '*ci*' | sed -n '1,160p'
printf '%s\n' '--- workflow and hook references ---'
find .github .githooks -type f -maxdepth 3 -print 2>/dev/null | while IFS= read -r file; do
printf '\n### %s\n' "$file"
rg -n -C 5 'shellcheck|swiftformat|swiftlint|staged|find |git diff|\.sh|\.swift|paths:|paths-ignore:' "$file" || true
doneRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 6675
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- repository configuration files ---'
git ls-files -z | python3 - <<'PY'
import sys
paths = sys.stdin.buffer.read().split(b'\0')
for raw in paths:
path = raw.decode()
name = path.rsplit('/', 1)[-1]
if name in {'.swiftformat', '.swiftlint.yml', '.swiftlint.yaml', '.gitignore'} or 'workflow' in path:
print(path)
PY
printf '%s\n' '--- relevant configuration contents ---'
for file in .swiftformat .swiftlint.yml .swiftlint.yaml .gitignore; do
if [ -f "$file" ]; then
printf '\n### %s\n' "$file"
cat -n "$file"
fi
done
printf '%s\n' '--- tracked files relevant to selector boundaries ---'
git ls-files -z | python3 - <<'PY'
import os, stat, sys
paths = [p.decode() for p in sys.stdin.buffer.read().split(b'\0') if p]
for path in paths:
base = path.rsplit('/', 1)[-1]
if base.endswith(('.sh', '.bash', '.zsh', '.ksh', '.swift')) or '.' not in base:
mode = os.stat(path).st_mode
executable = bool(mode & stat.S_IXUSR)
print(f"{path}\t{'executable' if executable else 'non-executable'}")
PY
printf '%s\n' '--- workflow files with complete lint sections ---'
cat -n .github/workflows/lint.ymlRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 214
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files -z > /tmp/xctesthtmlreport-files
printf '%s\n' '--- repository configuration files ---'
python3 - <<'PY'
paths = [p.decode() for p in open('/tmp/xctesthtmlreport-files', 'rb').read().split(b'\0') if p]
for path in paths:
name = path.rsplit('/', 1)[-1]
if name in {'.swiftformat', '.swiftlint.yml', '.swiftlint.yaml', '.gitignore'} or 'workflow' in path:
print(path)
PY
printf '%s\n' '--- relevant configuration contents ---'
for file in .swiftformat .swiftlint.yml .swiftlint.yaml .gitignore; do
if [ -f "$file" ]; then
printf '\n### %s\n' "$file"
cat -n "$file"
fi
done
printf '%s\n' '--- tracked files relevant to selector boundaries ---'
python3 - <<'PY'
import os, stat
paths = [p.decode() for p in open('/tmp/xctesthtmlreport-files', 'rb').read().split(b'\0') if p]
for path in paths:
base = path.rsplit('/', 1)[-1]
if base.endswith(('.sh', '.bash', '.zsh', '.ksh', '.swift')) or '.' not in base:
mode = os.stat(path).st_mode
executable = bool(mode & stat.S_IXUSR)
print(f"{path}\t{'executable' if executable else 'non-executable'}")
PY
printf '%s\n' '--- lint workflow ---'
cat -n .github/workflows/lint.ymlRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 10243
Align the hook selectors with CI.
CI checks only *.sh files for ShellCheck, so extensionless and other shell extensions are not missed by the selector. However, CI excludes Sources/XCTestHTMLReportCore/Classes/HTMLTemplates.swift through both .swiftformat and .swiftlint.yml, while the hook selects every staged *.swift path. Preserve the generated-file exclusion in the hook, or document why the hook intentionally differs from CI.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.githooks/pre-commit around lines 15 - 18, Align the pre-commit hook’s
staged Swift-file selector with CI by excluding
Sources/XCTestHTMLReportCore/Classes/HTMLTemplates.swift, while preserving the
existing *.sh ShellCheck selector. Update the relevant staged-file filtering
logic in the hook and retain the exclusion consistently with .swiftformat and
.swiftlint.yml.
| note "swiftlint" | ||
| # SwiftLint reads .swiftlint.yml for its own include/exclude rules, so pass | ||
| # paths explicitly to keep the hook scoped to what is being committed. | ||
| staged '*.swift' | xargs -0 swiftlint lint --quiet || fail=1 |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
command -v swiftlint >/dev/null
swiftlint lint --help | grep -q -- '--use-script-input-files'
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
printf 'struct One { let value = 1 }\n' > "$tmp/One.swift"
printf 'struct Two { let value = 2 }\n' > "$tmp/Two.swift"
(
cd "$tmp"
swiftlint lint --quiet "$tmp/One.swift" "$tmp/Two.swift"
)Repository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 803
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- hook and configuration files ---'
git ls-files '.githooks/pre-commit' '.swiftlint.yml' '.swiftlint.yaml' 'Package.swift' 'README.md'
printf '%s\n' '--- pre-commit context ---'
sed -n '1,90p' .githooks/pre-commit
printf '%s\n' '--- SwiftLint references ---'
rg -n --hidden --glob '!/.git/**' 'swiftlint|SCRIPT_INPUT_FILE|force-exclude|staged' .
printf '%s\n' '--- SwiftLint version and supported flags ---'
swiftlint version 2>&1 || true
swiftlint lint --help 2>&1 | grep -E -- '--use-script-input-files|--force-exclude|Usage|CONFIGURATION' || true
printf '%s\n' '--- SwiftLint configuration ---'
for f in .swiftlint.yml .swiftlint.yaml; do
if [ -f "$f" ]; then
cat -n "$f"
fi
doneRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 5120
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- full lint usage and relevant options ---'
swiftlint lint --help 2>&1 | sed -n '1,220p'
printf '%s\n' '--- controlled positional-path test ---'
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/Sources"
printf 'struct One { let value = 1 }\n' > "$tmp/Sources/One.swift"
printf 'struct Two { let value = 2 }\n' > "$tmp/Sources/Two.swift"
cat > "$tmp/.swiftlint.yml" <<'YAML'
included:
- Sources
disabled_rules:
- statement_position
YAML
(
cd "$tmp"
swiftlint lint --no-cache --quiet "$tmp/Sources/One.swift" "$tmp/Sources/Two.swift" 2>&1
)
positional_status=$?
printf 'positional_status=%s\n' "$positional_status"
printf '%s\n' '--- controlled script-input-file test ---'
(
cd "$tmp"
export SCRIPT_INPUT_FILE_COUNT=2
export SCRIPT_INPUT_FILE_0="$tmp/Sources/One.swift"
export SCRIPT_INPUT_FILE_1="$tmp/Sources/Two.swift"
swiftlint lint --no-cache --quiet --use-script-input-files 2>&1
)
script_input_status=$?
printf 'script_input_status=%s\n' "$script_input_status"
printf '%s\n' '--- force-exclude behavior with an explicitly passed excluded file ---'
printf 'struct Excluded { let value = 1 }\n' > "$tmp/Sources/Excluded.swift"
cat > "$tmp/.swiftlint.yml" <<'YAML'
included:
- Sources
excluded:
- Sources/Excluded.swift
line_length:
error: 1
disabled_rules:
- statement_position
YAML
(
cd "$tmp"
swiftlint lint --no-cache --quiet "$tmp/Sources/Excluded.swift" 2>&1
)
without_force_exclude_status=$?
printf 'without_force_exclude_status=%s\n' "$without_force_exclude_status"
(
cd "$tmp"
swiftlint lint --no-cache --quiet --force-exclude "$tmp/Sources/Excluded.swift" 2>&1
)
with_force_exclude_status=$?
printf 'with_force_exclude_status=%s\n' "$with_force_exclude_status"Repository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 4081
🏁 Script executed:
#!/bin/bash
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/Sources"
printf 'struct Included { let value = 1 }\n' > "$tmp/Sources/Included.swift"
printf 'struct Excluded { let value = 1 }\n' > "$tmp/Sources/Excluded.swift"
cat > "$tmp/.swiftlint.yml" <<'YAML'
included:
- Sources
excluded:
- Sources/Excluded.swift
line_length:
error: 1
disabled_rules:
- statement_position
YAML
printf '%s\n' '--- mixed explicit paths with --force-exclude ---'
(
cd "$tmp"
swiftlint lint --no-cache --quiet --force-exclude \
"$tmp/Sources/Included.swift" "$tmp/Sources/Excluded.swift" 2>&1
)
printf 'mixed_status=%s\n' "$?"
printf '%s\n' '--- only explicitly excluded path with --force-exclude ---'
(
cd "$tmp"
swiftlint lint --no-cache --quiet --force-exclude \
"$tmp/Sources/Excluded.swift" 2>&1
)
printf 'excluded_only_status=%s\n' "$?"Repository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 564
Apply configured exclusions to staged paths.
SwiftLint supports multiple positional paths, so xargs is valid. Add --force-exclude to honor .swiftlint.yml exclusions. Handle the No lintable files found result as success when all staged Swift files are excluded.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.githooks/pre-commit at line 43, Update the SwiftLint invocation in the
pre-commit hook to include --force-exclude so configured exclusions from
.swiftlint.yml apply to staged Swift paths. Preserve xargs handling for multiple
paths, and treat SwiftLint’s “No lintable files found” result as success when
all staged files are excluded, while retaining failure behavior for genuine lint
errors.
Closes #383. Last of the maintenance-automation batch (#402, #410, #413 preceding).
Runs the same three checks CI runs — shellcheck, SwiftFormat, SwiftLint — against staged files only, so it stays fast rather than linting the whole tree on every commit.
Three deliberate design choices
Opt-in, not automatic. Enabled with
git config core.hooksPath .githooks, documented in CONTRIBUTING. Nothing installs it for you — a repository that silently takes over your git config is worse than one that asks.Lints rather than formatting in place. A hook that rewrites files underneath you produces commits whose contents you never read. It tells you to run
swiftformat .instead.Degrades rather than blocking. Each tool is skipped with a note if it is not installed, so a contributor who has not run
brew install shellcheck swiftformat swiftlintyet can still commit.Verified by running it, not by reading it
.swiftwith a formatting violationgit logconfirmed no commit landed.swiftgit commit --no-verifyThe hook also passes
shellcheckitself, which felt like the minimum for a script whose job is running shellcheck.🤖 Generated with Claude Code
Summary by CodeRabbit
Chores
Documentation