Skip to content

feat(content-utils): add commit history tracking to git info#265

Open
Fryuni wants to merge 8 commits into
mainfrom
git-entry-history
Open

feat(content-utils): add commit history tracking to git info#265
Fryuni wants to merge 8 commits into
mainfrom
git-entry-history

Conversation

@Fryuni

@Fryuni Fryuni commented Feb 12, 2026

Copy link
Copy Markdown
Owner

Summary

Adds commit history tracking to getEntryGitInfo() in @inox-tools/content-utils. Each content entry now includes a full list of commits that modified it, with lazy content retrieval at each commit point.

Features

Core Functionality

  • New commits field on GitTrackingInfo containing an array of CommitInfo objects
  • Each commit includes:
    • hash - Full 40-character SHA
    • date - Date object
    • author + coAuthors - GitAuthor objects
    • content - Lazy getter that retrieves file content at that commit (memoized via Lazy.wrap)

Configuration

  • collectCommitHistory option (default: true) - Allows disabling for memory-constrained projects
  • When disabled, commits returns empty array []

Implementation Details

  • Dev mode: Lazy git show execution via Object.defineProperty getter
  • Build mode: Pre-fetches content as strings during build, wraps with Lazy.wrap for API consistency
  • Graceful degradation: Returns empty string when git show fails (deleted files, shallow clones)
  • Git parsing: Extended format to capture commit hashes (%H in --format)

Type Changes

export type CommitInfo = {
  hash: string;
  date: Date;
  content: string; // Lazy getter (transparent to consumers)
  author: GitAuthor;
  coAuthors: GitAuthor[];
};

export type GitTrackingInfo = {
  earliest: Date;
  latest: Date;
  authors: GitAuthor[];
  coAuthors: GitAuthor[];
  commits?: CommitInfo[]; // New field
};

Testing

Test Infrastructure

  • Git test fixture with real repository archived as .git.tar.gz (tracked via Git LFS)
  • Integration tests covering:
    • ✅ Commits field populated with correct hash/date/author/coAuthors
    • ✅ Lazy content getter retrieves file content at each commit
    • ✅ Disabled mode (collectCommitHistory: false) returns empty array

Test Results

  • All existing tests pass
  • 3 new integration tests (all passing)
  • Full monorepo build succeeds

Commits

  1. ab44d0a - chore: add LFS tracking for fixture git archives and ignore nested .git dirs
  2. 717813b - feat(content-utils): add CommitInfo type and collectCommitHistory option
  3. 36dc607 - test(content-utils): add git-tracking fixture with archived git repo
  4. c0c52aa - feat(content-utils): add commit history parsing and content helper to git runtime
  5. 2ea1a48 - feat(content-utils): add commit history support for dev and build modes
  6. 94179b7 - test(content-utils): add integration tests for commit history tracking

Files Changed

17 files changed: 349 insertions(+), 24 deletions(-)

Modified

  • packages/content-utils/virtual.d.ts - Type definitions
  • packages/content-utils/src/integration/state.ts - State management
  • packages/content-utils/src/integration/index.ts - Integration option
  • packages/content-utils/src/runtime/git.ts - Core git parsing logic
  • packages/content-utils/src/runtime/liveGit.ts - Dev mode support
  • packages/content-utils/src/integration/gitPlugin.ts - Dev/build plugins
  • .gitattributes - Git LFS tracking for test archives
  • .gitignore - Ignore unpacked .git directories

Created

  • packages/content-utils/tests/fixture/git-tracking/ - Complete test fixture
  • packages/content-utils/tests/git-tracking.test.ts - Integration tests (103 lines)

Breaking Changes

None - Fully backward compatible. The commits field is optional in types and always populated at runtime.

Dependencies

None added - Uses existing @inox-tools/utils package for Lazy.wrap.

Verification

✅ pnpm build --filter @inox-tools/content-utils  # Exit 0
✅ pnpm test --filter @inox-tools/content-utils   # All pass
✅ pnpm build                                      # Full monorepo - Exit 0
✅ LSP diagnostics                                 # Zero errors

