Skip to content

feat(commit): run pre-commit and post-commit git hooks#198

Open
veeso wants to merge 3 commits into
cococonscious:mainfrom
veeso:feat/git-hooks
Open

feat(commit): run pre-commit and post-commit git hooks#198
veeso wants to merge 3 commits into
cococonscious:mainfrom
veeso:feat/git-hooks

Conversation

@veeso
Copy link
Copy Markdown

@veeso veeso commented Apr 28, 2026

Summary

Koji currently bypasses git's pre-commit and post-commit hooks because the underlying cocogitto API only invokes the commit-msg hook. This PR runs both hooks around the commit so koji behaves like git commit, and adds a --no-verify flag to bypass them.

Changes

  • pre-commit runs before the commit. If it exits non-zero, the commit is aborted and koji exits non-zero.
  • post-commit runs after a successful commit. A failing post-commit hook is reported as a warning on stderr and does not abort, matching git commit.
  • When --all is set, files are staged before pre-commit runs so the hook sees the same index that will be committed (matches git commit -a semantics). Staging is performed by koji directly via gix (custom stage_tracked helper), not delegated to cocogitto, so the same set of files is staged regardless of whether hooks run. Only tracked modified/deleted files are staged, matching koji's documented --all flag.
  • New --no-verify flag to skip both hooks. Conflicts with --hook (the git-hook mode already bypasses koji's own hook orchestration).
  • commit-msg continues to be handled by cocogitto. prepare-commit-msg is intentionally skipped because koji generates the message itself.

Dependency changes

The new hook runner (CommitHook::PreCommit / CommitHook::PostCommit) is exposed by cocogitto 7, so this PR also bumps several core dependencies:

  • cocogitto 6.5 → 7 (major bump — required for the hook API; CommitOptions is re-exported by koji's lib, so this is a semver-relevant change for downstream users of the crate).
  • gix 0.80 → 0.83 (transitive alignment with cocogitto 7).

The signature of koji::commit::commit gains a no_verify: bool parameter. This is a breaking change to the public lib API, but the cocogitto 7 upgrade is already a breaking change at the same surface (re-exported CommitOptions), so the new parameter ships alongside it.

Test plan

Seven integration tests cover the new behavior:

  • test_pre_commit_hook_runs: a pre-commit hook that touches a sentinel file fires during the koji commit flow.
  • test_pre_commit_hook_failure_aborts: a failing pre-commit hook causes koji to exit non-zero and no new commit is created.
  • test_no_verify_skips_pre_commit_hook: --no-verify bypasses a failing pre-commit hook and the commit succeeds.
  • test_post_commit_hook_runs: a post-commit hook fires after a successful koji commit.
  • test_post_commit_hook_failure_does_not_abort: a failing post-commit hook is reported as a warning but the commit still lands and koji exits zero.
  • test_no_verify_skips_post_commit_hook: --no-verify skips the post-commit hook.
  • test_all_stages_tracked_only: --all stages tracked modified/deleted files but leaves untracked files alone, matching git commit -a.

All existing tests still pass (cargo test and cargo clippy --all-targets -- -D warnings).

Docs

README has a new "Git hooks" subsection under "Usage" documenting the default behavior, the --no-verify flag, the post-commit warning behavior, and the --all ordering note.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 86.66667% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/lib/commit.rs 86.66% 2 Missing ⚠️
Files with missing lines Coverage Δ
src/lib/commit.rs 94.11% <86.66%> (-5.89%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Koji’s commit flow to more closely match git commit by executing pre-commit and post-commit hooks around the commit operation, with a new --no-verify flag to bypass them.

Changes:

  • Run pre-commit before committing and abort on failure; run post-commit after a successful commit.
  • Add --no-verify CLI flag to skip both hooks.
  • Add integration tests for pre-commit behavior and update README documentation; adjust dependencies (including promoting git2).

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/integration.rs Adds integration coverage for pre-commit and --no-verify behavior.
src/lib/commit.rs Implements pre/post hook execution and pre-staging logic prior to pre-commit.
src/bin/main.rs Adds --no-verify flag and threads it into the commit call.
README.md Documents git hook behavior and --no-verify.
Cargo.toml Promotes git2 and updates dependency versions (notably cocogitto/gix).
Cargo.lock Lockfile updates from dependency changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/commit.rs Outdated
Comment thread src/lib/commit.rs
Comment thread Cargo.toml Outdated
Comment thread src/lib/commit.rs
Copy link
Copy Markdown
Owner

@cococonscious cococonscious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for the PR. There's a few changes I'd like to make.
One concern I have is regarding the differing staging behavior with and without having hooks enabled. With --all + hooks enabled, koji only stages tracked files while with --all + --no-verify, cocogitto stages both tracked and untracked files. Please find a solution for this, perhaps always staging with koji and not ever staging with cocogitto, but you're free to find the best solution.
There's also that post-commit hook failures are reported as an error, even though by default git commit ignores post-commit hook failures, so we should probably follow that. The thing is that cocogitto also propagates the errors, wrongfully so...

Comment thread Cargo.toml

[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the minor versions of the dependencies and the patch versions of the dev-dependencies.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted

Comment thread Cargo.toml Outdated
config = { version = "0.15", features = ["toml"] }
xdg = "3.0"
gix = "0.80.0"
git2 = "0.20"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to not include git2 again, would it be possible to implement this with purely gix?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@veeso It definitely is! Gix is still maturing, but they have all of the relevant pieces of late. It would be the simplest for Koji if we got the plumbing for this upstreamed, but short-term we could host it inline in the repository until the higher-level APIs mature. From what I found:

  1. gix::discover() for the repository discovering
  2. Statting modifications: repo.status()
  3. For writing new blobs: repo.write_blob
  4. The hardest part seems to be the loop for mutating index entries: You'll need to edit state.entries' id and stat fields manually in a manual loop. (I didn't research this deeply enough :()
  5. Index writing with index.write

Sorry for the difficulty! At least if we get it into mainline Gix then other Rust projects can use it too :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, replaced git2 with gix.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider upstreaming at gitoxide? Not to hold up merging now, but for future simplifications.

Comment thread src/bin/main.rs
all: bool,

#[arg(long, help = "Bypass the pre-commit and post-commit git hooks")]
no_verify: bool,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add conflicts_with = "hook" as no_verify has no effect in hook mode.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Wraps the cocogitto commit with `pre-commit` and `post-commit` git
hooks, matching `git commit` behavior. A failing `pre-commit` hook
aborts the commit; a failing `post-commit` hook is reported as a
warning and does not abort, matching `git commit`.

Staging is always performed by koji (via gix) regardless of whether
hooks are enabled, so `--all` and `--all --no-verify` produce the
same staged set: tracked modified/deleted files only, matching the
documented `--all` flag and `git commit -a` semantics. Cocogitto's
own staging is bypassed.

Adds `--no-verify` to skip both hooks (conflicts with `--hook`).
@veeso veeso requested a review from cococonscious April 28, 2026 15:55
veeso added 2 commits April 29, 2026 12:26
`stage_tracked` updates index entries' blob ids in-place but left two
gix-index pieces stale, which manifested only when cocogitto (libgit2)
wrote the commit afterwards:

- The TREE extension still pointed at the pre-edit root tree, so
  libgit2's `index.write_tree()` returned the cached old tree id —
  producing a commit whose tree equals its parent's (empty diff).
- The entry `stat` (mtime/size/ino) was not refreshed from disk, so
  `git status` reported the file as modified after commit even though
  index, HEAD and worktree blobs all matched.

Drop the TREE extension and rewrite `entry.stat` from the worktree
file's metadata before writing the index.
Stage untracked files (matching documented `git add -A` semantics) and
gracefully open `.git/index` via `at_or_default` so `koji -a` works in
a fresh repo before any commit exists.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants