mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
feat: implement initial commit creation for empty git repositories
- Added a function to ensure that a git repository has at least one commit before executing worktree commands. This function creates an empty initial commit with a predefined message if the repository is empty. - Updated the create route handler to call this function, ensuring smooth operation when adding worktrees to repositories without existing commits. - Introduced integration tests to verify the creation of the initial commit when no commits are present in the repository.
This commit is contained in:
@@ -13,6 +13,9 @@ import {
|
|||||||
const logger = createLogger("Worktree");
|
const logger = createLogger("Worktree");
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
export const AUTOMAKER_INITIAL_COMMIT_MESSAGE =
|
||||||
|
"chore: automaker initial commit";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize path separators to forward slashes for cross-platform consistency.
|
* Normalize path separators to forward slashes for cross-platform consistency.
|
||||||
* This ensures paths from `path.join()` (backslashes on Windows) match paths
|
* This ensures paths from `path.join()` (backslashes on Windows) match paths
|
||||||
@@ -73,3 +76,30 @@ export function logWorktreeError(
|
|||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
export const logError = createLogError(logger);
|
export const logError = createLogError(logger);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the repository has at least one commit so git commands that rely on HEAD work.
|
||||||
|
* Returns true if an empty commit was created, false if the repo already had commits.
|
||||||
|
*/
|
||||||
|
export async function ensureInitialCommit(repoPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await execAsync("git rev-parse --verify HEAD", { cwd: repoPath });
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
await execAsync(
|
||||||
|
`git commit --allow-empty -m "${AUTOMAKER_INITIAL_COMMIT_MESSAGE}"`,
|
||||||
|
{ cwd: repoPath }
|
||||||
|
);
|
||||||
|
logger.info(
|
||||||
|
`[Worktree] Created initial empty commit to enable worktrees in ${repoPath}`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const reason = getErrorMessageShared(error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to create initial git commit. Please commit manually and retry. ${reason}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ import { exec } from "child_process";
|
|||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { mkdir } from "fs/promises";
|
import { mkdir } from "fs/promises";
|
||||||
import { isGitRepo, getErrorMessage, logError, normalizePath } from "../common.js";
|
import {
|
||||||
|
isGitRepo,
|
||||||
|
getErrorMessage,
|
||||||
|
logError,
|
||||||
|
normalizePath,
|
||||||
|
ensureInitialCommit,
|
||||||
|
} from "../common.js";
|
||||||
import { trackBranch } from "./branch-tracking.js";
|
import { trackBranch } from "./branch-tracking.js";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -93,6 +99,9 @@ export function createCreateHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the repository has at least one commit so worktree commands referencing HEAD succeed
|
||||||
|
await ensureInitialCommit(projectPath);
|
||||||
|
|
||||||
// First, check if git already has a worktree for this branch (anywhere)
|
// First, check if git already has a worktree for this branch (anywhere)
|
||||||
const existingWorktree = await findExistingWorktreeForBranch(projectPath, branchName);
|
const existingWorktree = await findExistingWorktreeForBranch(projectPath, branchName);
|
||||||
if (existingWorktree) {
|
if (existingWorktree) {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||||
|
import { createCreateHandler } from "@/routes/worktree/routes/create.js";
|
||||||
|
import { AUTOMAKER_INITIAL_COMMIT_MESSAGE } from "@/routes/worktree/common.js";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import * as fs from "fs/promises";
|
||||||
|
import * as os from "os";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
describe("worktree create route - repositories without commits", () => {
|
||||||
|
let repoPath: string | null = null;
|
||||||
|
|
||||||
|
async function initRepoWithoutCommit() {
|
||||||
|
repoPath = await fs.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), "automaker-no-commit-")
|
||||||
|
);
|
||||||
|
await execAsync("git init", { cwd: repoPath });
|
||||||
|
await execAsync('git config user.email "test@example.com"', {
|
||||||
|
cwd: repoPath,
|
||||||
|
});
|
||||||
|
await execAsync('git config user.name "Test User"', { cwd: repoPath });
|
||||||
|
// Intentionally skip creating an initial commit
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (!repoPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fs.rm(repoPath, { recursive: true, force: true });
|
||||||
|
repoPath = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates an initial commit before adding a worktree when HEAD is missing", async () => {
|
||||||
|
await initRepoWithoutCommit();
|
||||||
|
const handler = createCreateHandler();
|
||||||
|
|
||||||
|
const json = vi.fn();
|
||||||
|
const status = vi.fn().mockReturnThis();
|
||||||
|
const req = {
|
||||||
|
body: { projectPath: repoPath, branchName: "feature/no-head" },
|
||||||
|
} as any;
|
||||||
|
const res = {
|
||||||
|
json,
|
||||||
|
status,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(status).not.toHaveBeenCalled();
|
||||||
|
expect(json).toHaveBeenCalled();
|
||||||
|
const payload = json.mock.calls[0][0];
|
||||||
|
expect(payload.success).toBe(true);
|
||||||
|
|
||||||
|
const { stdout: commitCount } = await execAsync(
|
||||||
|
"git rev-list --count HEAD",
|
||||||
|
{ cwd: repoPath! }
|
||||||
|
);
|
||||||
|
expect(Number(commitCount.trim())).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const { stdout: latestMessage } = await execAsync(
|
||||||
|
"git log -1 --pretty=%B",
|
||||||
|
{ cwd: repoPath! }
|
||||||
|
);
|
||||||
|
expect(latestMessage.trim()).toBe(AUTOMAKER_INITIAL_COMMIT_MESSAGE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user