mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
feat: enhance worktree management and UI integration
- Refactored BoardView and WorktreeSelector components for improved readability and maintainability, including consistent formatting and structure. - Updated feature handling to ensure correct worktree assignment and reset logic when worktrees are deleted, enhancing user experience. - Enhanced KanbanCard to display priority badges with improved styling and layout. - Removed deprecated revert feature logic from the server and client, streamlining the codebase. - Introduced new tests for feature lifecycle and worktree integration, ensuring robust functionality and error handling.
This commit is contained in:
@@ -22,3 +22,4 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -102,3 +102,4 @@ export function createDeleteApiKeyHandler() {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { createStatusHandler } from "./routes/status.js";
|
||||
import { createListHandler } from "./routes/list.js";
|
||||
import { createDiffsHandler } from "./routes/diffs.js";
|
||||
import { createFileDiffHandler } from "./routes/file-diff.js";
|
||||
import { createRevertHandler } from "./routes/revert.js";
|
||||
import { createMergeHandler } from "./routes/merge.js";
|
||||
import { createCreateHandler } from "./routes/create.js";
|
||||
import { createDeleteHandler } from "./routes/delete.js";
|
||||
@@ -19,9 +18,11 @@ import { createPullHandler } from "./routes/pull.js";
|
||||
import { createCheckoutBranchHandler } from "./routes/checkout-branch.js";
|
||||
import { createListBranchesHandler } from "./routes/list-branches.js";
|
||||
import { createSwitchBranchHandler } from "./routes/switch-branch.js";
|
||||
import { createOpenInEditorHandler, createGetDefaultEditorHandler } from "./routes/open-in-editor.js";
|
||||
import {
|
||||
createOpenInEditorHandler,
|
||||
createGetDefaultEditorHandler,
|
||||
} from "./routes/open-in-editor.js";
|
||||
import { createInitGitHandler } from "./routes/init-git.js";
|
||||
import { createActivateHandler } from "./routes/activate.js";
|
||||
import { createMigrateHandler } from "./routes/migrate.js";
|
||||
import { createStartDevHandler } from "./routes/start-dev.js";
|
||||
import { createStopDevHandler } from "./routes/stop-dev.js";
|
||||
@@ -35,7 +36,6 @@ export function createWorktreeRoutes(): Router {
|
||||
router.post("/list", createListHandler());
|
||||
router.post("/diffs", createDiffsHandler());
|
||||
router.post("/file-diff", createFileDiffHandler());
|
||||
router.post("/revert", createRevertHandler());
|
||||
router.post("/merge", createMergeHandler());
|
||||
router.post("/create", createCreateHandler());
|
||||
router.post("/delete", createDeleteHandler());
|
||||
@@ -49,7 +49,6 @@ export function createWorktreeRoutes(): Router {
|
||||
router.post("/open-in-editor", createOpenInEditorHandler());
|
||||
router.get("/default-editor", createGetDefaultEditorHandler());
|
||||
router.post("/init-git", createInitGitHandler());
|
||||
router.post("/activate", createActivateHandler());
|
||||
router.post("/migrate", createMigrateHandler());
|
||||
router.post("/start-dev", createStartDevHandler());
|
||||
router.post("/stop-dev", createStopDevHandler());
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
/**
|
||||
* POST /activate endpoint - Switch main project to a worktree's branch
|
||||
*
|
||||
* This allows users to "activate" a worktree so their running dev server
|
||||
* (like Vite) shows the worktree's files. It does this by:
|
||||
* 1. Checking for uncommitted changes (fails if found)
|
||||
* 2. Removing the worktree (unlocks the branch)
|
||||
* 3. Checking out that branch in the main directory
|
||||
*
|
||||
* Users should commit their changes before activating a worktree.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function hasUncommittedChanges(cwd: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git status --porcelain", { cwd });
|
||||
// Filter out our own .worktrees directory from the check
|
||||
const lines = stdout.trim().split("\n").filter((line) => {
|
||||
if (!line.trim()) return false;
|
||||
// Exclude .worktrees/ directory (created by automaker)
|
||||
if (line.includes(".worktrees/") || line.endsWith(".worktrees")) return false;
|
||||
return true;
|
||||
});
|
||||
return lines.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentBranch(cwd: string): Promise<string> {
|
||||
const { stdout } = await execAsync("git branch --show-current", { cwd });
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
async function getWorktreeBranch(worktreePath: string): Promise<string> {
|
||||
const { stdout } = await execAsync("git branch --show-current", {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
async function getChangesSummary(cwd: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git status --short", { cwd });
|
||||
const lines = stdout.trim().split("\n").filter((line) => {
|
||||
if (!line.trim()) return false;
|
||||
// Exclude .worktrees/ directory
|
||||
if (line.includes(".worktrees/") || line.endsWith(".worktrees")) return false;
|
||||
return true;
|
||||
});
|
||||
if (lines.length === 0) return "";
|
||||
if (lines.length <= 5) return lines.join(", ");
|
||||
return `${lines.slice(0, 5).join(", ")} and ${lines.length - 5} more files`;
|
||||
} catch {
|
||||
return "unknown changes";
|
||||
}
|
||||
}
|
||||
|
||||
export function createActivateHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, worktreePath } = req.body as {
|
||||
projectPath: string;
|
||||
worktreePath: string | null; // null means switch back to main branch
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentBranch = await getCurrentBranch(projectPath);
|
||||
let targetBranch: string;
|
||||
|
||||
// Check for uncommitted changes in main directory
|
||||
if (await hasUncommittedChanges(projectPath)) {
|
||||
const summary = await getChangesSummary(projectPath);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Cannot switch: you have uncommitted changes in the main directory (${summary}). Please commit your changes first.`,
|
||||
code: "UNCOMMITTED_CHANGES",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (worktreePath) {
|
||||
// Switching to a worktree's branch
|
||||
targetBranch = await getWorktreeBranch(worktreePath);
|
||||
|
||||
// Check for uncommitted changes in the worktree
|
||||
if (await hasUncommittedChanges(worktreePath)) {
|
||||
const summary = await getChangesSummary(worktreePath);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Cannot switch: you have uncommitted changes in the worktree (${summary}). Please commit your changes first.`,
|
||||
code: "UNCOMMITTED_CHANGES",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the worktree (unlocks the branch)
|
||||
console.log(`[activate] Removing worktree at ${worktreePath}...`);
|
||||
await execAsync(`git worktree remove "${worktreePath}" --force`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
// Checkout the branch in main directory
|
||||
console.log(`[activate] Checking out branch ${targetBranch}...`);
|
||||
await execAsync(`git checkout "${targetBranch}"`, { cwd: projectPath });
|
||||
} else {
|
||||
// Switching back to main branch
|
||||
try {
|
||||
const { stdout: mainBranch } = await execAsync(
|
||||
"git symbolic-ref refs/remotes/origin/HEAD --short 2>/dev/null | sed 's@origin/@@' || echo 'main'",
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
targetBranch = mainBranch.trim() || "main";
|
||||
} catch {
|
||||
targetBranch = "main";
|
||||
}
|
||||
|
||||
// Checkout main branch
|
||||
console.log(`[activate] Checking out main branch ${targetBranch}...`);
|
||||
await execAsync(`git checkout "${targetBranch}"`, { cwd: projectPath });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
previousBranch: currentBranch,
|
||||
currentBranch: targetBranch,
|
||||
message: `Switched from ${currentBranch} to ${targetBranch}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Activate worktree failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* POST /revert endpoint - Revert feature (remove worktree)
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export function createRevertHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, featureId } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Git worktrees are stored in project directory
|
||||
const worktreePath = path.join(projectPath, ".worktrees", featureId);
|
||||
|
||||
try {
|
||||
// Remove worktree
|
||||
await execAsync(`git worktree remove "${worktreePath}" --force`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
// Delete branch
|
||||
await execAsync(`git branch -D feature/${featureId}`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
res.json({ success: true, removedPath: worktreePath });
|
||||
} catch (error) {
|
||||
// Worktree might not exist
|
||||
res.json({ success: true, removedPath: null });
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Revert worktree failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user