Repository navigation
Add an opt-in pre-commit hook #415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| #!/bin/bash | ||
| # Runs the same checks CI runs, on staged files only. | ||
| # | ||
| # Not installed automatically — see CONTRIBUTING.md. A repository that silently | ||
| # takes over your git config is worse than one that asks. | ||
| # | ||
| # Skip a single commit with: git commit --no-verify | ||
| set -uo pipefail | ||
|
|
||
| staged() { git diff --cached --name-only --diff-filter=ACMR -z -- "$@"; } | ||
| fail=0 | ||
| note() { printf '\n\033[1m%s\033[0m\n' "$1"; } | ||
|
|
||
| # --- shell --------------------------------------------------------------- | ||
| if [[ -n "$(staged '*.sh' | tr -d '\0')" ]]; then | ||
| if command -v shellcheck >/dev/null; then | ||
| note "shellcheck" | ||
| staged '*.sh' | xargs -0 shellcheck || fail=1 | ||
|
Comment on lines
+15
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ 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 🤖 Prompt for AI Agents |
||
| else | ||
| echo "shellcheck not installed; skipping (brew install shellcheck)" | ||
| fi | ||
| fi | ||
|
|
||
| # --- swift --------------------------------------------------------------- | ||
| swift_staged="$(staged '*.swift' | tr -d '\0')" | ||
| if [[ -n "$swift_staged" ]]; then | ||
| if command -v swiftformat >/dev/null; then | ||
| note "swiftformat" | ||
| # --lint rather than formatting in place: a hook that rewrites files under | ||
| # you produces commits whose contents you did not read. | ||
| staged '*.swift' | xargs -0 swiftformat --lint || { | ||
| echo "run 'swiftformat .' to fix, then re-stage" | ||
| fail=1 | ||
| } | ||
| else | ||
| echo "swiftformat not installed; skipping (brew install swiftformat)" | ||
| fi | ||
|
|
||
| if command -v swiftlint >/dev/null; then | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 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 🤖 Prompt for AI Agents |
||
| else | ||
| echo "swiftlint not installed; skipping (brew install swiftlint)" | ||
| fi | ||
| fi | ||
|
|
||
| if [[ $fail -ne 0 ]]; then | ||
| printf '\n\033[31mpre-commit checks failed.\033[0m Fix the above, or commit with --no-verify.\n' | ||
| exit 1 | ||
| fi | ||
| exit 0 | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 8080
🏁 Script executed:
Repository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 5881
🏁 Script executed:
Repository: 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-stagedtool [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. Installgit-format-staged[1]. 3. Create or edit the.git/hooks/pre-commitfile 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 runningchmod +x .git/hooks/pre-commitin your terminal [1]. Important Considerations: - Stdin Usage: The use of--stdin-pathis critical [4]. Becausegit-format-stagedpasses 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.gitdirectory 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--configflag within the formatter string:git-format-staged --formatter "swiftformat stdin --stdin-path '{}' --config .swiftformat" "*.swift"[4][5].Citations:
🌐 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:
🏁 Script executed:
Repository: 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:
🌐 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
swiftlintorswiftlint lintcommand [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 orgitoutput), 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:
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