Summary by CodeRabbit

  • New Features

    • Added per-entry git commit history in content metadata (commit list with hash, date, author, coAuthors) and lazy-loaded commit content.
    • New optional collectCommitHistory option (true by default) to toggle history collection.
    • Public CommitInfo type and commits field exposed on content tracking info.
  • Tests

    • Added end-to-end tests validating git-history integration and lazy content retrieval.
  • Chores

    • Added Git LFS patterns and .gitignore rules; CI checkout now enables LFS.

@vercel

vercel Bot commented Feb 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
inox-tools Ready Ready Preview Feb 16, 2026 6:40pm

@coderabbitai

coderabbitai Bot commented Feb 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds optional commit-history collection and lazy per-commit content access to the content-utils Git tracking subsystem, refactors plugin factories to accept a unified IntegrationState, and adds test fixtures plus CI and repo metadata updates for handling packed .git fixtures.

Changes

Cohort / File(s) Summary
Git tracking runtime
packages/content-utils/src/runtime/git.ts, packages/content-utils/src/runtime/liveGit.ts
Add RawCommitInfo/CommitInfo, parse commit hashes in git logs, add getFileContentAtCommit, setCollectCommitHistory, and attach lazy content getters to commit entries.
Integration plumbing
packages/content-utils/src/integration/index.ts, packages/content-utils/src/integration/state.ts
Add collectCommitHistory option (default true) to integration schema and state; propagate option into state during setup.
Plugin refactor & build wiring
packages/content-utils/src/integration/gitPlugin.ts
Change gitDevPlugin signature to accept state param, wire state.collectCommitHistory into dev/build paths, prefetch commit content at build, and emit facade with lazy commit content helpers.
Public typings
packages/content-utils/virtual.d.ts
Introduce CommitInfo type and extend GitTrackingInfo with optional commits: CommitInfo[].
Tests & fixtures
packages/content-utils/tests/*, packages/content-utils/tests/fixture/git-tracking/*
Add git-tracking fixture (astro config, package, content, page), tests verifying commit history, lazy content, and behavior when collectCommitHistory disabled.
Repo metadata & CI
.gitattributes, .gitignore, .github/workflows/ci.yml
Add LFS filter for .git.tar.gz, ignore unpacked .git fixtures, and enable LFS in CI checkout steps.

Sequence Diagram(s)

sequenceDiagram
    participant Setup as Integration Setup
    participant Plugin as Dev/Build Plugin
    participant GitCollector as Git Runtime/Collector
    participant Lazy as Lazy Utility
    participant Consumer as Runtime API / Page

    Setup->>Plugin: pass IntegrationState (includes collectCommitHistory)
    Plugin->>GitCollector: initialize with projectRoot & collectCommitHistory

    alt collectCommitHistory = true
        GitCollector->>GitCollector: run git log parsing (include hashes)
        GitCollector->>GitCollector: build RawCommitInfo per file
        GitCollector->>Lazy: wrap getFileContentAtCommit into Lazy getter
        GitCollector-->>Plugin: attach commits (lazy content) to tracking info
    else collectCommitHistory = false
        GitCollector->>Plugin: return tracking info without commits
    end

    Consumer->>Plugin: getEntryGitInfo()
    Plugin-->>Consumer: returns commits array where content is fetched only when accessed (Lazy resolves)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Suggested Reviewers

  • florian-lefebvre
  • OliverSpeir

Poem

🐰 With whiskers twitching and a hop so spry,

I fetch old commits where the histories lie.
Lazy little getters, they wait till you call,
Then spill every line from the archive's small hall.
Hoppity code — git stories for all!

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding commit history tracking to git info. It accurately reflects the primary feature being introduced across the content-utils package.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch git-entry-history

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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 and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Feb 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@inox-tools/aik-mod

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/aik-mod@265

@inox-tools/aik-route-config

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/aik-route-config@265

@inox-tools/astro-tests

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/astro-tests@265

@inox-tools/astro-when

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/astro-when@265

@inox-tools/content-utils

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/content-utils@265

@inox-tools/custom-routing

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/custom-routing@265

@inox-tools/cut-short

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/cut-short@265

@inox-tools/inline-mod

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/inline-mod@265

@inox-tools/modular-station

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/modular-station@265

@inox-tools/portal-gun

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/portal-gun@265

@inox-tools/request-nanostores

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/request-nanostores@265

@inox-tools/request-state

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/request-state@265

@inox-tools/runtime-logger

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/runtime-logger@265

@inox-tools/server-islands

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/server-islands@265

@inox-tools/sitemap-ext

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/sitemap-ext@265

@inox-tools/star-warp

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/star-warp@265

@inox-tools/utils

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/utils@265

@inox-tools/velox-luna

npm i https://pkg.pr.new/Fryuni/inox-tools/@inox-tools/velox-luna@265

commit: 9dd2a34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/content-utils/src/runtime/liveGit.ts (1)

68-71: ⚠️ Potential issue | 🔴 Critical

Pre-existing bug: getOldestCommitDate returns info.latest instead of info.earliest.

Line 71 returns info.latest, but this function is supposed to return the oldest commit date. This should be info.earliest.

🐛 Proposed fix
 export async function getOldestCommitDate(...args: Parameters<typeof getEntry>): Promise<Date> {
 	const [file, info] = await getEntryGitInfoInner(args);
 
-	if (info !== undefined) return info.latest;
+	if (info !== undefined) return info.earliest;
🤖 Fix all issues with AI agents
In @.gitattributes:
- Line 1: Add Git LFS to CI and docs: update the CI workflow checkout step using
actions/checkout@v3 to enable lfs by adding lfs: true to the step, and update
every other workflow that runs tests to do the same; then add contributor setup
instructions to README.md or CONTRIBUTING.md describing running git lfs install
and git lfs pull (and noting the dependency) so local clones and CI will fetch
the .git.tar.gz fixture.

In `@packages/content-utils/src/runtime/git.ts`:
- Around line 46-58: getRepoRoot() is currently invoking a new git subprocess on
every call (causing many redundant spawnSync calls from getFileContentAtCommit
and the pre-fetch loop); memoize the repo root by introducing a module-level
cachedRepoRoot and have getRepoRoot return the cached value when set, otherwise
run spawnSync once and store the result; update setProjectRoot to clear/reset
cachedRepoRoot (e.g., set cachedRepoRoot = undefined) so changes to the root
invalidate the cache; then ensure getFileContentAtCommit and any other callers
use the memoized getRepoRoot.

In `@packages/content-utils/tests/git-tracking.test.ts`:
- Around line 39-47: Tests fail because the fixture .git.tar.gz is a Git LFS
pointer and CI checks out only the pointer; update CI to fetch LFS objects or
fetch them before tests: modify the test job’s checkout step to use
actions/checkout@v4 with lfs: true, or add an explicit git lfs install && git
lfs pull step prior to running tests so the beforeAll block that spawns tar to
unpack '.git.tar.gz' (using fixturePath) operates on the real gzip archive
rather than a 130-byte pointer; alternatively, replace the LFS-tracked fixture
with a normal committed file if size permits.
🧹 Nitpick comments (5)
.gitignore (1)

62-63: Clean up duplicate entry.

The test-results/ entry appears twice. Consider removing the duplicate for cleaner maintenance.

🧹 Proposed cleanup
 # Test results
 test-results/
-test-results/
 playwright-report/
packages/content-utils/src/runtime/liveGit.ts (1)

25-37: Duplicate CommitInfo construction logic — consider extracting a shared helper.

This mapping logic (create Record<string, unknown>, set fields, Object.defineProperty for lazy content, cast to CommitInfo) is duplicated nearly verbatim in git.ts (lines 214–226). Extracting a shared buildCommitInfo(c: RawCommitInfo): CommitInfo helper in git.ts and importing it here would reduce duplication and ensure consistent construction.

packages/content-utils/src/runtime/git.ts (1)

206-252: Hook-exposed GitTrackingInfo with commits is constructed then discarded — hook mutations to commits are lost.

Lines 214–226 build CommitInfo[] with lazy getters for the hook callback at line 235, but the function returns rawFileInfo (line 251), not fileInfo. While earliest/latest are synced back (lines 248–249), any modifications a hook consumer makes to fileInfo.commits (or authors/coAuthors) are silently dropped.

Currently the hook only exposes drop(), so this isn't actively broken, but it's a latent inconsistency that will surprise future hook consumers. Worth a TODO or syncing all fields back.

packages/content-utils/src/integration/gitPlugin.ts (1)

96-102: Build-time pre-fetch mutates RawCommitInfo objects in place — acceptable but consider a brief comment.

The as any casts to add content and delete repoPath are pragmatic for preparing serialization, but they silently change the shape of objects typed as RawCommitInfo. A one-line comment explaining why the mutation is safe (objects are consumed only by devalue.stringify immediately after) would help future readers.

packages/content-utils/tests/git-tracking.test.ts (1)

26-36: Missing &#39; / &apos; in HTML entity unescaping.

The unescape helper handles &amp;, &lt;, &gt;, and &quot; but omits &#39; (apostrophe), which Astro may also encode. This is unlikely to matter for the current JSON payloads (no single quotes in values), but worth noting for robustness.

Proposed fix
 	const json = match[1]
 		.replace(/&amp;/g, '&')
 		.replace(/&lt;/g, '<')
 		.replace(/&gt;/g, '>')
-		.replace(/&quot;/g, '"');
+		.replace(/&quot;/g, '"')
+		.replace(/&#39;/g, "'");

Comment thread .gitattributes
@@ -0,0 +1 @@
packages/*/tests/fixture/**/.git.tar.gz filter=lfs diff=lfs merge=lfs -text

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for .git.tar.gz files matching the pattern
echo "=== Searching for .git.tar.gz files in packages/*/tests/fixture ==="
find packages -type f -name "*.git.tar.gz" 2>/dev/null || echo "No .git.tar.gz files found"

