mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-19 22:34:20 +01:00
refactor: move directories
This commit is contained in:
386
src/server/core/git/functions/getDiff.ts
Normal file
386
src/server/core/git/functions/getDiff.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import parseGitDiff, {
|
||||
type AnyChunk,
|
||||
type AnyFileChange,
|
||||
} from "parse-git-diff";
|
||||
import {
|
||||
executeGitCommand,
|
||||
parseLines,
|
||||
stripAnsiColors,
|
||||
} from "../functions/utils";
|
||||
import type {
|
||||
GitComparisonResult,
|
||||
GitDiff,
|
||||
GitDiffFile,
|
||||
GitDiffHunk,
|
||||
GitDiffLine,
|
||||
GitResult,
|
||||
} from "../types";
|
||||
|
||||
/**
|
||||
* Convert parse-git-diff file change to GitDiffFile
|
||||
*/
|
||||
function convertToGitDiffFile(
|
||||
fileChange: AnyFileChange,
|
||||
fileStats: Map<string, { additions: number; deletions: number }>,
|
||||
): GitDiffFile {
|
||||
let filePath: string;
|
||||
let status: GitDiffFile["status"];
|
||||
let oldPath: string | undefined;
|
||||
|
||||
switch (fileChange.type) {
|
||||
case "AddedFile":
|
||||
filePath = fileChange.path;
|
||||
status = "added";
|
||||
break;
|
||||
case "DeletedFile":
|
||||
filePath = fileChange.path;
|
||||
status = "deleted";
|
||||
break;
|
||||
case "RenamedFile":
|
||||
filePath = fileChange.pathAfter;
|
||||
oldPath = fileChange.pathBefore;
|
||||
status = "renamed";
|
||||
break;
|
||||
case "ChangedFile":
|
||||
filePath = fileChange.path;
|
||||
status = "modified";
|
||||
break;
|
||||
default:
|
||||
// Fallback for any unknown types
|
||||
filePath = "";
|
||||
status = "modified";
|
||||
}
|
||||
|
||||
// Get stats from numstat
|
||||
const stats = fileStats.get(filePath) ||
|
||||
fileStats.get(oldPath || "") || { additions: 0, deletions: 0 };
|
||||
|
||||
return {
|
||||
filePath,
|
||||
status,
|
||||
additions: stats.additions,
|
||||
deletions: stats.deletions,
|
||||
oldPath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert parse-git-diff chunk to GitDiffHunk
|
||||
*/
|
||||
function convertToGitDiffHunk(chunk: AnyChunk): GitDiffHunk {
|
||||
if (chunk.type !== "Chunk") {
|
||||
// For non-standard chunks, return empty hunk
|
||||
return {
|
||||
oldStart: 0,
|
||||
oldCount: 0,
|
||||
newStart: 0,
|
||||
newCount: 0,
|
||||
header: "",
|
||||
lines: [],
|
||||
};
|
||||
}
|
||||
|
||||
const lines: GitDiffLine[] = [];
|
||||
|
||||
for (const change of chunk.changes) {
|
||||
let line: GitDiffLine;
|
||||
|
||||
switch (change.type) {
|
||||
case "AddedLine":
|
||||
line = {
|
||||
type: "added",
|
||||
content: change.content,
|
||||
newLineNumber: change.lineAfter,
|
||||
};
|
||||
break;
|
||||
case "DeletedLine":
|
||||
line = {
|
||||
type: "deleted",
|
||||
content: change.content,
|
||||
oldLineNumber: change.lineBefore,
|
||||
};
|
||||
break;
|
||||
case "UnchangedLine":
|
||||
line = {
|
||||
type: "context",
|
||||
content: change.content,
|
||||
oldLineNumber: change.lineBefore,
|
||||
newLineNumber: change.lineAfter,
|
||||
};
|
||||
break;
|
||||
case "MessageLine":
|
||||
// This is likely a hunk header or context line
|
||||
line = {
|
||||
type: "context",
|
||||
content: change.content,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
// Fallback for unknown line types
|
||||
line = {
|
||||
type: "context",
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
return {
|
||||
oldStart: chunk.fromFileRange.start,
|
||||
oldCount: chunk.fromFileRange.lines,
|
||||
newStart: chunk.toFileRange.start,
|
||||
newCount: chunk.toFileRange.lines,
|
||||
header: `@@ -${chunk.fromFileRange.start},${chunk.fromFileRange.lines} +${chunk.toFileRange.start},${chunk.toFileRange.lines} @@${chunk.context ? ` ${chunk.context}` : ""}`,
|
||||
lines,
|
||||
};
|
||||
}
|
||||
|
||||
const extractRef = (refText: string) => {
|
||||
const [group, ref] = refText.split(":");
|
||||
if (group === undefined || ref === undefined) {
|
||||
if (refText === "HEAD") {
|
||||
return "HEAD";
|
||||
}
|
||||
|
||||
if (refText === "working") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid ref text: ${refText}`);
|
||||
}
|
||||
|
||||
return ref;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get untracked files using git status
|
||||
*/
|
||||
async function getUntrackedFiles(cwd: string): Promise<GitResult<string[]>> {
|
||||
const statusResult = await executeGitCommand(
|
||||
["status", "--untracked-files=all", "--short"],
|
||||
cwd,
|
||||
);
|
||||
|
||||
if (!statusResult.success) {
|
||||
return statusResult;
|
||||
}
|
||||
|
||||
try {
|
||||
const untrackedFiles = parseLines(statusResult.data)
|
||||
.map((line) => stripAnsiColors(line)) // Remove ANSI color codes first
|
||||
.filter((line) => line.startsWith("??"))
|
||||
.map((line) => line.slice(3));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: untrackedFiles,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: "PARSE_ERROR",
|
||||
message: `Failed to parse status output: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create artificial diff for an untracked file (all lines as additions)
|
||||
*/
|
||||
async function createUntrackedFileDiff(
|
||||
cwd: string,
|
||||
filePath: string,
|
||||
): Promise<GitDiff | null> {
|
||||
try {
|
||||
const fullPath = resolve(cwd, filePath);
|
||||
const content = await readFile(fullPath, "utf8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
const diffLines: GitDiffLine[] = lines.map((line, index) => ({
|
||||
type: "added" as const,
|
||||
content: line,
|
||||
newLineNumber: index + 1,
|
||||
}));
|
||||
|
||||
const file: GitDiffFile = {
|
||||
filePath,
|
||||
status: "added",
|
||||
additions: lines.length,
|
||||
deletions: 0,
|
||||
};
|
||||
|
||||
const hunk: GitDiffHunk = {
|
||||
oldStart: 0,
|
||||
oldCount: 0,
|
||||
newStart: 1,
|
||||
newCount: lines.length,
|
||||
header: `@@ -0,0 +1,${lines.length} @@`,
|
||||
lines: diffLines,
|
||||
};
|
||||
|
||||
return {
|
||||
file,
|
||||
hunks: [hunk],
|
||||
};
|
||||
} catch (error) {
|
||||
// Skip files that can't be read (e.g., binary files, permission errors)
|
||||
console.warn(`Failed to read untracked file ${filePath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Git diff between two references (branches, commits, tags)
|
||||
*/
|
||||
export const getDiff = async (
|
||||
cwd: string,
|
||||
fromRefText: string,
|
||||
toRefText: string,
|
||||
): Promise<GitResult<GitComparisonResult>> => {
|
||||
const fromRef = extractRef(fromRefText);
|
||||
const toRef = extractRef(toRefText);
|
||||
|
||||
if (fromRef === toRef) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
diffs: [],
|
||||
files: [],
|
||||
summary: {
|
||||
totalFiles: 0,
|
||||
totalAdditions: 0,
|
||||
totalDeletions: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (fromRef === undefined) {
|
||||
throw new Error(`Invalid fromRef: ${fromRefText}`);
|
||||
}
|
||||
|
||||
const commandArgs = toRef === undefined ? [fromRef] : [fromRef, toRef];
|
||||
|
||||
// Get diff with numstat for file statistics
|
||||
const numstatResult = await executeGitCommand(
|
||||
["diff", "--numstat", ...commandArgs],
|
||||
cwd,
|
||||
);
|
||||
|
||||
if (!numstatResult.success) {
|
||||
return numstatResult;
|
||||
}
|
||||
|
||||
// Get diff with full content
|
||||
const diffResult = await executeGitCommand(
|
||||
["diff", "--unified=5", ...commandArgs],
|
||||
cwd,
|
||||
);
|
||||
|
||||
if (!diffResult.success) {
|
||||
return diffResult;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse numstat output to get file statistics
|
||||
const fileStats = new Map<
|
||||
string,
|
||||
{ additions: number; deletions: number }
|
||||
>();
|
||||
const numstatLines = parseLines(numstatResult.data);
|
||||
|
||||
for (const line of numstatLines) {
|
||||
const parts = line.split("\t");
|
||||
if (parts.length >= 3 && parts[0] && parts[1] && parts[2]) {
|
||||
const additions = parts[0] === "-" ? 0 : parseInt(parts[0], 10);
|
||||
const deletions = parts[1] === "-" ? 0 : parseInt(parts[1], 10);
|
||||
const filePath = parts[2];
|
||||
fileStats.set(filePath, { additions, deletions });
|
||||
}
|
||||
}
|
||||
|
||||
// Parse diff output using parse-git-diff
|
||||
const parsedDiff = parseGitDiff(diffResult.data);
|
||||
|
||||
const files: GitDiffFile[] = [];
|
||||
const diffs: GitDiff[] = [];
|
||||
let totalAdditions = 0;
|
||||
let totalDeletions = 0;
|
||||
|
||||
for (const fileChange of parsedDiff.files) {
|
||||
// Convert to GitDiffFile format
|
||||
const file = convertToGitDiffFile(fileChange, fileStats);
|
||||
files.push(file);
|
||||
|
||||
// Convert chunks to hunks
|
||||
const hunks: GitDiffHunk[] = [];
|
||||
for (const chunk of fileChange.chunks) {
|
||||
const hunk = convertToGitDiffHunk(chunk);
|
||||
hunks.push(hunk);
|
||||
}
|
||||
|
||||
diffs.push({
|
||||
file,
|
||||
hunks,
|
||||
});
|
||||
|
||||
totalAdditions += file.additions;
|
||||
totalDeletions += file.deletions;
|
||||
}
|
||||
|
||||
// Include untracked files when comparing to working directory
|
||||
if (toRef === undefined) {
|
||||
const untrackedResult = await getUntrackedFiles(cwd);
|
||||
if (untrackedResult.success) {
|
||||
for (const untrackedFile of untrackedResult.data) {
|
||||
const untrackedDiff = await createUntrackedFileDiff(
|
||||
cwd,
|
||||
untrackedFile,
|
||||
);
|
||||
if (untrackedDiff) {
|
||||
files.push(untrackedDiff.file);
|
||||
diffs.push(untrackedDiff);
|
||||
totalAdditions += untrackedDiff.file.additions;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
files,
|
||||
diffs,
|
||||
summary: {
|
||||
totalFiles: files.length,
|
||||
totalAdditions,
|
||||
totalDeletions,
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: "PARSE_ERROR",
|
||||
message: `Failed to parse diff: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare between two branches (shorthand for getDiff)
|
||||
*/
|
||||
export async function compareBranches(
|
||||
cwd: string,
|
||||
baseBranch: string,
|
||||
targetBranch: string,
|
||||
): Promise<GitResult<GitComparisonResult>> {
|
||||
return getDiff(cwd, baseBranch, targetBranch);
|
||||
}
|
||||
Reference in New Issue
Block a user