forked from mirrors/kingfisher
Updated precommit behavior and docs
This commit is contained in:
parent
356ecc5748
commit
f1a77a736c
23 changed files with 933 additions and 14 deletions
18
.pre-commit-hooks.yaml
Normal file
18
.pre-commit-hooks.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
- id: kingfisher-docker
|
||||
name: kingfisher (docker)
|
||||
description: Run Kingfisher in Docker against staged changes at the repository root. No local install required.
|
||||
entry: ghcr.io/kingfisher-sec/kingfisher:latest
|
||||
language: docker
|
||||
args: ["scan", ".", "--staged", "--quiet", "--redact", "--only-valid", "--no-update-check"]
|
||||
pass_filenames: false
|
||||
stages: [commit]
|
||||
|
||||
- id: kingfisher
|
||||
name: kingfisher
|
||||
description: Scan staged changes with the locally installed Kingfisher binary.
|
||||
entry: kingfisher
|
||||
language: system
|
||||
args: ["scan", ".", "--staged", "--quiet", "--redact", "--only-valid", "--no-update-check"]
|
||||
pass_filenames: false
|
||||
types: [file]
|
||||
stages: [commit]
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v1.70.0]
|
||||
- Added new rules for AWS Bedrock, Voyage.ai, Posthog
|
||||
- Added `--staged` argument to support new `pre-commit` behavior and added integration coverage to ensure validated secrets block commits when used as pre-commit hook
|
||||
- Added new rules for AWS Bedrock, Voyage.ai, Posthog, Atlassian
|
||||
- Added a `kingfisher view` subcommand that serves the bundled access-map HTML viewer from the binary so users can load JSON or JSONL reports passed on the CLI (or upload them in the browser) over a configurable local-only port.
|
||||
|
||||
## [v1.69.0]
|
||||
|
|
|
|||
131
README.md
131
README.md
|
|
@ -64,6 +64,10 @@ See ([docs/COMPARISON.md](docs/COMPARISON.md))
|
|||
- [Homebrew](#homebrew)
|
||||
- [Linux and macOS](#linux-and-macos)
|
||||
- [Windows](#windows)
|
||||
- [Pre-commit hooks](#pre-commit-hooks)
|
||||
- [macOS and Linux](#macos-and-linux)
|
||||
- [Windows PowerShell](#windows-powershell)
|
||||
- [Using the `pre-commit` framework](#using-the-pre-commit-framework)
|
||||
- [Compile](#compile)
|
||||
- [ Run Kingfisher in Docker](#-run-kingfisher-in-docker)
|
||||
- [🔐 Detection Rules at a Glance](#-detection-rules-at-a-glance)
|
||||
|
|
@ -216,6 +220,133 @@ You can provide a custom destination using the `-InstallDir` parameter:
|
|||
</details>
|
||||
|
||||
|
||||
### Pre-commit hooks
|
||||
|
||||
Install a Git pre-commit hook to block commits that introduce new secrets.
|
||||
|
||||
The installer:
|
||||
|
||||
- Preserves any existing `pre-commit` hook by chaining it **before** Kingfisher.
|
||||
- Supports custom hook directories via `--hooks-path` (or Git’s `core.hooksPath`).
|
||||
- Can be installed either **per-repository** or as a **global** hook.
|
||||
|
||||
#### macOS and Linux
|
||||
|
||||
<details>
|
||||
|
||||
Install a **per-repository** hook from the root of the repo you want to protect:
|
||||
|
||||
```bash
|
||||
curl --silent --location \
|
||||
https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-kingfisher-pre-commit.sh | \
|
||||
bash
|
||||
```
|
||||
|
||||
Uninstall from that repository:
|
||||
|
||||
```bash
|
||||
curl --silent --location \
|
||||
https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-kingfisher-pre-commit.sh | \
|
||||
bash -s -- --uninstall
|
||||
```
|
||||
|
||||
Install as a **global** pre-commit hook (using core.hooksPath):
|
||||
|
||||
```bash
|
||||
curl --silent --location \
|
||||
https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-kingfisher-pre-commit.sh | \
|
||||
bash -s -- --global
|
||||
```
|
||||
|
||||
Uninstall the **global** hook:
|
||||
|
||||
```bash
|
||||
curl --silent --location \
|
||||
https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-kingfisher-pre-commit.sh | \
|
||||
bash -s -- --global --uninstall
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Windows PowerShell
|
||||
|
||||
<details>
|
||||
|
||||
Install a **per-repository** hook from the root of the target repo:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
|
||||
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-kingfisher-pre-commit.ps1' -OutFile install-kingfisher-pre-commit.ps1
|
||||
./install-kingfisher-pre-commit.ps1
|
||||
```
|
||||
|
||||
Uninstall from that repository:
|
||||
|
||||
```powershell
|
||||
./install-kingfisher-pre-commit.ps1 -Uninstall
|
||||
```
|
||||
|
||||
Install as a **global** hook (using core.hooksPath):
|
||||
|
||||
```powershell
|
||||
./install-kingfisher-pre-commit.ps1 -Global
|
||||
```
|
||||
|
||||
Uninstall the **global** hook:
|
||||
```powershell
|
||||
./install-kingfisher-pre-commit.ps1 -Global -Uninstall
|
||||
```
|
||||
|
||||
> The installer automatically runs any existing `pre-commit` hook first, then
|
||||
> executes `kingfisher scan . --staged --quiet --redact --only-valid --no-update-check`
|
||||
> against the staged diff (anchored to `HEAD` when no commits exist yet).
|
||||
|
||||
</details>
|
||||
|
||||
#### Using the `pre-commit` framework
|
||||
|
||||
Add Kingfisher as a hook in your `.pre-commit-config.yaml`:
|
||||
|
||||
<details>
|
||||
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/mongodb/kingfisher
|
||||
rev: <version-or-commit>
|
||||
hooks:
|
||||
# No local install required; runs Kingfisher from Docker at the repo root
|
||||
- id: kingfisher-docker
|
||||
|
||||
# Fastest when you already have Kingfisher installed locally
|
||||
- id: kingfisher
|
||||
```
|
||||
|
||||
Then install the hook via `pre-commit install`. Every hook now drives Kingfisher
|
||||
directly with the built-in `--staged` flag:
|
||||
|
||||
```bash
|
||||
kingfisher scan . --staged --quiet --redact --only-valid --no-update-check
|
||||
```
|
||||
|
||||
When `--staged` is set, Kingfisher snapshots the staged index into a temporary
|
||||
commit, diffs it against `HEAD` (or an empty tree if no commits exist yet), and
|
||||
scans only those staged changes. This mirrors how gitleaks and TruffleHog handle
|
||||
pre-commit workflows while keeping everything inside the Kingfisher binary.
|
||||
|
||||
> Exit codes: Kingfisher exits `0` when no findings are present and returns
|
||||
> `205` when validated credentials are discovered (other findings use codes in
|
||||
> the `200` range). The hook surfaces those exit codes directly to `pre-commit`,
|
||||
> so no extra handling is required—the commit will fail automatically on
|
||||
> non-zero exits.
|
||||
|
||||
To trigger a hook in CI without installing to `.git/hooks`, run (for example):
|
||||
|
||||
```bash
|
||||
pre-commit run kingfisher-pre-commit --all-files
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Compile
|
||||
You may compile for your platform via `make`
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ rules:
|
|||
id: kingfisher.ngrok.1
|
||||
pattern: |
|
||||
(?xi)
|
||||
\b
|
||||
ngrok
|
||||
(?:.|[\\n\r]){0,32}?
|
||||
(?:SECRET|PRIVATE|ACCESS|KEY|TOKEN)
|
||||
|
|
|
|||
161
scripts/install-kingfisher-pre-commit.ps1
Normal file
161
scripts/install-kingfisher-pre-commit.ps1
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<#!
|
||||
.SYNOPSIS
|
||||
Install or remove Kingfisher Git pre-commit hooks (local or global).
|
||||
|
||||
.DESCRIPTION
|
||||
Supports repo installs, global installs (via core.hooksPath), and
|
||||
custom hook directories. Preserves existing hooks safely and provides
|
||||
uninstall behavior mirroring the Bash installer.
|
||||
|
||||
.PARAMETER Global
|
||||
Install into the global Git hooks directory.
|
||||
|
||||
.PARAMETER HooksPath
|
||||
Manually override the hooks directory.
|
||||
|
||||
.PARAMETER Uninstall
|
||||
Remove the Kingfisher hook and restore a legacy hook if present.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$HooksPath,
|
||||
[switch]$Uninstall,
|
||||
[switch]$Global
|
||||
)
|
||||
|
||||
function Ensure-InRepo {
|
||||
if (-not $Global -and -not (git rev-parse --is-inside-work-tree 2>$null)) {
|
||||
throw "This installer must be run inside a Git repository unless --Global is specified."
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-HooksPath {
|
||||
param([string]$Override, [switch]$Global)
|
||||
|
||||
# Explicit override wins
|
||||
if ($Override) {
|
||||
return (Resolve-Path $Override).Path
|
||||
}
|
||||
|
||||
# Global mode
|
||||
if ($Global) {
|
||||
$p = git config --global core.hooksPath 2>$null
|
||||
if (-not $p) {
|
||||
# Default global hooks directory
|
||||
$p = Join-Path $HOME ".git-hooks"
|
||||
git config --global core.hooksPath $p
|
||||
Write-Host "Configured global Git hooks at $p"
|
||||
}
|
||||
return $p
|
||||
}
|
||||
|
||||
# Repo mode
|
||||
$repoHooks = git rev-parse --git-path hooks 2>$null
|
||||
if (-not $repoHooks) { throw "Unable to resolve repository hooks path." }
|
||||
return $repoHooks.Trim()
|
||||
}
|
||||
|
||||
function Uninstall-Kingfisher {
|
||||
param(
|
||||
[string]$PreCommit,
|
||||
[string]$Legacy,
|
||||
[string]$KFHook,
|
||||
[string]$Marker
|
||||
)
|
||||
|
||||
# Only try to inspect hook if it exists
|
||||
if (Test-Path $PreCommit) {
|
||||
# Only restore if this is our wrapper
|
||||
if (Select-String -Quiet -SimpleMatch -Path $PreCommit -Pattern $Marker) {
|
||||
if (Test-Path $Legacy) {
|
||||
Move-Item -Force $Legacy $PreCommit
|
||||
& chmod +x $PreCommit 2>$null | Out-Null
|
||||
Write-Host "Restored previous pre-commit hook from $Legacy"
|
||||
}
|
||||
else {
|
||||
Remove-Item -Force $PreCommit
|
||||
Write-Host "Removed Kingfisher pre-commit wrapper."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Always clean up wrapper + legacy
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue $KFHook, $Legacy
|
||||
Write-Host "Kingfisher pre-commit hook uninstalled."
|
||||
}
|
||||
|
||||
Ensure-InRepo
|
||||
|
||||
# Determine hooks directory safely
|
||||
$hooksDir = Resolve-HooksPath -Override $HooksPath -Global:$Global
|
||||
|
||||
if (-not (Test-Path $hooksDir)) {
|
||||
New-Item -ItemType Directory -Force -Path $hooksDir | Out-Null
|
||||
}
|
||||
|
||||
$preCommit = Join-Path $hooksDir "pre-commit"
|
||||
$legacy = Join-Path $hooksDir "pre-commit.legacy.kingfisher"
|
||||
$kfHook = Join-Path $hooksDir "kingfisher-pre-commit"
|
||||
$marker = "# Kingfisher pre-commit wrapper"
|
||||
|
||||
if ($Uninstall) {
|
||||
Uninstall-Kingfisher -PreCommit $preCommit -Legacy $legacy -KFHook $kfHook -Marker $marker
|
||||
return
|
||||
}
|
||||
|
||||
# ---- Kingfisher hook ----
|
||||
$kfContent = @"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v kingfisher >/dev/null 2>&1; then
|
||||
echo "Kingfisher is not on PATH; skipping scan." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
repo_root="\$(git rev-parse --show-toplevel)"
|
||||
cd "\$repo_root"
|
||||
|
||||
kingfisher scan . --staged --quiet --redact --only-valid --no-update-check
|
||||
"@
|
||||
|
||||
# ---- Wrapper ----
|
||||
# Note: No dirname logic here — absolute paths only
|
||||
$wrapper = @"
|
||||
#!/usr/bin/env bash
|
||||
$marker
|
||||
set -euo pipefail
|
||||
|
||||
legacy_hook="$legacy"
|
||||
kingfisher_hook="$kfHook"
|
||||
|
||||
if [[ -f "\$legacy_hook" && -x "\$legacy_hook" ]]; then
|
||||
"\$legacy_hook" "\$@"
|
||||
fi
|
||||
|
||||
"\$kingfisher_hook" "\$@"
|
||||
"@
|
||||
|
||||
# Write inner Kingfisher hook
|
||||
Set-Content -Path $kfHook -Value $kfContent -NoNewline
|
||||
& chmod +x $kfHook 2>$null | Out-Null
|
||||
|
||||
# Preserve existing hook ONLY if it exists
|
||||
if (Test-Path $preCommit) {
|
||||
# And if it's not our wrapper
|
||||
if (-not (Select-String -Quiet -SimpleMatch -Path $preCommit -Pattern $marker)) {
|
||||
Move-Item -Force $preCommit $legacy
|
||||
& chmod +x $legacy 2>$null
|
||||
Write-Host "Existing pre-commit hook preserved at $legacy"
|
||||
}
|
||||
}
|
||||
|
||||
# Write wrapper
|
||||
Set-Content -Path $preCommit -Value $wrapper -NoNewline
|
||||
& chmod +x $preCommit 2>$null | Out-Null
|
||||
|
||||
Write-Host "Kingfisher pre-commit hook installed at $preCommit"
|
||||
if (Test-Path $legacy) {
|
||||
Write-Host "Existing hook will run first from $legacy"
|
||||
}
|
||||
153
scripts/install-kingfisher-pre-commit.sh
Normal file
153
scripts/install-kingfisher-pre-commit.sh
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: install-kingfisher-pre-commit.sh [--global] [--hooks-path PATH] [--uninstall]
|
||||
|
||||
Installs a Git pre-commit hook that runs Kingfisher.
|
||||
|
||||
Modes:
|
||||
(default) Install in the current repo.
|
||||
--global Install in the global Git hooks directory.
|
||||
--hooks-path Override hooks directory (repo only).
|
||||
--uninstall Remove the installed hook.
|
||||
|
||||
USAGE
|
||||
}
|
||||
|
||||
GLOBAL=false
|
||||
UNINSTALL=false
|
||||
HOOKS_PATH=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--global)
|
||||
GLOBAL=true
|
||||
shift
|
||||
;;
|
||||
--hooks-path)
|
||||
HOOKS_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--uninstall)
|
||||
UNINSTALL=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ------------------------------
|
||||
# Determine hooks directory
|
||||
# ------------------------------
|
||||
if $GLOBAL; then
|
||||
GLOBAL_PATH="$(git config --global core.hooksPath || true)"
|
||||
if [[ -z "$GLOBAL_PATH" ]]; then
|
||||
GLOBAL_PATH="$HOME/.git-hooks"
|
||||
git config --global core.hooksPath "$GLOBAL_PATH"
|
||||
echo "Configured global Git hooks at $GLOBAL_PATH"
|
||||
fi
|
||||
HOOKS_PATH="$GLOBAL_PATH"
|
||||
else
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "Error: must be run inside a Git repository unless using --global." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$HOOKS_PATH" ]]; then
|
||||
HOOKS_PATH="$(git rev-parse --git-path hooks)"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$HOOKS_PATH"
|
||||
|
||||
PRE_COMMIT="$HOOKS_PATH/pre-commit"
|
||||
LEGACY="$HOOKS_PATH/pre-commit.legacy.kingfisher"
|
||||
KF_HOOK="$HOOKS_PATH/kingfisher-pre-commit"
|
||||
MARKER="# Kingfisher pre-commit wrapper"
|
||||
|
||||
# ------------------------------
|
||||
# Uninstall
|
||||
# ------------------------------
|
||||
uninstall() {
|
||||
if [[ -f "$PRE_COMMIT" ]] && grep -q "$MARKER" "$PRE_COMMIT"; then
|
||||
if [[ -f "$LEGACY" ]]; then
|
||||
mv "$LEGACY" "$PRE_COMMIT"
|
||||
chmod +x "$PRE_COMMIT"
|
||||
echo "Restored previous pre-commit hook from $LEGACY"
|
||||
else
|
||||
rm -f "$PRE_COMMIT"
|
||||
echo "Removed Kingfisher pre-commit wrapper."
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$KF_HOOK" "$LEGACY"
|
||||
echo "Kingfisher pre-commit hook uninstalled."
|
||||
}
|
||||
|
||||
if $UNINSTALL; then
|
||||
uninstall
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ------------------------------
|
||||
# Create Kingfisher hook
|
||||
# ------------------------------
|
||||
cat > "$KF_HOOK" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v kingfisher >/dev/null 2>&1; then
|
||||
echo "Kingfisher is not on PATH; skipping scan." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
cd "$repo_root"
|
||||
|
||||
kingfisher scan . --staged --quiet --redact --only-valid --no-update-check
|
||||
EOF
|
||||
chmod +x "$KF_HOOK"
|
||||
|
||||
# ------------------------------
|
||||
# Preserve existing hook only if it exists
|
||||
# ------------------------------
|
||||
if [[ -f "$PRE_COMMIT" ]]; then
|
||||
if ! grep -q "$MARKER" "$PRE_COMMIT"; then
|
||||
mv "$PRE_COMMIT" "$LEGACY"
|
||||
chmod +x "$LEGACY"
|
||||
echo "Existing pre-commit hook preserved at $LEGACY"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ------------------------------
|
||||
# Install wrapper
|
||||
# ------------------------------
|
||||
cat > "$PRE_COMMIT" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
$MARKER
|
||||
set -euo pipefail
|
||||
|
||||
legacy_hook="$LEGACY"
|
||||
kf_hook="$KF_HOOK"
|
||||
|
||||
if [[ -f "\$legacy_hook" && -x "\$legacy_hook" ]]; then
|
||||
"\$legacy_hook" "\$@"
|
||||
fi
|
||||
|
||||
"\$kf_hook" "\$@"
|
||||
EOF
|
||||
chmod +x "$PRE_COMMIT"
|
||||
|
||||
echo "Kingfisher pre-commit hook installed at $PRE_COMMIT"
|
||||
if [[ -f "$LEGACY" ]]; then
|
||||
echo "Existing hook will run first from $LEGACY"
|
||||
fi
|
||||
|
|
@ -323,6 +323,16 @@ pub struct InputSpecifierArgs {
|
|||
#[arg(long = "since-commit", value_name = "GIT-REF", help_heading = "Git Options")]
|
||||
pub since_commit: Option<String>,
|
||||
|
||||
/// Scan only staged changes by synthesizing a temporary commit and diffing it
|
||||
/// against the current HEAD (or an empty tree when no commits exist).
|
||||
#[arg(
|
||||
long,
|
||||
help_heading = "Git Options",
|
||||
conflicts_with = "branch_root",
|
||||
conflicts_with = "branch_root_commit"
|
||||
)]
|
||||
pub staged: bool,
|
||||
|
||||
/// Branch, tag, or commit to scan or compare against (defaults to HEAD)
|
||||
#[arg(
|
||||
long,
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ pub struct GitDiffConfig {
|
|||
pub since_ref: Option<String>,
|
||||
pub branch_ref: String,
|
||||
pub branch_root: Option<String>,
|
||||
pub staged: bool,
|
||||
}
|
||||
|
||||
struct EnumeratorConfig {
|
||||
|
|
|
|||
|
|
@ -444,6 +444,7 @@ fn create_default_scan_args() -> cli::commands::scan::ScanArgs {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
extra_ignore_comments: Vec::new(),
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
|
|
|
|||
|
|
@ -917,6 +917,7 @@ mod tests {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
extra_ignore_comments: Vec::new(),
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ mod tests {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
extra_ignore_comments: Vec::new(),
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::{
|
||||
marker::PhantomData,
|
||||
path::Path,
|
||||
process::Command,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
|
|
@ -63,10 +64,12 @@ pub fn enumerate_filesystem_inputs(
|
|||
let branch_root_enabled = args.input_specifier_args.branch_root
|
||||
|| args.input_specifier_args.branch_root_commit.is_some();
|
||||
|
||||
let diff_config = if args.input_specifier_args.since_commit.is_some()
|
||||
let wants_git_diff = args.input_specifier_args.staged
|
||||
|| args.input_specifier_args.since_commit.is_some()
|
||||
|| args.input_specifier_args.branch.is_some()
|
||||
|| branch_root_enabled
|
||||
{
|
||||
|| branch_root_enabled;
|
||||
|
||||
let diff_config = if wants_git_diff {
|
||||
let branch_arg = args.input_specifier_args.branch.clone();
|
||||
let branch_root_commit = args.input_specifier_args.branch_root_commit.clone();
|
||||
let (branch_ref, branch_root) = if branch_root_enabled {
|
||||
|
|
@ -83,6 +86,7 @@ pub fn enumerate_filesystem_inputs(
|
|||
since_ref: args.input_specifier_args.since_commit.clone(),
|
||||
branch_ref,
|
||||
branch_root,
|
||||
staged: args.input_specifier_args.staged,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
@ -737,7 +741,26 @@ fn enumerate_git_diff_repo(
|
|||
exclude_globset: Option<std::sync::Arc<globset::GlobSet>>,
|
||||
collect_commit_metadata: bool,
|
||||
) -> Result<GitRepoResult> {
|
||||
let GitDiffConfig { since_ref, branch_ref, branch_root } = diff_cfg;
|
||||
let GitDiffConfig { since_ref, branch_ref, branch_root, staged } = diff_cfg;
|
||||
|
||||
let (branch_ref, since_ref, branch_root) = if staged {
|
||||
if branch_root.is_some() {
|
||||
bail!("--staged cannot be combined with --branch-root options");
|
||||
}
|
||||
|
||||
let base_ref = match since_ref {
|
||||
Some(explicit) => explicit,
|
||||
None => detect_staged_base_ref(path)?,
|
||||
};
|
||||
|
||||
let parent_ref = resolve_optional_diff_ref(&repository, path, &branch_ref)
|
||||
.unwrap_or_else(|_| branch_ref.clone());
|
||||
let staged_commit = synthesize_staged_commit(path, parent_ref.as_str())?;
|
||||
|
||||
(staged_commit, Some(base_ref), None)
|
||||
} else {
|
||||
(branch_ref, since_ref, branch_root)
|
||||
};
|
||||
|
||||
let blobs = {
|
||||
let head_id = resolve_diff_ref(&repository, path, &branch_ref).with_context(|| {
|
||||
|
|
@ -892,6 +915,64 @@ fn enumerate_git_diff_repo(
|
|||
Ok(GitRepoResult { repository, path: path.to_owned(), blobs })
|
||||
}
|
||||
|
||||
fn synthesize_staged_commit(path: &Path, parent_ref: &str) -> Result<String> {
|
||||
let parent_arg: Vec<&str> =
|
||||
if parent_ref.is_empty() { Vec::new() } else { vec!["-p", parent_ref] };
|
||||
|
||||
let staged_tree =
|
||||
run_git_command(path, &["write-tree"], true)?.context("Failed to snapshot staged index")?;
|
||||
|
||||
let mut args = vec!["commit-tree", &staged_tree, "-m", "kingfisher staged snapshot"];
|
||||
args.extend(parent_arg.iter().copied());
|
||||
|
||||
run_git_command(path, &args, true)?.context("Failed to create staged snapshot commit")
|
||||
}
|
||||
|
||||
fn detect_staged_base_ref(path: &Path) -> Result<String> {
|
||||
if let Some(head) = run_git_command(path, &["rev-parse", "--verify", "HEAD"], false)? {
|
||||
return Ok(head);
|
||||
}
|
||||
|
||||
run_git_command(path, &["hash-object", "-t", "tree", "/dev/null"], true)?
|
||||
.context("Failed to resolve an empty tree when no base ref was available")
|
||||
}
|
||||
|
||||
fn resolve_optional_diff_ref(
|
||||
repository: &gix::Repository,
|
||||
path: &Path,
|
||||
reference: &str,
|
||||
) -> Result<String> {
|
||||
resolve_diff_ref(repository, path, reference).map(|id| id.to_hex().to_string())
|
||||
}
|
||||
|
||||
fn run_git_command(path: &Path, args: &[&str], bubble_up_error: bool) -> Result<Option<String>> {
|
||||
let output = Command::new("git").arg("-C").arg(path).args(args).output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
if bubble_up_error {
|
||||
bail!(
|
||||
"Git command failed ({}): git -C {} {}",
|
||||
output.status,
|
||||
path.display(),
|
||||
args.join(" ")
|
||||
);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if stdout.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(stdout))
|
||||
}
|
||||
}
|
||||
|
||||
fn command_succeeds(path: &Path, args: &[&str]) -> Result<bool> {
|
||||
let status = Command::new("git").arg("-C").arg(path).args(args).status()?;
|
||||
Ok(status.success())
|
||||
}
|
||||
|
||||
fn resolve_diff_ref<'repo>(
|
||||
repository: &'repo gix::Repository,
|
||||
path: &Path,
|
||||
|
|
@ -1064,6 +1145,7 @@ mod tests {
|
|||
since_ref: None,
|
||||
branch_ref: "featurefake".to_string(),
|
||||
branch_root: None,
|
||||
staged: false,
|
||||
},
|
||||
None,
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -533,10 +533,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_body_looks_like_html_trims_whitespace() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
);
|
||||
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8"));
|
||||
|
||||
let body = "\n\n \n<!DOCTYPE html>\n<html lang=\"en\"><body>page</body></html>";
|
||||
|
||||
|
|
@ -547,16 +544,13 @@ mod tests {
|
|||
fn test_html_response_rejected_when_not_allowed() {
|
||||
let matchers = vec![ResponseMatcher::StatusMatch {
|
||||
r#type: "status-match".to_string(),
|
||||
status: vec![StatusCode::OK],
|
||||
status: vec![StatusCode::OK.into()],
|
||||
match_all_status: false,
|
||||
negative: false,
|
||||
}];
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
);
|
||||
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8"));
|
||||
|
||||
let body = "\n<html><body>Sign in</body></html>";
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ fn run_skiplist(skip_regex: Vec<String>, skip_skipword: Vec<String>) -> Result<u
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
extra_ignore_comments: Vec::new(),
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ fn test_bitbucket_remote_scan() -> Result<()> {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
max_file_size_mb: 25.0,
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ rules:
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
max_file_size_mb: 5.0,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ fn test_github_remote_scan() -> Result<()> {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
max_file_size_mb: 25.0,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ fn test_gitlab_remote_scan() -> Result<()> {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
extra_ignore_comments: Vec::new(),
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
|
|
@ -283,6 +284,7 @@ fn test_gitlab_remote_scan_no_history() -> Result<()> {
|
|||
gcs_bucket: None,
|
||||
gcs_prefix: None,
|
||||
gcs_service_account: None,
|
||||
staged: false,
|
||||
},
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
max_file_size_mb: 25.0,
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ async fn test_redact_hashes_finding_values() -> Result<()> {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
max_file_size_mb: 25.0,
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ impl TestContext {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
extra_ignore_comments: Vec::new(),
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
|
|
@ -154,6 +155,7 @@ impl TestContext {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_scan_slack_messages() -> Result<()> {
|
||||
use std::env;
|
||||
let ctx = TestContext::new()?;
|
||||
|
||||
let server = MockServer::start().await;
|
||||
|
|
@ -256,6 +258,7 @@ async fn test_scan_slack_messages() -> Result<()> {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
max_file_size_mb: 25.0,
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ async fn test_validation_cache_and_depvars() -> Result<()> {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
max_file_size_mb: 25.0,
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ impl TestContext {
|
|||
branch: None,
|
||||
branch_root: false,
|
||||
branch_root_commit: None,
|
||||
staged: false,
|
||||
},
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
max_file_size_mb: 25.0,
|
||||
|
|
@ -259,6 +260,7 @@ impl TestContext {
|
|||
gcs_bucket: None,
|
||||
gcs_prefix: None,
|
||||
gcs_service_account: None,
|
||||
staged: false,
|
||||
},
|
||||
extra_ignore_comments: Vec::new(),
|
||||
content_filtering_args: ContentFilteringArgs {
|
||||
|
|
|
|||
351
tests/pre_commit_installer.rs
Normal file
351
tests/pre_commit_installer.rs
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_cmd::Command;
|
||||
use predicates::str::contains;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command as StdCommand;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn project_root() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
}
|
||||
|
||||
fn copy_scripts(dest: &Path) {
|
||||
let scripts_dir = dest.join("scripts");
|
||||
fs::create_dir_all(&scripts_dir).unwrap();
|
||||
|
||||
let src = project_root().join("scripts").join("install-kingfisher-pre-commit.sh");
|
||||
let dst = scripts_dir.join("install-kingfisher-pre-commit.sh");
|
||||
fs::copy(src, dst).unwrap();
|
||||
}
|
||||
|
||||
fn init_repo() -> (TempDir, PathBuf, PathBuf) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let repo = dir.path().to_path_buf();
|
||||
|
||||
copy_scripts(&repo);
|
||||
|
||||
Command::new("git").arg("init").current_dir(&repo).assert().success();
|
||||
|
||||
let hooks_path = repo.join(".git/hooks");
|
||||
fs::create_dir_all(&hooks_path).unwrap();
|
||||
|
||||
(dir, repo.clone(), hooks_path)
|
||||
}
|
||||
|
||||
fn install(repo: &Path, hooks_path: &Path) {
|
||||
Command::new("bash")
|
||||
.arg(repo.join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.arg("--hooks-path")
|
||||
.arg(hooks_path)
|
||||
.current_dir(repo)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Kingfisher pre-commit hook installed"));
|
||||
}
|
||||
|
||||
//
|
||||
// =====================================================
|
||||
// REPO-MODE TESTS (original ones, unchanged)
|
||||
// =====================================================
|
||||
//
|
||||
|
||||
#[test]
|
||||
fn installs_wrapper_without_existing_hook() {
|
||||
let (_tmp, repo, hooks_path) = init_repo();
|
||||
|
||||
install(&repo, &hooks_path);
|
||||
|
||||
let pre_commit = hooks_path.join("pre-commit");
|
||||
let kf_wrapper = hooks_path.join("kingfisher-pre-commit");
|
||||
let legacy = hooks_path.join("pre-commit.legacy.kingfisher");
|
||||
|
||||
let wrapper = fs::read_to_string(&pre_commit).unwrap();
|
||||
let kf_script = fs::read_to_string(&kf_wrapper).unwrap();
|
||||
|
||||
assert!(wrapper.contains("# Kingfisher pre-commit wrapper"));
|
||||
assert!(wrapper.contains("kingfisher-pre-commit"));
|
||||
assert!(kf_script
|
||||
.contains("kingfisher scan . --staged --quiet --redact --only-valid --no-update-check"));
|
||||
assert!(!legacy.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_existing_hook_and_runs_it_first() {
|
||||
let (_tmp, repo, hooks_path) = init_repo();
|
||||
|
||||
let log = repo.join("hook.log");
|
||||
let legacy = hooks_path.join("pre-commit");
|
||||
fs::write(&legacy, format!("#!/usr/bin/env bash\necho legacy >> {}\n", log.display())).unwrap();
|
||||
StdCommand::new("chmod").args(["+x", legacy.to_str().unwrap()]).assert().success();
|
||||
|
||||
let bin_dir = repo.join("bin");
|
||||
fs::create_dir_all(&bin_dir).unwrap();
|
||||
|
||||
let fake_kingfisher = bin_dir.join("kingfisher");
|
||||
fs::write(
|
||||
&fake_kingfisher,
|
||||
format!("#!/usr/bin/env bash\necho \"kingfisher $*\" >> {}\n", log.display()),
|
||||
)
|
||||
.unwrap();
|
||||
StdCommand::new("chmod").args(["+x", fake_kingfisher.to_str().unwrap()]).assert().success();
|
||||
|
||||
install(&repo, &hooks_path);
|
||||
|
||||
// Execute wrapper
|
||||
let wrapper = hooks_path.join("pre-commit");
|
||||
StdCommand::new(wrapper)
|
||||
.current_dir(&repo)
|
||||
.env("PATH", format!("{}:{}", bin_dir.display(), std::env::var("PATH").unwrap()))
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let log_contents = fs::read_to_string(&log).unwrap();
|
||||
let lines: Vec<_> = log_contents.lines().collect();
|
||||
assert_eq!(lines[0], "legacy");
|
||||
assert!(lines[1]
|
||||
.contains("kingfisher scan . --staged --quiet --redact --only-valid --no-update-check"));
|
||||
|
||||
assert!(hooks_path.join("pre-commit.legacy.kingfisher").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uninstall_restores_original_hook() {
|
||||
let (_tmp, repo, hooks_path) = init_repo();
|
||||
|
||||
let legacy = hooks_path.join("pre-commit");
|
||||
fs::write(&legacy, "#!/usr/bin/env bash\necho legacy\n").unwrap();
|
||||
StdCommand::new("chmod").args(["+x", legacy.to_str().unwrap()]).assert().success();
|
||||
|
||||
install(&repo, &hooks_path);
|
||||
|
||||
Command::new("bash")
|
||||
.arg(repo.join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.arg("--uninstall")
|
||||
.arg("--hooks-path")
|
||||
.arg(&hooks_path)
|
||||
.current_dir(&repo)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let restored = hooks_path.join("pre-commit");
|
||||
let restored_content = fs::read_to_string(&restored).unwrap();
|
||||
assert!(restored_content.contains("legacy"));
|
||||
assert!(!restored_content.contains("Kingfisher pre-commit wrapper"));
|
||||
assert!(!hooks_path.join("kingfisher-pre-commit").exists());
|
||||
assert!(!hooks_path.join("pre-commit.legacy.kingfisher").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uninstall_removes_wrapper_when_no_previous_hook() {
|
||||
let (_tmp, repo, hooks_path) = init_repo();
|
||||
|
||||
install(&repo, &hooks_path);
|
||||
|
||||
Command::new("bash")
|
||||
.arg(repo.join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.arg("--uninstall")
|
||||
.arg("--hooks-path")
|
||||
.arg(&hooks_path)
|
||||
.current_dir(&repo)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(!hooks_path.join("pre-commit").exists());
|
||||
assert!(!hooks_path.join("kingfisher-pre-commit").exists());
|
||||
assert!(!hooks_path.join("pre-commit.legacy.kingfisher").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_outside_git_repository() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
copy_scripts(dir.path());
|
||||
|
||||
Command::new("bash")
|
||||
.arg(dir.path().join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.current_dir(dir.path())
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("must be run inside a Git repository"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_commit_framework_invokes_kingfisher() {
|
||||
let (_tmp, repo, hooks_path) = init_repo();
|
||||
|
||||
let log = repo.join("hook.log");
|
||||
let bin_dir = repo.join("bin");
|
||||
fs::create_dir_all(&bin_dir).unwrap();
|
||||
|
||||
let fake_kingfisher = bin_dir.join("kingfisher");
|
||||
fs::write(&fake_kingfisher, format!("#!/usr/bin/env bash\necho \"$@\" > {}\n", log.display()))
|
||||
.unwrap();
|
||||
StdCommand::new("chmod").args(["+x", fake_kingfisher.to_str().unwrap()]).assert().success();
|
||||
|
||||
fs::write(
|
||||
repo.join(".pre-commit-config.yaml"),
|
||||
r#"repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: kingfisher-local
|
||||
name: kingfisher (local binary)
|
||||
entry: kingfisher
|
||||
language: system
|
||||
args: ["scan", ".", "--staged", "--quiet", "--redact", "--only-valid", "--no-update-check"]
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
fs::write(repo.join("README.md"), "demo").unwrap();
|
||||
|
||||
StdCommand::new("uv")
|
||||
.args(["run", "--no-config", "--with", "pre-commit", "pre-commit", "run", "--all-files"])
|
||||
.current_dir(&repo)
|
||||
.env("PATH", format!("{}:{}", bin_dir.display(), std::env::var("PATH").unwrap()))
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("kingfisher (local binary)"));
|
||||
|
||||
let log_contents = fs::read_to_string(&log).unwrap();
|
||||
assert!(log_contents.contains("scan"));
|
||||
assert!(log_contents.contains("--staged"));
|
||||
assert!(log_contents.contains("--quiet"));
|
||||
assert!(log_contents.contains("--redact"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installer_hook_executes_kingfisher_command() {
|
||||
let (_tmp, repo, hooks_path) = init_repo();
|
||||
|
||||
fs::write(repo.join("canary.txt"), "secret").unwrap();
|
||||
StdCommand::new("git").args(["add", "canary.txt"]).current_dir(&repo).assert().success();
|
||||
|
||||
let log = repo.join("hook.log");
|
||||
let bin_dir = repo.join("bin");
|
||||
fs::create_dir_all(&bin_dir).unwrap();
|
||||
|
||||
let fake_kingfisher = bin_dir.join("kingfisher");
|
||||
fs::write(
|
||||
&fake_kingfisher,
|
||||
format!("#!/usr/bin/env bash\necho \"kingfisher $@\" >> {}\n", log.display()),
|
||||
)
|
||||
.unwrap();
|
||||
StdCommand::new("chmod").args(["+x", fake_kingfisher.to_str().unwrap()]).assert().success();
|
||||
|
||||
install(&repo, &hooks_path);
|
||||
|
||||
let wrapper = hooks_path.join("pre-commit");
|
||||
StdCommand::new(wrapper)
|
||||
.current_dir(&repo)
|
||||
.env("PATH", format!("{}:{}", bin_dir.display(), std::env::var("PATH").unwrap()))
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let log_contents = fs::read_to_string(&log).unwrap();
|
||||
assert!(log_contents
|
||||
.contains("kingfisher scan . --staged --quiet --redact --only-valid --no-update-check"));
|
||||
}
|
||||
|
||||
//
|
||||
// =====================================================
|
||||
// "GLOBAL" SEMANTICS TESTS USING --hooks-path
|
||||
// (deterministic, no real global config)
|
||||
// =====================================================
|
||||
//
|
||||
|
||||
fn init_fake_global() -> (TempDir, PathBuf, PathBuf) {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
let fake_global_hooks = root.join("fake-global-hooks");
|
||||
fs::create_dir_all(&fake_global_hooks).unwrap();
|
||||
|
||||
copy_scripts(&root);
|
||||
|
||||
(tmp, root, fake_global_hooks)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_semantics_installs_wrapper_and_inner_hook() {
|
||||
let (_tmp, root, hooks) = init_fake_global();
|
||||
|
||||
Command::new("bash")
|
||||
.arg(root.join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.arg("--hooks-path")
|
||||
.arg(&hooks)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(hooks.join("pre-commit").exists());
|
||||
assert!(hooks.join("kingfisher-pre-commit").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_semantics_preserves_existing_hook_and_backup() {
|
||||
let (_tmp, root, hooks) = init_fake_global();
|
||||
|
||||
let legacy = hooks.join("pre-commit");
|
||||
fs::write(&legacy, "#!/usr/bin/env bash\necho global-legacy\n").unwrap();
|
||||
StdCommand::new("chmod").args(["+x", legacy.to_str().unwrap()]).assert().success();
|
||||
|
||||
Command::new("bash")
|
||||
.arg(root.join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.arg("--hooks-path")
|
||||
.arg(&hooks)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(hooks.join("pre-commit").exists());
|
||||
assert!(hooks.join("pre-commit.legacy.kingfisher").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_semantics_uninstall_restores_or_removes() {
|
||||
let (_tmp, root, hooks) = init_fake_global();
|
||||
|
||||
// case 1: with existing legacy
|
||||
let legacy = hooks.join("pre-commit");
|
||||
fs::write(&legacy, "#!/usr/bin/env bash\necho global-legacy\n").unwrap();
|
||||
StdCommand::new("chmod").args(["+x", legacy.to_str().unwrap()]).assert().success();
|
||||
|
||||
Command::new("bash")
|
||||
.arg(root.join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.arg("--hooks-path")
|
||||
.arg(&hooks)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
Command::new("bash")
|
||||
.arg(root.join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.arg("--uninstall")
|
||||
.arg("--hooks-path")
|
||||
.arg(&hooks)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// After uninstall with legacy, pre-commit should exist and contain legacy content
|
||||
let restored = fs::read_to_string(hooks.join("pre-commit")).unwrap();
|
||||
assert!(restored.contains("global-legacy"));
|
||||
|
||||
// case 2: no existing legacy, fresh install then uninstall
|
||||
let (_tmp2, root2, hooks2) = init_fake_global();
|
||||
Command::new("bash")
|
||||
.arg(root2.join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.arg("--hooks-path")
|
||||
.arg(&hooks2)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
Command::new("bash")
|
||||
.arg(root2.join("scripts/install-kingfisher-pre-commit.sh"))
|
||||
.arg("--uninstall")
|
||||
.arg("--hooks-path")
|
||||
.arg(&hooks2)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(!hooks2.join("pre-commit").exists());
|
||||
assert!(!hooks2.join("kingfisher-pre-commit").exists());
|
||||
assert!(!hooks2.join("pre-commit.legacy.kingfisher").exists());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue