mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat: integrate git repository diff handling into common route
- Added functions to check if a path is a git repository and to parse git status output into a structured format. - Refactored diff handling in both git and worktree routes to utilize the new common functions, improving code reuse and maintainability. - Enhanced error logging for better debugging during git operations. This update streamlines the process of retrieving diffs for both git and non-git directories, ensuring a consistent approach across the application.
This commit is contained in:
@@ -5,9 +5,14 @@
|
|||||||
import { createLogger } from "../lib/logger.js";
|
import { createLogger } from "../lib/logger.js";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
type Logger = ReturnType<typeof createLogger>;
|
type Logger = ReturnType<typeof createLogger>;
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
const logger = createLogger("Common");
|
||||||
|
|
||||||
// Max file size for generating synthetic diffs (1MB)
|
// Max file size for generating synthetic diffs (1MB)
|
||||||
const MAX_SYNTHETIC_DIFF_SIZE = 1024 * 1024;
|
const MAX_SYNTHETIC_DIFF_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
@@ -23,6 +28,26 @@ const BINARY_EXTENSIONS = new Set([
|
|||||||
".pyc", ".pyo", ".class", ".o", ".obj",
|
".pyc", ".pyo", ".class", ".o", ".obj",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Status map for git status codes
|
||||||
|
const GIT_STATUS_MAP: Record<string, string> = {
|
||||||
|
M: "Modified",
|
||||||
|
A: "Added",
|
||||||
|
D: "Deleted",
|
||||||
|
R: "Renamed",
|
||||||
|
C: "Copied",
|
||||||
|
U: "Updated",
|
||||||
|
"?": "Untracked",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File status interface for git status results
|
||||||
|
*/
|
||||||
|
export interface FileStatus {
|
||||||
|
status: string;
|
||||||
|
path: string;
|
||||||
|
statusText: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a file is likely binary based on extension
|
* Check if a file is likely binary based on extension
|
||||||
*/
|
*/
|
||||||
@@ -31,6 +56,36 @@ function isBinaryFile(filePath: string): boolean {
|
|||||||
return BINARY_EXTENSIONS.has(ext);
|
return BINARY_EXTENSIONS.has(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a path is a git repository
|
||||||
|
*/
|
||||||
|
export async function isGitRepo(repoPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the output of `git status --porcelain` into FileStatus array
|
||||||
|
*/
|
||||||
|
export function parseGitStatus(statusOutput: string): FileStatus[] {
|
||||||
|
return statusOutput
|
||||||
|
.split("\n")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const statusChar = line[0];
|
||||||
|
const filePath = line.slice(3);
|
||||||
|
return {
|
||||||
|
status: statusChar,
|
||||||
|
path: filePath,
|
||||||
|
statusText: GIT_STATUS_MAP[statusChar] || "Unknown",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a synthetic unified diff for an untracked (new) file
|
* Generate a synthetic unified diff for an untracked (new) file
|
||||||
* This is needed because `git diff HEAD` doesn't include untracked files
|
* This is needed because `git diff HEAD` doesn't include untracked files
|
||||||
@@ -67,10 +122,11 @@ index 0000000..0000000
|
|||||||
|
|
||||||
// Read file content
|
// Read file content
|
||||||
const content = await fs.readFile(fullPath, "utf-8");
|
const content = await fs.readFile(fullPath, "utf-8");
|
||||||
|
const hasTrailingNewline = content.endsWith("\n");
|
||||||
const lines = content.split("\n");
|
const lines = content.split("\n");
|
||||||
|
|
||||||
// Remove trailing empty line if the file ends with newline
|
// Remove trailing empty line if the file ends with newline
|
||||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
if (lines.length > 0 && lines.at(-1) === "") {
|
||||||
lines.pop();
|
lines.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,16 +134,24 @@ index 0000000..0000000
|
|||||||
const lineCount = lines.length;
|
const lineCount = lines.length;
|
||||||
const addedLines = lines.map(line => `+${line}`).join("\n");
|
const addedLines = lines.map(line => `+${line}`).join("\n");
|
||||||
|
|
||||||
return `diff --git a/${relativePath} b/${relativePath}
|
let diff = `diff --git a/${relativePath} b/${relativePath}
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..0000000
|
index 0000000..0000000
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/${relativePath}
|
+++ b/${relativePath}
|
||||||
@@ -0,0 +1,${lineCount} @@
|
@@ -0,0 +1,${lineCount} @@
|
||||||
${addedLines}
|
${addedLines}`;
|
||||||
`;
|
|
||||||
|
// Add "No newline at end of file" indicator if needed
|
||||||
|
if (!hasTrailingNewline && content.length > 0) {
|
||||||
|
diff += "\n\\ No newline at end of file";
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff + "\n";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If we can't read the file, return a placeholder diff
|
// Log the error for debugging
|
||||||
|
logger.error(`Failed to generate synthetic diff for ${fullPath}:`, error);
|
||||||
|
// Return a placeholder diff
|
||||||
return `diff --git a/${relativePath} b/${relativePath}
|
return `diff --git a/${relativePath} b/${relativePath}
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..0000000
|
index 0000000..0000000
|
||||||
@@ -162,8 +226,9 @@ export async function listAllFilesInDirectory(
|
|||||||
files.push(entryRelPath);
|
files.push(entryRelPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
// Ignore errors (permission denied, etc.)
|
// Log the error to help diagnose file system issues
|
||||||
|
logger.error(`Error reading directory ${fullPath}:`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
@@ -175,10 +240,10 @@ export async function listAllFilesInDirectory(
|
|||||||
*/
|
*/
|
||||||
export async function generateDiffsForNonGitDirectory(
|
export async function generateDiffsForNonGitDirectory(
|
||||||
basePath: string
|
basePath: string
|
||||||
): Promise<{ diff: string; files: Array<{ status: string; path: string; statusText: string }> }> {
|
): Promise<{ diff: string; files: FileStatus[] }> {
|
||||||
const allFiles = await listAllFilesInDirectory(basePath);
|
const allFiles = await listAllFilesInDirectory(basePath);
|
||||||
|
|
||||||
const files = allFiles.map(filePath => ({
|
const files: FileStatus[] = allFiles.map(filePath => ({
|
||||||
status: "?",
|
status: "?",
|
||||||
path: filePath,
|
path: filePath,
|
||||||
statusText: "New",
|
statusText: "New",
|
||||||
@@ -195,6 +260,47 @@ export async function generateDiffsForNonGitDirectory(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get git repository diffs for a given path
|
||||||
|
* Handles both git repos and non-git directories
|
||||||
|
*/
|
||||||
|
export async function getGitRepositoryDiffs(
|
||||||
|
repoPath: string
|
||||||
|
): Promise<{ diff: string; files: FileStatus[]; hasChanges: boolean }> {
|
||||||
|
// Check if it's a git repository
|
||||||
|
const isRepo = await isGitRepo(repoPath);
|
||||||
|
|
||||||
|
if (!isRepo) {
|
||||||
|
// Not a git repo - list all files and treat them as new
|
||||||
|
const result = await generateDiffsForNonGitDirectory(repoPath);
|
||||||
|
return {
|
||||||
|
diff: result.diff,
|
||||||
|
files: result.files,
|
||||||
|
hasChanges: result.files.length > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get git diff and status
|
||||||
|
const { stdout: diff } = await execAsync("git diff HEAD", {
|
||||||
|
cwd: repoPath,
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||||
|
cwd: repoPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = parseGitStatus(status);
|
||||||
|
|
||||||
|
// Generate synthetic diffs for untracked (new) files
|
||||||
|
const combinedDiff = await appendUntrackedFileDiffs(repoPath, diff, files);
|
||||||
|
|
||||||
|
return {
|
||||||
|
diff: combinedDiff,
|
||||||
|
files,
|
||||||
|
hasChanges: files.length > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get error message from error object
|
* Get error message from error object
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,24 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { exec } from "child_process";
|
|
||||||
import { promisify } from "util";
|
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from "../common.js";
|
||||||
import { appendUntrackedFileDiffs, generateDiffsForNonGitDirectory } from "../../common.js";
|
import { getGitRepositoryDiffs } from "../../common.js";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a path is a git repository
|
|
||||||
*/
|
|
||||||
async function isGitRepo(repoPath: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDiffsHandler() {
|
export function createDiffsHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -32,70 +16,16 @@ export function createDiffsHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a git repository
|
|
||||||
const isRepo = await isGitRepo(projectPath);
|
|
||||||
|
|
||||||
if (!isRepo) {
|
|
||||||
// Not a git repo - list all files and treat them as new
|
|
||||||
try {
|
try {
|
||||||
const result = await generateDiffsForNonGitDirectory(projectPath);
|
const result = await getGitRepositoryDiffs(projectPath);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
diff: result.diff,
|
diff: result.diff,
|
||||||
files: result.files,
|
files: result.files,
|
||||||
hasChanges: result.files.length > 0,
|
hasChanges: result.hasChanges,
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, "Failed to list files in non-git directory");
|
|
||||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { stdout: diff } = await execAsync("git diff HEAD", {
|
|
||||||
cwd: projectPath,
|
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
|
||||||
});
|
|
||||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
|
||||||
cwd: projectPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const files = status
|
|
||||||
.split("\n")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((line) => {
|
|
||||||
const statusChar = line[0];
|
|
||||||
const filePath = line.slice(3);
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
M: "Modified",
|
|
||||||
A: "Added",
|
|
||||||
D: "Deleted",
|
|
||||||
R: "Renamed",
|
|
||||||
C: "Copied",
|
|
||||||
U: "Updated",
|
|
||||||
"?": "Untracked",
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
status: statusChar,
|
|
||||||
path: filePath,
|
|
||||||
statusText: statusMap[statusChar] || "Unknown",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate synthetic diffs for untracked (new) files
|
|
||||||
// git diff HEAD doesn't include untracked files, so we need to generate them
|
|
||||||
const combinedDiff = await appendUntrackedFileDiffs(projectPath, diff, files);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
diff: combinedDiff,
|
|
||||||
files,
|
|
||||||
hasChanges: files.length > 0,
|
|
||||||
});
|
});
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
// Log the error for debugging instead of silently swallowing it
|
logError(innerError, "Git diff failed");
|
||||||
logError(innerError, "Git command failed");
|
|
||||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -3,26 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { exec } from "child_process";
|
|
||||||
import { promisify } from "util";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from "../common.js";
|
||||||
import { appendUntrackedFileDiffs, generateDiffsForNonGitDirectory } from "../../common.js";
|
import { getGitRepositoryDiffs } from "../../common.js";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a path is a git repository
|
|
||||||
*/
|
|
||||||
async function isGitRepo(repoPath: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDiffsHandler() {
|
export function createDiffsHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -50,121 +34,28 @@ export function createDiffsHandler() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if worktree exists
|
||||||
await fs.access(worktreePath);
|
await fs.access(worktreePath);
|
||||||
|
|
||||||
// Check if worktree is a git repository
|
// Get diffs from worktree
|
||||||
const isRepo = await isGitRepo(worktreePath);
|
const result = await getGitRepositoryDiffs(worktreePath);
|
||||||
|
|
||||||
if (!isRepo) {
|
|
||||||
// Not a git repo - list all files and treat them as new
|
|
||||||
const result = await generateDiffsForNonGitDirectory(worktreePath);
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
diff: result.diff,
|
diff: result.diff,
|
||||||
files: result.files,
|
files: result.files,
|
||||||
hasChanges: result.files.length > 0,
|
hasChanges: result.hasChanges,
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { stdout: diff } = await execAsync("git diff HEAD", {
|
|
||||||
cwd: worktreePath,
|
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
|
||||||
});
|
|
||||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
|
||||||
cwd: worktreePath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const files = status
|
|
||||||
.split("\n")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((line) => {
|
|
||||||
const statusChar = line[0];
|
|
||||||
const filePath = line.slice(3);
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
M: "Modified",
|
|
||||||
A: "Added",
|
|
||||||
D: "Deleted",
|
|
||||||
R: "Renamed",
|
|
||||||
C: "Copied",
|
|
||||||
U: "Updated",
|
|
||||||
"?": "Untracked",
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
status: statusChar,
|
|
||||||
path: filePath,
|
|
||||||
statusText: statusMap[statusChar] || "Unknown",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate synthetic diffs for untracked (new) files
|
|
||||||
// git diff HEAD doesn't include untracked files, so we need to generate them
|
|
||||||
const combinedDiff = await appendUntrackedFileDiffs(worktreePath, diff, files);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
diff: combinedDiff,
|
|
||||||
files,
|
|
||||||
hasChanges: files.length > 0,
|
|
||||||
});
|
});
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
// Worktree doesn't exist - fallback to main project path
|
// 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 {
|
try {
|
||||||
// Check if main project is a git repo
|
const result = await getGitRepositoryDiffs(projectPath);
|
||||||
const isRepo = await isGitRepo(projectPath);
|
|
||||||
|
|
||||||
if (!isRepo) {
|
|
||||||
// Not a git repo - list all files and treat them as new
|
|
||||||
const result = await generateDiffsForNonGitDirectory(projectPath);
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
diff: result.diff,
|
diff: result.diff,
|
||||||
files: result.files,
|
files: result.files,
|
||||||
hasChanges: result.files.length > 0,
|
hasChanges: result.hasChanges,
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try main project path for git diffs
|
|
||||||
const { stdout: diff } = await execAsync("git diff HEAD", {
|
|
||||||
cwd: projectPath,
|
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
|
||||||
});
|
|
||||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
|
||||||
cwd: projectPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const files = status
|
|
||||||
.split("\n")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((line) => {
|
|
||||||
const statusChar = line[0];
|
|
||||||
const filePath = line.slice(3);
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
M: "Modified",
|
|
||||||
A: "Added",
|
|
||||||
D: "Deleted",
|
|
||||||
R: "Renamed",
|
|
||||||
C: "Copied",
|
|
||||||
U: "Updated",
|
|
||||||
"?": "Untracked",
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
status: statusChar,
|
|
||||||
path: filePath,
|
|
||||||
statusText: statusMap[statusChar] || "Unknown",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const combinedDiff = await appendUntrackedFileDiffs(projectPath, diff, files);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
diff: combinedDiff,
|
|
||||||
files,
|
|
||||||
hasChanges: files.length > 0,
|
|
||||||
});
|
});
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
logError(fallbackError, "Fallback to main project also failed");
|
logError(fallbackError, "Fallback to main project also failed");
|
||||||
|
|||||||
Reference in New Issue
Block a user