mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-19 10:43:08 +00:00
feat: Add GPT-5 model variants and improve Codex execution logic. Addressed code review comments
This commit is contained in:
161
apps/server/src/services/commit-log-service.ts
Normal file
161
apps/server/src/services/commit-log-service.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Service for fetching commit log data from a worktree.
|
||||
*
|
||||
* Extracts the heavy Git command execution and parsing logic from the
|
||||
* commit-log route handler so the handler only validates input,
|
||||
* invokes this service, streams lifecycle events, and sends the response.
|
||||
*
|
||||
* Follows the same approach as branch-commit-log-service: a single
|
||||
* `git log --name-only` call with custom separators to fetch both
|
||||
* commit metadata and file lists, avoiding N+1 git invocations.
|
||||
*/
|
||||
|
||||
import { execGitCommand } from '../lib/git.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface CommitLogEntry {
|
||||
hash: string;
|
||||
shortHash: string;
|
||||
author: string;
|
||||
authorEmail: string;
|
||||
date: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
export interface CommitLogResult {
|
||||
branch: string;
|
||||
commits: CommitLogEntry[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Service
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch the commit log for a worktree (HEAD).
|
||||
*
|
||||
* Runs a single `git log --name-only` invocation plus `git rev-parse`
|
||||
* inside the given worktree path and returns a structured result.
|
||||
*
|
||||
* @param worktreePath - Absolute path to the worktree / repository
|
||||
* @param limit - Maximum number of commits to return (clamped 1-100)
|
||||
*/
|
||||
export async function getCommitLog(worktreePath: string, limit: number): Promise<CommitLogResult> {
|
||||
// Clamp limit to a reasonable range
|
||||
const parsedLimit = Number(limit);
|
||||
const commitLimit = Math.min(Math.max(1, Number.isFinite(parsedLimit) ? parsedLimit : 20), 100);
|
||||
|
||||
// Use custom separators to parse both metadata and file lists from
|
||||
// a single git log invocation (same approach as branch-commit-log-service).
|
||||
//
|
||||
// -m causes merge commits to be diffed against each parent so all
|
||||
// files touched by the merge are listed (without -m, --name-only
|
||||
// produces no file output for merge commits because they have 2+ parents).
|
||||
// This means merge commits appear multiple times in the output (once per
|
||||
// parent), so we deduplicate by hash below and merge their file lists.
|
||||
// We over-fetch (2x the limit) to compensate for -m duplicating merge
|
||||
// commit entries, then trim the result to the requested limit.
|
||||
// Use ASCII control characters as record separators – these cannot appear in
|
||||
// git commit messages, so these delimiters are safe regardless of commit
|
||||
// body content. %x00 and %x01 in git's format string emit literal NUL /
|
||||
// SOH bytes respectively.
|
||||
//
|
||||
// COMMIT_SEP (\x00) – marks the start of each commit record.
|
||||
// META_END (\x01) – separates commit metadata from the --name-only file list.
|
||||
//
|
||||
// Full per-commit layout emitted by git:
|
||||
// \x00\n<hash>\n<shorthash>\n...\n<subject>\n<body>\x01<files...>
|
||||
const COMMIT_SEP = '\x00';
|
||||
const META_END = '\x01';
|
||||
const fetchLimit = commitLimit * 2;
|
||||
|
||||
const logOutput = await execGitCommand(
|
||||
[
|
||||
'log',
|
||||
`--max-count=${fetchLimit}`,
|
||||
'-m',
|
||||
'--name-only',
|
||||
`--format=%x00%n%H%n%h%n%an%n%ae%n%aI%n%s%n%b%x01`,
|
||||
],
|
||||
worktreePath
|
||||
);
|
||||
|
||||
// Split output into per-commit blocks and drop the empty first chunk
|
||||
// (the output starts with a NUL commit separator).
|
||||
const commitBlocks = logOutput.split(COMMIT_SEP).filter((block) => block.trim());
|
||||
|
||||
// Use a Map to deduplicate merge commit entries (which appear once per
|
||||
// parent when -m is used) while preserving insertion order.
|
||||
const commitMap = new Map<string, CommitLogEntry>();
|
||||
|
||||
for (const block of commitBlocks) {
|
||||
const metaEndIdx = block.indexOf(META_END);
|
||||
if (metaEndIdx === -1) continue; // malformed block, skip
|
||||
|
||||
// --- Parse metadata (everything before the META_END delimiter) ---
|
||||
const metaRaw = block.substring(0, metaEndIdx);
|
||||
const metaLines = metaRaw.split('\n');
|
||||
|
||||
// The first line may be empty (newline right after COMMIT_SEP), skip it
|
||||
const nonEmptyStart = metaLines.findIndex((l) => l.trim() !== '');
|
||||
if (nonEmptyStart === -1) continue;
|
||||
|
||||
const fields = metaLines.slice(nonEmptyStart);
|
||||
if (fields.length < 6) continue; // need at least hash..subject
|
||||
|
||||
const hash = fields[0].trim();
|
||||
if (!hash) continue; // defensive: skip if hash is empty
|
||||
const shortHash = fields[1]?.trim() ?? '';
|
||||
const author = fields[2]?.trim() ?? '';
|
||||
const authorEmail = fields[3]?.trim() ?? '';
|
||||
const date = fields[4]?.trim() ?? '';
|
||||
const subject = fields[5]?.trim() ?? '';
|
||||
const body = fields.slice(6).join('\n').trim();
|
||||
|
||||
// --- Parse file list (everything after the META_END delimiter) ---
|
||||
const filesRaw = block.substring(metaEndIdx + META_END.length);
|
||||
const blockFiles = filesRaw
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((f) => f.trim());
|
||||
|
||||
// Merge file lists for duplicate entries (merge commits with -m)
|
||||
const existing = commitMap.get(hash);
|
||||
if (existing) {
|
||||
// Add new files to the existing entry's file set
|
||||
const fileSet = new Set(existing.files);
|
||||
for (const f of blockFiles) fileSet.add(f);
|
||||
existing.files = [...fileSet];
|
||||
} else {
|
||||
commitMap.set(hash, {
|
||||
hash,
|
||||
shortHash,
|
||||
author,
|
||||
authorEmail,
|
||||
date,
|
||||
subject,
|
||||
body,
|
||||
files: [...new Set(blockFiles)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Trim to the requested limit (we over-fetched to account for -m duplicates)
|
||||
const commits = [...commitMap.values()].slice(0, commitLimit);
|
||||
|
||||
// Get current branch name
|
||||
const branchOutput = await execGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], worktreePath);
|
||||
const branch = branchOutput.trim();
|
||||
|
||||
return {
|
||||
branch,
|
||||
commits,
|
||||
total: commits.length,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user