mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge main into massive-terminal-upgrade
Resolves merge conflicts: - apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger - apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions - apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling) - apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes - apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,18 +2,14 @@
|
||||
* Common utilities for worktree routes
|
||||
*/
|
||||
|
||||
import { createLogger } from "../../lib/logger.js";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import {
|
||||
getErrorMessage as getErrorMessageShared,
|
||||
createLogError,
|
||||
} from "../common.js";
|
||||
import { FeatureLoader } from "../../services/feature-loader.js";
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||
import { FeatureLoader } from '../../services/feature-loader.js';
|
||||
|
||||
const logger = createLogger("Worktree");
|
||||
const logger = createLogger('Worktree');
|
||||
export const execAsync = promisify(exec);
|
||||
const featureLoader = new FeatureLoader();
|
||||
|
||||
@@ -28,10 +24,10 @@ export const MAX_BRANCH_NAME_LENGTH = 250;
|
||||
// Extended PATH configuration for Electron apps
|
||||
// ============================================================================
|
||||
|
||||
const pathSeparator = process.platform === "win32" ? ";" : ":";
|
||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
||||
const additionalPaths: string[] = [];
|
||||
|
||||
if (process.platform === "win32") {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows paths
|
||||
if (process.env.LOCALAPPDATA) {
|
||||
additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`);
|
||||
@@ -39,23 +35,22 @@ if (process.platform === "win32") {
|
||||
if (process.env.PROGRAMFILES) {
|
||||
additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`);
|
||||
}
|
||||
if (process.env["ProgramFiles(x86)"]) {
|
||||
additionalPaths.push(`${process.env["ProgramFiles(x86)"]}\\Git\\cmd`);
|
||||
if (process.env['ProgramFiles(x86)']) {
|
||||
additionalPaths.push(`${process.env['ProgramFiles(x86)']}\\Git\\cmd`);
|
||||
}
|
||||
} else {
|
||||
// Unix/Mac paths
|
||||
additionalPaths.push(
|
||||
"/opt/homebrew/bin", // Homebrew on Apple Silicon
|
||||
"/usr/local/bin", // Homebrew on Intel Mac, common Linux location
|
||||
"/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
|
||||
`${process.env.HOME}/.local/bin`, // pipx, other user installs
|
||||
'/opt/homebrew/bin', // Homebrew on Apple Silicon
|
||||
'/usr/local/bin', // Homebrew on Intel Mac, common Linux location
|
||||
'/home/linuxbrew/.linuxbrew/bin', // Linuxbrew
|
||||
`${process.env.HOME}/.local/bin` // pipx, other user installs
|
||||
);
|
||||
}
|
||||
|
||||
const extendedPath = [
|
||||
process.env.PATH,
|
||||
...additionalPaths.filter(Boolean),
|
||||
].filter(Boolean).join(pathSeparator);
|
||||
const extendedPath = [process.env.PATH, ...additionalPaths.filter(Boolean)]
|
||||
.filter(Boolean)
|
||||
.join(pathSeparator);
|
||||
|
||||
/**
|
||||
* Environment variables with extended PATH for executing shell commands.
|
||||
@@ -85,9 +80,7 @@ export function isValidBranchName(name: string): boolean {
|
||||
*/
|
||||
export async function isGhCliAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const checkCommand = process.platform === "win32"
|
||||
? "where gh"
|
||||
: "command -v gh";
|
||||
const checkCommand = process.platform === 'win32' ? 'where gh' : 'command -v gh';
|
||||
await execAsync(checkCommand, { env: execEnv });
|
||||
return true;
|
||||
} catch {
|
||||
@@ -95,8 +88,7 @@ export async function isGhCliAvailable(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export const AUTOMAKER_INITIAL_COMMIT_MESSAGE =
|
||||
"chore: automaker initial commit";
|
||||
export const AUTOMAKER_INITIAL_COMMIT_MESSAGE = 'chore: automaker initial commit';
|
||||
|
||||
/**
|
||||
* Normalize path separators to forward slashes for cross-platform consistency.
|
||||
@@ -104,7 +96,7 @@ export const AUTOMAKER_INITIAL_COMMIT_MESSAGE =
|
||||
* from git commands (which may use forward slashes).
|
||||
*/
|
||||
export function normalizePath(p: string): string {
|
||||
return p.replace(/\\/g, "/");
|
||||
return p.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +104,7 @@ export function normalizePath(p: string): string {
|
||||
*/
|
||||
export async function isGitRepo(repoPath: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
|
||||
await execAsync('git rev-parse --is-inside-work-tree', { cwd: repoPath });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -124,30 +116,21 @@ export async function isGitRepo(repoPath: string): Promise<boolean> {
|
||||
* These are expected in test environments with mock paths
|
||||
*/
|
||||
export function isENOENT(error: unknown): boolean {
|
||||
return (
|
||||
error !== null &&
|
||||
typeof error === "object" &&
|
||||
"code" in error &&
|
||||
error.code === "ENOENT"
|
||||
);
|
||||
return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is a mock/test path that doesn't exist
|
||||
*/
|
||||
export function isMockPath(worktreePath: string): boolean {
|
||||
return worktreePath.startsWith("/mock/") || worktreePath.includes("/mock/");
|
||||
return worktreePath.startsWith('/mock/') || worktreePath.includes('/mock/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally log worktree errors - suppress ENOENT for mock paths
|
||||
* to reduce noise in test output
|
||||
*/
|
||||
export function logWorktreeError(
|
||||
error: unknown,
|
||||
message: string,
|
||||
worktreePath?: string
|
||||
): void {
|
||||
export function logWorktreeError(error: unknown, message: string, worktreePath?: string): void {
|
||||
// Don't log ENOENT errors for mock paths (expected in tests)
|
||||
if (isENOENT(error) && worktreePath && isMockPath(worktreePath)) {
|
||||
return;
|
||||
@@ -165,17 +148,14 @@ export const logError = createLogError(logger);
|
||||
*/
|
||||
export async function ensureInitialCommit(repoPath: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse --verify HEAD", { cwd: repoPath });
|
||||
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}`
|
||||
);
|
||||
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);
|
||||
|
||||
@@ -2,59 +2,64 @@
|
||||
* Worktree routes - HTTP API for git worktree operations
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { createInfoHandler } from "./routes/info.js";
|
||||
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 { createMergeHandler } from "./routes/merge.js";
|
||||
import { createCreateHandler } from "./routes/create.js";
|
||||
import { createDeleteHandler } from "./routes/delete.js";
|
||||
import { createCreatePRHandler } from "./routes/create-pr.js";
|
||||
import { createPRInfoHandler } from "./routes/pr-info.js";
|
||||
import { createCommitHandler } from "./routes/commit.js";
|
||||
import { createPushHandler } from "./routes/push.js";
|
||||
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 { Router } from 'express';
|
||||
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||
import { createInfoHandler } from './routes/info.js';
|
||||
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 { createMergeHandler } from './routes/merge.js';
|
||||
import { createCreateHandler } from './routes/create.js';
|
||||
import { createDeleteHandler } from './routes/delete.js';
|
||||
import { createCreatePRHandler } from './routes/create-pr.js';
|
||||
import { createPRInfoHandler } from './routes/pr-info.js';
|
||||
import { createCommitHandler } from './routes/commit.js';
|
||||
import { createPushHandler } from './routes/push.js';
|
||||
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 { createInitGitHandler } from "./routes/init-git.js";
|
||||
import { createMigrateHandler } from "./routes/migrate.js";
|
||||
import { createStartDevHandler } from "./routes/start-dev.js";
|
||||
import { createStopDevHandler } from "./routes/stop-dev.js";
|
||||
import { createListDevServersHandler } from "./routes/list-dev-servers.js";
|
||||
} from './routes/open-in-editor.js';
|
||||
import { createInitGitHandler } from './routes/init-git.js';
|
||||
import { createMigrateHandler } from './routes/migrate.js';
|
||||
import { createStartDevHandler } from './routes/start-dev.js';
|
||||
import { createStopDevHandler } from './routes/stop-dev.js';
|
||||
import { createListDevServersHandler } from './routes/list-dev-servers.js';
|
||||
|
||||
export function createWorktreeRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post("/info", createInfoHandler());
|
||||
router.post("/status", createStatusHandler());
|
||||
router.post("/list", createListHandler());
|
||||
router.post("/diffs", createDiffsHandler());
|
||||
router.post("/file-diff", createFileDiffHandler());
|
||||
router.post("/merge", createMergeHandler());
|
||||
router.post("/create", createCreateHandler());
|
||||
router.post("/delete", createDeleteHandler());
|
||||
router.post("/create-pr", createCreatePRHandler());
|
||||
router.post("/pr-info", createPRInfoHandler());
|
||||
router.post("/commit", createCommitHandler());
|
||||
router.post("/push", createPushHandler());
|
||||
router.post("/pull", createPullHandler());
|
||||
router.post("/checkout-branch", createCheckoutBranchHandler());
|
||||
router.post("/list-branches", createListBranchesHandler());
|
||||
router.post("/switch-branch", createSwitchBranchHandler());
|
||||
router.post("/open-in-editor", createOpenInEditorHandler());
|
||||
router.get("/default-editor", createGetDefaultEditorHandler());
|
||||
router.post("/init-git", createInitGitHandler());
|
||||
router.post("/migrate", createMigrateHandler());
|
||||
router.post("/start-dev", createStartDevHandler());
|
||||
router.post("/stop-dev", createStopDevHandler());
|
||||
router.post("/list-dev-servers", createListDevServersHandler());
|
||||
router.post('/info', validatePathParams('projectPath'), createInfoHandler());
|
||||
router.post('/status', validatePathParams('projectPath'), createStatusHandler());
|
||||
router.post('/list', createListHandler());
|
||||
router.post('/diffs', validatePathParams('projectPath'), createDiffsHandler());
|
||||
router.post('/file-diff', validatePathParams('projectPath', 'filePath'), createFileDiffHandler());
|
||||
router.post('/merge', validatePathParams('projectPath'), createMergeHandler());
|
||||
router.post('/create', validatePathParams('projectPath'), createCreateHandler());
|
||||
router.post('/delete', validatePathParams('projectPath', 'worktreePath'), createDeleteHandler());
|
||||
router.post('/create-pr', createCreatePRHandler());
|
||||
router.post('/pr-info', createPRInfoHandler());
|
||||
router.post('/commit', validatePathParams('worktreePath'), createCommitHandler());
|
||||
router.post('/push', validatePathParams('worktreePath'), createPushHandler());
|
||||
router.post('/pull', validatePathParams('worktreePath'), createPullHandler());
|
||||
router.post('/checkout-branch', createCheckoutBranchHandler());
|
||||
router.post('/list-branches', validatePathParams('worktreePath'), createListBranchesHandler());
|
||||
router.post('/switch-branch', createSwitchBranchHandler());
|
||||
router.post('/open-in-editor', validatePathParams('worktreePath'), createOpenInEditorHandler());
|
||||
router.get('/default-editor', createGetDefaultEditorHandler());
|
||||
router.post('/init-git', validatePathParams('projectPath'), createInitGitHandler());
|
||||
router.post('/migrate', createMigrateHandler());
|
||||
router.post(
|
||||
'/start-dev',
|
||||
validatePathParams('projectPath', 'worktreePath'),
|
||||
createStartDevHandler()
|
||||
);
|
||||
router.post('/stop-dev', createStopDevHandler());
|
||||
router.post('/list-dev-servers', createListDevServersHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
* can switch between branches even after worktrees are removed.
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import {
|
||||
getBranchTrackingPath,
|
||||
ensureAutomakerDir,
|
||||
} from "../../../lib/automaker-paths.js";
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import path from 'path';
|
||||
import { getBranchTrackingPath, ensureAutomakerDir } from '@automaker/platform';
|
||||
|
||||
export interface TrackedBranch {
|
||||
name: string;
|
||||
@@ -25,19 +22,17 @@ interface BranchTrackingData {
|
||||
/**
|
||||
* Read tracked branches from file
|
||||
*/
|
||||
export async function getTrackedBranches(
|
||||
projectPath: string
|
||||
): Promise<TrackedBranch[]> {
|
||||
export async function getTrackedBranches(projectPath: string): Promise<TrackedBranch[]> {
|
||||
try {
|
||||
const filePath = getBranchTrackingPath(projectPath);
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const content = (await secureFs.readFile(filePath, 'utf-8')) as string;
|
||||
const data: BranchTrackingData = JSON.parse(content);
|
||||
return data.branches || [];
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
if (error.code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
console.warn("[branch-tracking] Failed to read tracked branches:", error);
|
||||
console.warn('[branch-tracking] Failed to read tracked branches:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -45,23 +40,17 @@ export async function getTrackedBranches(
|
||||
/**
|
||||
* Save tracked branches to file
|
||||
*/
|
||||
async function saveTrackedBranches(
|
||||
projectPath: string,
|
||||
branches: TrackedBranch[]
|
||||
): Promise<void> {
|
||||
async function saveTrackedBranches(projectPath: string, branches: TrackedBranch[]): Promise<void> {
|
||||
const automakerDir = await ensureAutomakerDir(projectPath);
|
||||
const filePath = path.join(automakerDir, "active-branches.json");
|
||||
const filePath = path.join(automakerDir, 'active-branches.json');
|
||||
const data: BranchTrackingData = { branches };
|
||||
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
||||
await secureFs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a branch to tracking
|
||||
*/
|
||||
export async function trackBranch(
|
||||
projectPath: string,
|
||||
branchName: string
|
||||
): Promise<void> {
|
||||
export async function trackBranch(projectPath: string, branchName: string): Promise<void> {
|
||||
const branches = await getTrackedBranches(projectPath);
|
||||
|
||||
// Check if already tracked
|
||||
@@ -82,10 +71,7 @@ export async function trackBranch(
|
||||
/**
|
||||
* Remove a branch from tracking
|
||||
*/
|
||||
export async function untrackBranch(
|
||||
projectPath: string,
|
||||
branchName: string
|
||||
): Promise<void> {
|
||||
export async function untrackBranch(projectPath: string, branchName: string): Promise<void> {
|
||||
const branches = await getTrackedBranches(projectPath);
|
||||
const filtered = branches.filter((b) => b.name !== branchName);
|
||||
|
||||
@@ -114,10 +100,7 @@ export async function updateBranchActivation(
|
||||
/**
|
||||
* Check if a branch is tracked
|
||||
*/
|
||||
export async function isBranchTracked(
|
||||
projectPath: string,
|
||||
branchName: string
|
||||
): Promise<boolean> {
|
||||
export async function isBranchTracked(projectPath: string, branchName: string): Promise<boolean> {
|
||||
const branches = await getTrackedBranches(projectPath);
|
||||
return branches.some((b) => b.name === branchName);
|
||||
}
|
||||
|
||||
@@ -7,19 +7,19 @@
|
||||
* 3. Only creates a new worktree if none exists for the branch
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import { mkdir } from "fs/promises";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import {
|
||||
isGitRepo,
|
||||
getErrorMessage,
|
||||
logError,
|
||||
normalizePath,
|
||||
ensureInitialCommit,
|
||||
} from "../common.js";
|
||||
import { trackBranch } from "./branch-tracking.js";
|
||||
} from '../common.js';
|
||||
import { trackBranch } from './branch-tracking.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -31,20 +31,20 @@ async function findExistingWorktreeForBranch(
|
||||
branchName: string
|
||||
): Promise<{ path: string; branch: string } | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git worktree list --porcelain", {
|
||||
const { stdout } = await execAsync('git worktree list --porcelain', {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
const lines = stdout.split("\n");
|
||||
const lines = stdout.split('\n');
|
||||
let currentPath: string | null = null;
|
||||
let currentBranch: string | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("worktree ")) {
|
||||
if (line.startsWith('worktree ')) {
|
||||
currentPath = line.slice(9);
|
||||
} else if (line.startsWith("branch ")) {
|
||||
currentBranch = line.slice(7).replace("refs/heads/", "");
|
||||
} else if (line === "" && currentPath && currentBranch) {
|
||||
} else if (line.startsWith('branch ')) {
|
||||
currentBranch = line.slice(7).replace('refs/heads/', '');
|
||||
} else if (line === '' && currentPath && currentBranch) {
|
||||
// End of a worktree entry
|
||||
if (currentBranch === branchName) {
|
||||
// Resolve to absolute path - git may return relative paths
|
||||
@@ -86,7 +86,7 @@ export function createCreateHandler() {
|
||||
if (!projectPath || !branchName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath and branchName required",
|
||||
error: 'projectPath and branchName required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export function createCreateHandler() {
|
||||
if (!(await isGitRepo(projectPath))) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "Not a git repository",
|
||||
error: 'Not a git repository',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -107,7 +107,9 @@ export function createCreateHandler() {
|
||||
if (existingWorktree) {
|
||||
// Worktree already exists, return it as success (not an error)
|
||||
// This handles manually created worktrees or worktrees from previous runs
|
||||
console.log(`[Worktree] Found existing worktree for branch "${branchName}" at: ${existingWorktree.path}`);
|
||||
console.log(
|
||||
`[Worktree] Found existing worktree for branch "${branchName}" at: ${existingWorktree.path}`
|
||||
);
|
||||
|
||||
// Track the branch so it persists in the UI
|
||||
await trackBranch(projectPath, branchName);
|
||||
@@ -124,12 +126,12 @@ export function createCreateHandler() {
|
||||
}
|
||||
|
||||
// Sanitize branch name for directory usage
|
||||
const sanitizedName = branchName.replace(/[^a-zA-Z0-9_-]/g, "-");
|
||||
const worktreesDir = path.join(projectPath, ".worktrees");
|
||||
const sanitizedName = branchName.replace(/[^a-zA-Z0-9_-]/g, '-');
|
||||
const worktreesDir = path.join(projectPath, '.worktrees');
|
||||
const worktreePath = path.join(worktreesDir, sanitizedName);
|
||||
|
||||
// Create worktrees directory if it doesn't exist
|
||||
await mkdir(worktreesDir, { recursive: true });
|
||||
await secureFs.mkdir(worktreesDir, { recursive: true });
|
||||
|
||||
// Check if branch exists
|
||||
let branchExists = false;
|
||||
@@ -149,7 +151,7 @@ export function createCreateHandler() {
|
||||
createCmd = `git worktree add "${worktreePath}" ${branchName}`;
|
||||
} else {
|
||||
// Create new branch from base or HEAD
|
||||
const base = baseBranch || "HEAD";
|
||||
const base = baseBranch || 'HEAD';
|
||||
createCmd = `git worktree add -b ${branchName} "${worktreePath}" ${base}`;
|
||||
}
|
||||
|
||||
@@ -174,7 +176,7 @@ export function createCreateHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Create worktree failed");
|
||||
logError(error, 'Create worktree failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
* POST /delete endpoint - Delete a git worktree
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { isGitRepo, getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { isGitRepo } from '@automaker/git-utils';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -21,7 +22,7 @@ export function createDeleteHandler() {
|
||||
if (!projectPath || !worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath and worktreePath required",
|
||||
error: 'projectPath and worktreePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -29,7 +30,7 @@ export function createDeleteHandler() {
|
||||
if (!(await isGitRepo(projectPath))) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "Not a git repository",
|
||||
error: 'Not a git repository',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -37,7 +38,7 @@ export function createDeleteHandler() {
|
||||
// Get branch name before removing worktree
|
||||
let branchName: string | null = null;
|
||||
try {
|
||||
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
||||
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
branchName = stdout.trim();
|
||||
@@ -52,11 +53,11 @@ export function createDeleteHandler() {
|
||||
});
|
||||
} catch (error) {
|
||||
// Try with prune if remove fails
|
||||
await execAsync("git worktree prune", { cwd: projectPath });
|
||||
await execAsync('git worktree prune', { cwd: projectPath });
|
||||
}
|
||||
|
||||
// Optionally delete the branch
|
||||
if (deleteBranch && branchName && branchName !== "main" && branchName !== "master") {
|
||||
if (deleteBranch && branchName && branchName !== 'main' && branchName !== 'master') {
|
||||
try {
|
||||
await execAsync(`git branch -D ${branchName}`, { cwd: projectPath });
|
||||
} catch {
|
||||
@@ -72,7 +73,7 @@ export function createDeleteHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Delete worktree failed");
|
||||
logError(error, 'Delete worktree failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* POST /diffs endpoint - Get diffs for a worktree
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { getGitRepositoryDiffs } from "../../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import path from 'path';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { getGitRepositoryDiffs } from '../../common.js';
|
||||
|
||||
export function createDiffsHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -17,21 +17,19 @@ export function createDiffsHandler() {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId required",
|
||||
});
|
||||
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);
|
||||
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||
|
||||
try {
|
||||
// Check if worktree exists
|
||||
await fs.access(worktreePath);
|
||||
await secureFs.access(worktreePath);
|
||||
|
||||
// Get diffs from worktree
|
||||
const result = await getGitRepositoryDiffs(worktreePath);
|
||||
@@ -43,7 +41,7 @@ export function createDiffsHandler() {
|
||||
});
|
||||
} catch (innerError) {
|
||||
// Worktree doesn't exist - fallback to main project path
|
||||
logError(innerError, "Worktree access failed, falling back to main project");
|
||||
logError(innerError, 'Worktree access failed, falling back to main project');
|
||||
|
||||
try {
|
||||
const result = await getGitRepositoryDiffs(projectPath);
|
||||
@@ -54,12 +52,12 @@ export function createDiffsHandler() {
|
||||
hasChanges: result.hasChanges,
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
logError(fallbackError, "Fallback to main project also failed");
|
||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
||||
logError(fallbackError, 'Fallback to main project also failed');
|
||||
res.json({ success: true, diff: '', files: [], hasChanges: false });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Get worktree diffs failed");
|
||||
logError(error, 'Get worktree diffs failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
* POST /file-diff endpoint - Get diff for a specific file
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { generateSyntheticDiffForNewFile } from "../../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { generateSyntheticDiffForNewFile } from '../../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -24,24 +24,23 @@ export function createFileDiffHandler() {
|
||||
if (!projectPath || !featureId || !filePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath, featureId, and filePath required",
|
||||
error: 'projectPath, featureId, and filePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Git worktrees are stored in project directory
|
||||
const worktreePath = path.join(projectPath, ".worktrees", featureId);
|
||||
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||
|
||||
try {
|
||||
await fs.access(worktreePath);
|
||||
await secureFs.access(worktreePath);
|
||||
|
||||
// First check if the file is untracked
|
||||
const { stdout: status } = await execAsync(
|
||||
`git status --porcelain -- "${filePath}"`,
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
const { stdout: status } = await execAsync(`git status --porcelain -- "${filePath}"`, {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
|
||||
const isUntracked = status.trim().startsWith("??");
|
||||
const isUntracked = status.trim().startsWith('??');
|
||||
|
||||
let diff: string;
|
||||
if (isUntracked) {
|
||||
@@ -49,23 +48,20 @@ export function createFileDiffHandler() {
|
||||
diff = await generateSyntheticDiffForNewFile(worktreePath, filePath);
|
||||
} else {
|
||||
// Use regular git diff for tracked files
|
||||
const result = await execAsync(
|
||||
`git diff HEAD -- "${filePath}"`,
|
||||
{
|
||||
cwd: worktreePath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
}
|
||||
);
|
||||
const result = await execAsync(`git diff HEAD -- "${filePath}"`, {
|
||||
cwd: worktreePath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
diff = result.stdout;
|
||||
}
|
||||
|
||||
res.json({ success: true, diff, filePath });
|
||||
} catch (innerError) {
|
||||
logError(innerError, "Worktree file diff failed");
|
||||
res.json({ success: true, diff: "", filePath });
|
||||
logError(innerError, 'Worktree file diff failed');
|
||||
res.json({ success: true, diff: '', filePath });
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Get worktree file diff failed");
|
||||
logError(error, 'Get worktree file diff failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* POST /info endpoint - Get worktree info
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { getErrorMessage, logError, normalizePath } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { getErrorMessage, logError, normalizePath } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -20,20 +20,18 @@ export function createInfoHandler() {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath and featureId required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if worktree exists (git worktrees are stored in project directory)
|
||||
const worktreePath = path.join(projectPath, ".worktrees", featureId);
|
||||
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||
try {
|
||||
await fs.access(worktreePath);
|
||||
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
||||
await secureFs.access(worktreePath);
|
||||
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
res.json({
|
||||
@@ -45,7 +43,7 @@ export function createInfoHandler() {
|
||||
res.json({ success: true, worktreePath: null, branchName: null });
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Get worktree info failed");
|
||||
logError(error, 'Get worktree info failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* POST /init-git endpoint - Initialize a git repository in a directory
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { join } from 'path';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -21,39 +21,42 @@ export function createInitGitHandler() {
|
||||
if (!projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath required",
|
||||
error: 'projectPath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if .git already exists
|
||||
const gitDirPath = join(projectPath, ".git");
|
||||
if (existsSync(gitDirPath)) {
|
||||
const gitDirPath = join(projectPath, '.git');
|
||||
try {
|
||||
await secureFs.access(gitDirPath);
|
||||
// .git exists
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
initialized: false,
|
||||
message: "Git repository already exists",
|
||||
message: 'Git repository already exists',
|
||||
},
|
||||
});
|
||||
return;
|
||||
} catch {
|
||||
// .git doesn't exist, continue with initialization
|
||||
}
|
||||
|
||||
// Initialize git and create an initial empty commit
|
||||
await execAsync(
|
||||
`git init && git commit --allow-empty -m "Initial commit"`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
await execAsync(`git init && git commit --allow-empty -m "Initial commit"`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
initialized: true,
|
||||
message: "Git repository initialized with initial commit",
|
||||
message: 'Git repository initialized with initial commit',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Init git failed");
|
||||
logError(error, 'Init git failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
* Does NOT include tracked branches - only real worktrees with separate directories.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync } from "fs";
|
||||
import { isGitRepo, getErrorMessage, logError, normalizePath } from "../common.js";
|
||||
import { readAllWorktreeMetadata, type WorktreePRInfo } from "../../../lib/worktree-metadata.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { isGitRepo } from '@automaker/git-utils';
|
||||
import { getErrorMessage, logError, normalizePath } from '../common.js';
|
||||
import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -27,10 +28,10 @@ interface WorktreeInfo {
|
||||
|
||||
async function getCurrentBranch(cwd: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git branch --show-current", { cwd });
|
||||
const { stdout } = await execAsync('git branch --show-current', { cwd });
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +44,7 @@ export function createListHandler() {
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath required" });
|
||||
res.status(400).json({ success: false, error: 'projectPath required' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,28 +57,35 @@ export function createListHandler() {
|
||||
const currentBranch = await getCurrentBranch(projectPath);
|
||||
|
||||
// Get actual worktrees from git
|
||||
const { stdout } = await execAsync("git worktree list --porcelain", {
|
||||
const { stdout } = await execAsync('git worktree list --porcelain', {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
const worktrees: WorktreeInfo[] = [];
|
||||
const removedWorktrees: Array<{ path: string; branch: string }> = [];
|
||||
const lines = stdout.split("\n");
|
||||
const lines = stdout.split('\n');
|
||||
let current: { path?: string; branch?: string } = {};
|
||||
let isFirst = true;
|
||||
|
||||
// First pass: detect removed worktrees
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("worktree ")) {
|
||||
if (line.startsWith('worktree ')) {
|
||||
current.path = normalizePath(line.slice(9));
|
||||
} else if (line.startsWith("branch ")) {
|
||||
current.branch = line.slice(7).replace("refs/heads/", "");
|
||||
} else if (line === "") {
|
||||
} else if (line.startsWith('branch ')) {
|
||||
current.branch = line.slice(7).replace('refs/heads/', '');
|
||||
} else if (line === '') {
|
||||
if (current.path && current.branch) {
|
||||
const isMainWorktree = isFirst;
|
||||
// Check if the worktree directory actually exists
|
||||
// Skip checking/pruning the main worktree (projectPath itself)
|
||||
if (!isMainWorktree && !existsSync(current.path)) {
|
||||
let worktreeExists = false;
|
||||
try {
|
||||
await secureFs.access(current.path);
|
||||
worktreeExists = true;
|
||||
} catch {
|
||||
worktreeExists = false;
|
||||
}
|
||||
if (!isMainWorktree && !worktreeExists) {
|
||||
// Worktree directory doesn't exist - it was manually deleted
|
||||
removedWorktrees.push({
|
||||
path: current.path,
|
||||
@@ -102,7 +110,7 @@ export function createListHandler() {
|
||||
// Prune removed worktrees from git (only if any were detected)
|
||||
if (removedWorktrees.length > 0) {
|
||||
try {
|
||||
await execAsync("git worktree prune", { cwd: projectPath });
|
||||
await execAsync('git worktree prune', { cwd: projectPath });
|
||||
} catch {
|
||||
// Prune failed, but we'll still report the removed worktrees
|
||||
}
|
||||
@@ -115,13 +123,12 @@ export function createListHandler() {
|
||||
if (includeDetails) {
|
||||
for (const worktree of worktrees) {
|
||||
try {
|
||||
const { stdout: statusOutput } = await execAsync(
|
||||
"git status --porcelain",
|
||||
{ cwd: worktree.path }
|
||||
);
|
||||
const { stdout: statusOutput } = await execAsync('git status --porcelain', {
|
||||
cwd: worktree.path,
|
||||
});
|
||||
const changedFiles = statusOutput
|
||||
.trim()
|
||||
.split("\n")
|
||||
.split('\n')
|
||||
.filter((line) => line.trim());
|
||||
worktree.hasChanges = changedFiles.length > 0;
|
||||
worktree.changedFilesCount = changedFiles.length;
|
||||
@@ -140,13 +147,13 @@ export function createListHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
res.json({
|
||||
success: true,
|
||||
worktrees,
|
||||
removedWorktrees: removedWorktrees.length > 0 ? removedWorktrees : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "List worktrees failed");
|
||||
logError(error, 'List worktrees failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* any migration since .automaker is now stored in the project directory.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getAutomakerDir } from "../../../lib/automaker-paths.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getAutomakerDir } from '@automaker/platform';
|
||||
|
||||
export function createMigrateHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,7 +15,7 @@ export function createMigrateHandler() {
|
||||
if (!projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath is required",
|
||||
error: 'projectPath is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export function createMigrateHandler() {
|
||||
res.json({
|
||||
success: true,
|
||||
migrated: false,
|
||||
message: "No migration needed - .automaker is stored in project directory",
|
||||
message: 'No migration needed - .automaker is stored in project directory',
|
||||
path: automakerDir,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* POST /status endpoint - Get worktree status
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import * as secureFs from '../../../lib/secure-fs.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -20,53 +20,50 @@ export function createStatusHandler() {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId required",
|
||||
});
|
||||
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);
|
||||
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||
|
||||
try {
|
||||
await fs.access(worktreePath);
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
await secureFs.access(worktreePath);
|
||||
const { stdout: status } = await execAsync('git status --porcelain', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const files = status
|
||||
.split("\n")
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((line) => line.slice(3));
|
||||
const { stdout: diffStat } = await execAsync("git diff --stat", {
|
||||
const { stdout: diffStat } = await execAsync('git diff --stat', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const { stdout: logOutput } = await execAsync('git log --oneline -5 --format="%h %s"', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const { stdout: logOutput } = await execAsync(
|
||||
'git log --oneline -5 --format="%h %s"',
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
modifiedFiles: files.length,
|
||||
files,
|
||||
diffStat: diffStat.trim(),
|
||||
recentCommits: logOutput.trim().split("\n").filter(Boolean),
|
||||
recentCommits: logOutput.trim().split('\n').filter(Boolean),
|
||||
});
|
||||
} catch {
|
||||
res.json({
|
||||
success: true,
|
||||
modifiedFiles: 0,
|
||||
files: [],
|
||||
diffStat: "",
|
||||
diffStat: '',
|
||||
recentCommits: [],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Get worktree status failed");
|
||||
logError(error, 'Get worktree status failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user