Skip to content

[1/4] feat(git): add commit context collector - #1227

Open
Rafael-Silva-Oliveira wants to merge 1 commit into
Zoo-Code-Org:mainfrom
Rafael-Silva-Oliveira:feat/commit-msg-1-git-context
Open

[1/4] feat(git): add commit context collector#1227
Rafael-Silva-Oliveira wants to merge 1 commit into
Zoo-Code-Org:mainfrom
Rafael-Silva-Oliveira:feat/commit-msg-1-git-context

Conversation

@Rafael-Silva-Oliveira

@Rafael-Silva-Oliveira Rafael-Silva-Oliveira commented Aug 12, 2026

Copy link
Copy Markdown

Related GitHub Issue

Closes: #282

Part of: #145 · Stack 1 of 4 · Replaces the all-in-one #1218

Description

Adds getCommitContext(), the Git-reading half of AI commit-message generation.
Nothing consumes it yet — that arrives in stack 2. This PR is self-contained and
introduces no user-facing behavior.

Staged first, working tree as fallback. Staged changes are what a commit will
actually contain, so they take priority. When nothing is staged the collector falls
back to the working tree, so a caller still has something to summarize before the
user has staged anything.

The fallback reads git status --short, not a diff. Untracked files appear in no
diff, so a diff-only fallback would silently omit brand-new files — usually the most
interesting thing in the change.

No HEAD in the fallback diff. git diff is used rather than git diff HEAD.
The index is known to be empty on that path so the two are equivalent, but HEAD does
not resolve in a repository without an initial commit, where it fails outright. This
is covered by a regression test.

Reuses the existing checkGitInstalled, checkGitRepo, and truncateOutput helpers
rather than adding new ones. maxBuffer is raised past Node's 1 MB exec default,
which real diffs routinely exceed.

Scope note: this deliberately shells out to git diff and passes the output through.
It does not parse rename/copy status codes or summarize binary files. #298 takes a
much more thorough approach to the same problem — see "Relationship to #298-#301" below.

Test Procedure

src/utils/__tests__/git.spec.ts covers: staged path, working-tree fallback,
untracked-only repository with no initial commit, clean tree returning null, git not
installed, and not-a-repository.

There is also a test asserting the diff argument string contains no shell
metacharacters. That guards a real bug found during development: an earlier revision
used :(exclude) pathspecs to skip lockfiles, which unit tests happily passed because
they mock exec — but real git rejected the command, since exec runs through
cmd.exe on Windows and does not strip the single quotes those pathspecs require. The
mocked tests cannot validate git syntax, so that class of bug needs the guard.

Verified against real repositories, not just mocks:

  • Repo with staged + unstaged + untracked changes → correct staged-first context.
  • Repo with nothing staged → fallback context including the untracked file.
  • Freshly git init'd repo with no commits → returns context instead of throwing.

Local checks: pnpm lint, pnpm check-types (11/11 packages), full src suite
(7379 passed, 37 skipped), node scripts/find-missing-translations.js.

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I have performed a thorough self-review of my code.
  • Testing: New and/or updated tests have been added to cover my changes (if applicable).
  • Visual Snapshot (UI changes only): not applicable — no UI in this PR.
  • Documentation Impact: I have considered if my changes require documentation updates.
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines.

Visual Snapshots

Not applicable. This PR adds a service function with no UI.

Documentation Updates

  • No documentation updates are required.
  • Yes, documentation updates are required.

Nothing user-facing lands until stack 3.

Additional Notes

Review order: 1 → #1228#1229#1230. Each targets main because GitHub cannot
base a cross-fork PR on another fork's branch, so later PRs show cumulative diffs until
their parents merge. The Commits tab shows only that PR's own commit — that is the
reviewable unit.

Relationship to #298-#301. @Mirrowel has an open stack covering this same feature,
untouched since 2026-06-30. I built this independently before finding it. Where they
overlap, that stack is more thorough: #298 is ~996 lines handling rename/copy status
codes, -z null-delimited parsing, synthetic diffs for untracked files, and binary-file
summarization. This is ~190 lines and does none of that.

The tradeoff is size against completeness. If #298 is revived I would rather see that
land, and I am happy to close this. If it stays stale, this is ready now.

