mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
fix: Address review comments
This commit is contained in:
@@ -13,7 +13,11 @@ export function createGitRoutes(): Router {
|
||||
|
||||
router.post('/diffs', validatePathParams('projectPath'), createDiffsHandler());
|
||||
router.post('/file-diff', validatePathParams('projectPath', 'filePath'), createFileDiffHandler());
|
||||
router.post('/stage-files', validatePathParams('projectPath'), createStageFilesHandler());
|
||||
router.post(
|
||||
'/stage-files',
|
||||
validatePathParams('projectPath', 'files[]'),
|
||||
createStageFilesHandler()
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* POST /stage-files endpoint - Stage or unstage files in the main project
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { Request, Response } from 'express';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { execGitCommand } from '../../../lib/git.js';
|
||||
@@ -39,17 +40,49 @@ export function createStageFilesHandler() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and sanitize each file path to prevent path traversal attacks
|
||||
const base = path.resolve(projectPath) + path.sep;
|
||||
const sanitizedFiles: string[] = [];
|
||||
for (const file of files) {
|
||||
// Reject absolute paths
|
||||
if (path.isAbsolute(file)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid file path (absolute paths not allowed): ${file}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Reject entries containing '..'
|
||||
if (file.includes('..')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid file path (path traversal not allowed): ${file}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Ensure the resolved path stays within the project directory
|
||||
const resolved = path.resolve(path.join(projectPath, file));
|
||||
if (resolved !== path.resolve(projectPath) && !resolved.startsWith(base)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid file path (outside project directory): ${file}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
sanitizedFiles.push(file);
|
||||
}
|
||||
|
||||
if (operation === 'stage') {
|
||||
await execGitCommand(['add', '--', ...files], projectPath);
|
||||
await execGitCommand(['add', '--', ...sanitizedFiles], projectPath);
|
||||
} else {
|
||||
await execGitCommand(['reset', 'HEAD', '--', ...files], projectPath);
|
||||
await execGitCommand(['reset', 'HEAD', '--', ...sanitizedFiles], projectPath);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
operation,
|
||||
filesCount: files.length,
|
||||
filesCount: sanitizedFiles.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -298,7 +298,7 @@ export function createWorktreeRoutes(
|
||||
// Stage/unstage files route
|
||||
router.post(
|
||||
'/stage-files',
|
||||
validatePathParams('worktreePath'),
|
||||
validatePathParams('worktreePath', 'files[]'),
|
||||
requireGitRepoOnly,
|
||||
createStageFilesHandler()
|
||||
);
|
||||
|
||||
@@ -58,9 +58,9 @@ interface WorktreeInfo {
|
||||
hasChanges?: boolean;
|
||||
changedFilesCount?: number;
|
||||
pr?: WorktreePRInfo; // PR info if a PR has been created for this branch
|
||||
/** Whether a merge or rebase is in progress (has conflicts) */
|
||||
/** Whether there are actual unresolved conflict files (conflictFiles.length > 0) */
|
||||
hasConflicts?: boolean;
|
||||
/** Type of conflict operation in progress */
|
||||
/** Type of git operation in progress (merge/rebase/cherry-pick), set independently of hasConflicts */
|
||||
conflictType?: 'merge' | 'rebase' | 'cherry-pick';
|
||||
/** List of files with conflicts */
|
||||
conflictFiles?: string[];
|
||||
@@ -80,6 +80,7 @@ async function detectConflictState(worktreePath: string): Promise<{
|
||||
// Find the canonical .git directory for this worktree
|
||||
const { stdout: gitDirRaw } = await execAsync('git rev-parse --git-dir', {
|
||||
cwd: worktreePath,
|
||||
timeout: 15000,
|
||||
});
|
||||
const gitDir = path.resolve(worktreePath, gitDirRaw.trim());
|
||||
|
||||
@@ -122,6 +123,7 @@ async function detectConflictState(worktreePath: string): Promise<{
|
||||
try {
|
||||
const { stdout: statusOutput } = await execAsync('git diff --name-only --diff-filter=U', {
|
||||
cwd: worktreePath,
|
||||
timeout: 15000,
|
||||
});
|
||||
conflictFiles = statusOutput
|
||||
.trim()
|
||||
@@ -132,7 +134,7 @@ async function detectConflictState(worktreePath: string): Promise<{
|
||||
}
|
||||
|
||||
return {
|
||||
hasConflicts: true,
|
||||
hasConflicts: conflictFiles.length > 0,
|
||||
conflictType,
|
||||
conflictFiles,
|
||||
};
|
||||
@@ -476,11 +478,14 @@ export function createListHandler() {
|
||||
// Detect merge/rebase/cherry-pick in progress
|
||||
try {
|
||||
const conflictState = await detectConflictState(worktree.path);
|
||||
if (conflictState.hasConflicts) {
|
||||
worktree.hasConflicts = true;
|
||||
// Always propagate conflictType so callers know an operation is in progress,
|
||||
// even when there are no unresolved conflict files yet.
|
||||
if (conflictState.conflictType) {
|
||||
worktree.conflictType = conflictState.conflictType;
|
||||
worktree.conflictFiles = conflictState.conflictFiles;
|
||||
}
|
||||
// hasConflicts is true only when there are actual unresolved files
|
||||
worktree.hasConflicts = conflictState.hasConflicts;
|
||||
worktree.conflictFiles = conflictState.conflictFiles;
|
||||
} catch {
|
||||
// Ignore conflict detection errors
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
* the requireGitRepoOnly middleware in index.ts
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import type { Request, Response } from 'express';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { execGitCommand } from '../../../lib/git.js';
|
||||
@@ -46,19 +48,70 @@ export function createStageFilesHandler() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Canonicalize the worktree root by resolving symlinks so that
|
||||
// path-traversal checks are reliable even when symlinks are involved.
|
||||
let canonicalRoot: string;
|
||||
try {
|
||||
canonicalRoot = await fs.realpath(worktreePath);
|
||||
} catch {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'worktreePath does not exist or is not accessible',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and sanitize each file path to prevent path traversal attacks.
|
||||
// Each file entry is resolved against the canonicalized worktree root and
|
||||
// must remain within that root directory.
|
||||
const base = canonicalRoot + path.sep;
|
||||
const sanitizedFiles: string[] = [];
|
||||
for (const file of files) {
|
||||
// Reject absolute paths
|
||||
if (path.isAbsolute(file)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid file path (absolute paths not allowed): ${file}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Reject entries containing '..'
|
||||
if (file.includes('..')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid file path (path traversal not allowed): ${file}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Resolve the file path against the canonicalized worktree root and
|
||||
// ensure the result stays within the worktree directory.
|
||||
const resolved = path.resolve(canonicalRoot, file);
|
||||
if (resolved !== canonicalRoot && !resolved.startsWith(base)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid file path (outside worktree directory): ${file}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Forward only the original relative path to git — git interprets
|
||||
// paths relative to its working directory (canonicalRoot / worktreePath),
|
||||
// so we do not need to pass the resolved absolute path.
|
||||
sanitizedFiles.push(file);
|
||||
}
|
||||
|
||||
if (operation === 'stage') {
|
||||
// Stage the specified files
|
||||
await execGitCommand(['add', '--', ...files], worktreePath);
|
||||
await execGitCommand(['add', '--', ...sanitizedFiles], worktreePath);
|
||||
} else {
|
||||
// Unstage the specified files
|
||||
await execGitCommand(['reset', 'HEAD', '--', ...files], worktreePath);
|
||||
await execGitCommand(['reset', 'HEAD', '--', ...sanitizedFiles], worktreePath);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
operation,
|
||||
filesCount: files.length,
|
||||
filesCount: sanitizedFiles.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user