echo ""
echo "=== Checking directory structure ==="
find packages -type d -name fixture 2>/dev/null | head -20 || echo "No fixture directories found"

echo ""
echo "=== Looking for any fixture files ==="
find packages -path "*/tests/fixture/*" -type f 2>/dev/null | head -20 || echo "No fixture files found"

Repository: Fryuni/inox-tools

Length of output: 1905


🏁 Script executed:

#!/bin/bash
# Check for Git LFS documentation
echo "=== Searching for Git LFS documentation ==="
if [ -f README.md ]; then
  echo "README.md exists, checking for LFS references:"
  grep -i "git lfs\|lfs\|large file" README.md || echo "No LFS references in README.md"
fi

if [ -f CONTRIBUTING.md ]; then
  echo "CONTRIBUTING.md exists, checking for LFS references:"
  grep -i "git lfs\|lfs\|large file" CONTRIBUTING.md || echo "No LFS references in CONTRIBUTING.md"
fi

echo ""
echo "=== Checking for any setup/installation docs ==="
find . -maxdepth 2 -type f \( -name "*.md" -o -name "SETUP*" -o -name "INSTALL*" \) 2>/dev/null | head -20

Repository: Fryuni/inox-tools

Length of output: 353


🏁 Script executed:

#!/bin/bash
# Check CI/CD configuration for Git LFS
echo "=== Searching for CI/CD configuration files ==="
find . -path "./.git*" -prune -o -type f \( -name "*.yml" -o -name "*.yaml" \) -print | grep -E "(github|gitlab|circle|travis|jenkins|workflows)" | head -20