Summary by CodeRabbit

  • New Features

    • Added commit context generation from staged changes.
    • Falls back to unstaged or untracked changes when no staged changes are available.
    • Supports repositories without an initial commit and clean working trees.
    • Limits generated context to concise diff excerpts.
  • Bug Fixes

    • Improved handling of unavailable Git installations and non-repository locations.
    • Added safer processing for diff arguments and larger diff outputs.

Adds `getCommitContext()`, which gathers the changes a commit message should
describe. Part 1 of 4 for AI commit-message generation; nothing consumes it yet.

Staged changes are collected first, since that is what a commit will actually
contain. When nothing is staged it falls back to the working tree so callers
still have something to summarize before staging. The fallback reads
`git status --short` rather than a diff because untracked files appear in no
diff and would otherwise be invisible.

The fallback deliberately runs `git diff` rather than `git diff HEAD`: the index
is known to be empty at that point so the output is identical, but `HEAD` does
not resolve in a repository without an initial commit, where it would fail.

Reuses the existing `checkGitInstalled`, `checkGitRepo`, and `truncateOutput`
helpers. `maxBuffer` is raised past Node's 1MB `exec` default, which real diffs
routinely exceed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Git context collection

Layer / File(s) Summary
Commit context collection
src/utils/git.ts
Adds getCommitContext, which prefers staged changes and falls back to unstaged and untracked changes. It handles empty repositories, unavailable Git, clean trees, non-repository paths, diff limits, and output truncation.
Commit context validation
src/utils/__tests__/git.spec.ts
Adds mocks and tests for staged precedence, working-tree fallback, untracked files, shell-safe arguments, empty repositories, clean trees, missing Git, and non-repository paths.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant getCommitContext
  participant Git
  Caller->>getCommitContext: Request commit context
  getCommitContext->>Git: Check repository and staged changes
  Git-->>getCommitContext: Return status and diff
  getCommitContext->>Git: Read working-tree changes when no staged changes exist
  Git-->>getCommitContext: Return fallback status and diff
  getCommitContext-->>Caller: Return formatted context or null
Loading

Possibly related issues

  • #145 — The change implements staged-first Git commit-context generation with unstaged fallback.

Suggested reviewers: edelauna

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR meets staged-first and fallback requirements, but it does not safely represent renamed, copied, or binary files required by issue #282. Add path-safe, machine-readable parsing and tests for renamed, copied, binary, and unusual-path files while preserving staged precedence and untracked fallback.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The code and tests are focused on the Git context collector described by issue #282, with no unrelated implementation changes identified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly identifies the staged Git context collector, which is the primary change in the pull request.
Description check ✅ Passed The description includes the linked issue, implementation details, test procedure, checklist, documentation status, and relevant reviewer notes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/utils/__tests__/git.spec.ts (2)

401-426: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Reject all relevant shell metacharacters.

The assertion permits &, |, <, >, %, and ^. cmd.exe treats these characters specially. Use an allowlist of the expected Git commands, or reject the complete metacharacter set.

🤖 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 `@src/utils/__tests__/git.spec.ts` around lines 401 - 426, Update the command
validation assertion in the “should build diff arguments that need no shell
quoting” test to reject all relevant cmd.exe shell metacharacters, including &,
|, <, >, %, and ^, rather than only quotes and parentheses. Keep the existing
diffCommands filtering and command safety check intact.

378-378: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document or remove the double assertion.

The nearby comment explains the empty ChildProcess return value. It does not explain why implementation as unknown as typeof exec is safe. Type the mock against the required exec overload, or add a nearby comment that explains why the double assertion is necessary.

As per coding guidelines, “Use double assertions only as a last resort and explain them with a comment.”

🤖 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 `@src/utils/__tests__/git.spec.ts` at line 378, Update the mock setup around
vitest.mocked(exec) to avoid the implementation as unknown as typeof exec double
assertion by typing the mock implementation against the required exec overload;
if the assertion is unavoidable, add a nearby comment explaining why it is safe
and necessary.

Source: Coding guidelines

🤖 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 `@src/utils/__tests__/git.spec.ts`:
- Line 358: Remove the duplicate ExecResult and gitAvailable declarations in the
test scope, retaining only one declaration of each and updating references as
needed so the git tests compile without changing their behavior.

