Merge pull request #266 from tony-nekola-silk/fix/untracked-directory-diff-display

fix: expand untracked directories to show individual file diffs
This commit is contained in:
Web Dev Cody
2025-12-24 21:53:48 -05:00
committed by GitHub
2 changed files with 65 additions and 20 deletions

View File

@@ -45,36 +45,50 @@ ${addedLines}
/** /**
* 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
*
* If the path is a directory, this will recursively generate diffs for all files inside
*/ */
export async function generateSyntheticDiffForNewFile( export async function generateSyntheticDiffForNewFile(
basePath: string, basePath: string,
relativePath: string relativePath: string
): Promise<string> { ): Promise<string> {
const fullPath = path.join(basePath, relativePath); // Remove trailing slash if present (git status reports directories with trailing /)
const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
const fullPath = path.join(basePath, cleanPath);
try { try {
// Check if it's a binary file
if (isBinaryFile(relativePath)) {
return `diff --git a/${relativePath} b/${relativePath}
new file mode 100644
index 0000000..0000000
Binary file ${relativePath} added
`;
}
// Get file stats to check size and type // Get file stats to check size and type
const stats = await secureFs.stat(fullPath); const stats = await secureFs.stat(fullPath);
// Check if it's a directory (can happen with untracked directories from git status) // Check if it's a directory first (before binary check)
// This handles edge cases like directories named "images.png/"
if (stats.isDirectory()) { if (stats.isDirectory()) {
return createNewFileDiff(relativePath, '040000', ['[Directory]']); const filesInDir = await listAllFilesInDirectory(basePath, cleanPath);
if (filesInDir.length === 0) {
// Empty directory
return createNewFileDiff(cleanPath, '040000', ['[Empty directory]']);
}
// Generate diffs for all files in the directory sequentially
// Using sequential processing to avoid exhausting file descriptors on large directories
const diffs: string[] = [];
for (const filePath of filesInDir) {
diffs.push(await generateSyntheticDiffForNewFile(basePath, filePath));
}
return diffs.join('');
}
// Check if it's a binary file (after directory check to handle dirs with binary extensions)
if (isBinaryFile(cleanPath)) {
return `diff --git a/${cleanPath} b/${cleanPath}
new file mode 100644
index 0000000..0000000
Binary file ${cleanPath} added
`;
} }
if (stats.size > MAX_SYNTHETIC_DIFF_SIZE) { if (stats.size > MAX_SYNTHETIC_DIFF_SIZE) {
const sizeKB = Math.round(stats.size / 1024); const sizeKB = Math.round(stats.size / 1024);
return createNewFileDiff(relativePath, '100644', [ return createNewFileDiff(cleanPath, '100644', [`[File too large to display: ${sizeKB}KB]`]);
`[File too large to display: ${sizeKB}KB]`,
]);
} }
// Read file content // Read file content
@@ -91,11 +105,11 @@ Binary file ${relativePath} added
const lineCount = lines.length; const lineCount = lines.length;
const addedLines = lines.map((line) => `+${line}`).join('\n'); const addedLines = lines.map((line) => `+${line}`).join('\n');
let diff = `diff --git a/${relativePath} b/${relativePath} let diff = `diff --git a/${cleanPath} b/${cleanPath}
new file mode 100644 new file mode 100644
index 0000000..0000000 index 0000000..0000000
--- /dev/null --- /dev/null
+++ b/${relativePath} +++ b/${cleanPath}
@@ -0,0 +1,${lineCount} @@ @@ -0,0 +1,${lineCount} @@
${addedLines}`; ${addedLines}`;
@@ -109,7 +123,7 @@ ${addedLines}`;
// Log the error for debugging // Log the error for debugging
logger.error(`Failed to generate synthetic diff for ${fullPath}:`, error); logger.error(`Failed to generate synthetic diff for ${fullPath}:`, error);
// Return a placeholder diff // Return a placeholder diff
return createNewFileDiff(relativePath, '100644', ['[Unable to read file content]']); return createNewFileDiff(cleanPath, '100644', ['[Unable to read file content]']);
} }
} }

View File

@@ -118,7 +118,7 @@ describe('diff.ts', () => {
expect(diff).toContain('[Unable to read file content]'); expect(diff).toContain('[Unable to read file content]');
}); });
it('should handle directory path gracefully', async () => { it('should handle empty directory path gracefully', async () => {
const dirName = 'some-directory'; const dirName = 'some-directory';
const dirPath = path.join(tempDir, dirName); const dirPath = path.join(tempDir, dirName);
await fs.mkdir(dirPath); await fs.mkdir(dirPath);
@@ -127,7 +127,38 @@ describe('diff.ts', () => {
expect(diff).toContain(`diff --git a/${dirName} b/${dirName}`); expect(diff).toContain(`diff --git a/${dirName} b/${dirName}`);
expect(diff).toContain('new file mode 040000'); expect(diff).toContain('new file mode 040000');
expect(diff).toContain('[Directory]'); expect(diff).toContain('[Empty directory]');
});
it('should expand directory with files and generate diffs for each file', async () => {
const dirName = 'new-feature';
const dirPath = path.join(tempDir, dirName);
await fs.mkdir(dirPath);
await fs.writeFile(path.join(dirPath, 'index.ts'), 'export const foo = 1;\n');
await fs.writeFile(path.join(dirPath, 'utils.ts'), 'export const bar = 2;\n');
const diff = await generateSyntheticDiffForNewFile(tempDir, dirName);
// Should contain diffs for both files in the directory
expect(diff).toContain(`diff --git a/${dirName}/index.ts b/${dirName}/index.ts`);
expect(diff).toContain(`diff --git a/${dirName}/utils.ts b/${dirName}/utils.ts`);
expect(diff).toContain('+export const foo = 1;');
expect(diff).toContain('+export const bar = 2;');
// Should NOT contain a diff for the directory itself
expect(diff).not.toContain('[Empty directory]');
});
it('should handle directory path with trailing slash', async () => {
const dirName = 'trailing-slash-dir';
const dirPath = path.join(tempDir, dirName);
await fs.mkdir(dirPath);
await fs.writeFile(path.join(dirPath, 'file.txt'), 'content\n');
// git status reports untracked directories with trailing slash
const diff = await generateSyntheticDiffForNewFile(tempDir, `${dirName}/`);
expect(diff).toContain(`diff --git a/${dirName}/file.txt b/${dirName}/file.txt`);
expect(diff).toContain('+content');
}); });
}); });