echo ""
echo "=== Checking GitHub Actions workflows ==="
if [ -d ".github/workflows" ]; then
  ls -la .github/workflows/
  echo ""
  echo "=== Checking for lfs in workflow files ==="
  grep -r "lfs\|LFS" .github/workflows/ 2>/dev/null || echo "No LFS references in workflows"
else
  echo "No .github/workflows directory found"
fi

Repository: Fryuni/inox-tools

Length of output: 840


🏁 Script executed:

#!/bin/bash
# Check what Git LFS is currently tracking
echo "=== Current Git LFS tracking ==="
if command -v git lfs &> /dev/null; then
  git lfs ls-files || echo "Git LFS command available but no files tracked yet"
else
  echo "Git LFS not installed in this environment"
fi

echo ""
echo "=== Checking .gitattributes content ==="
if [ -f .gitattributes ]; then
  cat -n .gitattributes
fi

Repository: Fryuni/inox-tools

Length of output: 370


Add Git LFS configuration to CI/CD workflows and document the dependency for contributors.

The .gitattributes pattern correctly targets packages/content-utils/tests/fixture/git-tracking/.git.tar.gz, which exists in the repository. However, this introduces critical issues:

  1. CI/CD will fail: The GitHub Actions workflows (ci.yml, changesets.yml, etc.) don't have Git LFS configured. Tests requiring the .git.tar.gz fixture will fail because the file won't be pulled during checkout.

  2. Undocumented requirement: README.md and other setup documentation don't mention the Git LFS requirement, leaving contributors unable to clone or work with these fixtures locally.