In `@src/utils/git.ts`:
- Line 16: Update truncateOutput usage in the commit-context output paths to
enforce both the existing GIT_OUTPUT_LINE_LIMIT and a character limit, including
the alternate return paths near the referenced locations. Preserve complete-line
truncation while adding the character cap, and add a focused test covering a
diff containing one very long changed line.

---

Nitpick comments:
In `@src/utils/__tests__/git.spec.ts`:
- Around line 401-426: Update the command validation assertion in the “should
build diff arguments that need no shell quoting” test to reject all relevant
cmd.exe shell metacharacters, including &, |, <, >, %, and ^, rather than only
quotes and parentheses. Keep the existing diffCommands filtering and command
safety check intact.
- Line 378: Update the mock setup around vitest.mocked(exec) to avoid the
implementation as unknown as typeof exec double assertion by typing the mock
implementation against the required exec overload; if the assertion is
unavoidable, add a nearby comment explaining why it is safe and necessary.
🪄 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: 737f916c-e49f-4b5b-8453-ccbf4fe7ed77

📥 Commits

Reviewing files that changed from the base of the PR and between abaf732 and 6b955f0.

📒 Files selected for processing (2)
  • src/utils/__tests__/git.spec.ts
  • src/utils/git.ts

describe("getCommitContext", () => {
const mockDiff = "@@ -1,1 +1,2 @@\n-old line\n+new line"

type ExecResult = { stdout: string; stderr: string }

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicate declarations.

Lines 358 and 381 redeclare ExecResult and gitAvailable in the same scope. TypeScript rejects these declarations, so the test file cannot compile. Keep one declaration of each.

Also applies to: 381-381

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { ExecException } from "child_process"
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@src/utils/__tests__/git.spec.ts` at line 358, Remove the duplicate ExecResult
and gitAvailable declarations in the test scope, retaining only one declaration
of each and updating references as needed so the git tests compile without
changing their behavior.

Comment thread src/utils/git.ts
const GIT_OUTPUT_LINE_LIMIT = 500

// Node's default `exec` buffer is 1MB, which real-world diffs routinely exceed.
const GIT_DIFF_MAX_BUFFER = 10 * 1024 * 1024

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a character limit to commit context output.

GIT_DIFF_MAX_BUFFER permits up to 10 MiB of diff output. truncateOutput(output, GIT_OUTPUT_LINE_LIMIT) limits lines only. A diff with one very long changed line remains almost unbounded because the helper preserves complete retained lines.

Pass a character limit in both return paths. Add a focused test with a long single-line diff.

Also applies to: 387-387, 403-403

🤖 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 `@src/utils/git.ts` at line 16, Update truncateOutput usage in the
commit-context output paths to enforce both the existing GIT_OUTPUT_LINE_LIMIT
and a character limit, including the alternate return paths near the referenced
locations. Preserve complete-line truncation while adding the character cap, and
add a focused test covering a diff containing one very long changed line.

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 12, 2026
Comment thread src/utils/git.ts

if (!status.trim()) {
return null
}

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.

Could we include bounded contents for untracked files rather than only their paths? Without the contents, an untracked-only change does not give the model enough information to write an accurate commit message.

Comment thread src/utils/git.ts

if (!status.trim()) {
return null
}

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.

Could we use NUL-delimited, machine-readable Git output and parse it into structured file entries? The current human-readable output can ambiguously represent renames, binary files, and filenames containing unusual characters.

Comment thread src/utils/git.ts

if (stagedSummary.trim()) {
const { stdout: stagedDiff } = await execAsync(`git diff --cached ${COMMIT_DIFF_ARGS}`, options)
const output = `Staged changes:\n\n${stagedSummary.trim()}\n\n${stagedDiff.trim()}`

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.

Could we also pass a finite character limit to truncateOutput()? A minified or generated file can contain a multi-megabyte single line that bypasses the current 500-line limit.

Comment thread src/utils/git.ts

const options = { cwd, maxBuffer: GIT_DIFF_MAX_BUFFER }

const { stdout: stagedSummary } = await execAsync("git diff --cached --stat", options)

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.

Could we handle failures from the status and diff commands through a clear nullable or typed-error contract? Right now an expected Git failure, such as exceeding maxBuffer, rejects even though this function is documented as returning context or null.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ENHANCEMENT] Add Git context collector for commit-message generation

2 participants