Required fixes:

  • Add lfs: true to the actions/checkout@v3 step in .github/workflows/ci.yml and any other workflows that run tests
  • Add Git LFS setup documentation to README.md or CONTRIBUTING.md, including git lfs install and git lfs pull instructions for developers
🤖 Prompt for AI Agents
In @.gitattributes at line 1, Add Git LFS to CI and docs: update the CI workflow
checkout step using actions/checkout@v3 to enable lfs by adding lfs: true to the
step, and update every other workflow that runs tests to do the same; then add
contributor setup instructions to README.md or CONTRIBUTING.md describing
running git lfs install and git lfs pull (and noting the dependency) so local
clones and CI will fetch the .git.tar.gz fixture.

Comment on lines +46 to +58
export function getFileContentAtCommit(hash: string, repoPath: string): string {
const result = spawnSync('git', ['show', `${hash}:${repoPath}`], {
cwd: getRepoRoot(),
encoding: 'utf-8',
});

if (result.error || result.status !== 0) {
debug('Failed to retrieve file content at commit:', hash, repoPath, result.stderr);
return '';
}

return result.stdout;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getRepoRoot() spawns a new git rev-parse process on every call — memoize it.

getFileContentAtCommit calls getRepoRoot() each time, which runs spawnSync('git', ['rev-parse', '--show-toplevel']). In dev mode, every lazy content access triggers this. In build mode, the pre-fetch loop in gitPlugin.ts calls this for every commit across all files. This could mean hundreds of redundant git subprocesses.

⚡ Proposed fix — memoize getRepoRoot
+let cachedRepoRoot: string | undefined;
+
 function getRepoRoot(): string {
+	if (cachedRepoRoot !== undefined) return cachedRepoRoot;
 	debug('Retrieving git repo root', { projectRoot });
 	const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
 		cwd: projectRoot,
 		encoding: 'utf-8',
 	});
 
 	if (result.error) {
 		debug(`Failed to retrieve repo root:`, result.error, result.stderr);
 		debug('Falling back to contentPath:', projectRoot);
-		return projectRoot;
+		cachedRepoRoot = projectRoot;
+		return cachedRepoRoot;
 	}
 
-	return result.stdout.trim();
+	cachedRepoRoot = result.stdout.trim();
+	return cachedRepoRoot;
 }

Note: setProjectRoot should also reset the cache (cachedRepoRoot = undefined;) to stay consistent if the root changes.

🤖 Prompt for AI Agents
In `@packages/content-utils/src/runtime/git.ts` around lines 46 - 58,
getRepoRoot() is currently invoking a new git subprocess on every call (causing
many redundant spawnSync calls from getFileContentAtCommit and the pre-fetch
loop); memoize the repo root by introducing a module-level cachedRepoRoot and
have getRepoRoot return the cached value when set, otherwise run spawnSync once
and store the result; update setProjectRoot to clear/reset cachedRepoRoot (e.g.,
set cachedRepoRoot = undefined) so changes to the root invalidate the cache;
then ensure getFileContentAtCommit and any other callers use the memoized
getRepoRoot.

Comment thread packages/content-utils/tests/git-tracking.test.ts

@Fryuni Fryuni left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

sent from aretui

Comment thread .gitignore
Comment on lines +68 to +70

# Nested git repos in test fixtures (unpacked from .git.tar.gz during tests)
packages/*/tests/fixture/**/.git

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

testing new pr review tool

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant