Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/git/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,43 @@
};
}

function getUntrackedDiff(): string {
const files = spawnSync('git', ['ls-files', '--others', '--exclude-standard', '-z'], {
encoding: 'utf-8',
maxBuffer: GIT_DIFF_MAX_BUFFER,
});
if (files.error) throw files.error;
if (files.status !== 0) {
throw new Error(files.stderr.trim() || `git ls-files exited with code ${files.status}`);
}

return files.stdout
.split('\0')
.filter(Boolean)
.map((file) => {
const result = spawnSync('git', ['diff', '--no-index', '--', '/dev/null', file], {

Check warning on line 72 in src/git/diff.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure the "PATH" variable only contains fixed, unwriteable directories.

See more on https://sonarcloud.io/project/issues?id=404-PF_commit-echo&issues=AZ-wccNE4CZyFP_GwVjV&open=AZ-wccNE4CZyFP_GwVjV&pullRequest=250

Check notice

Code scanning / SonarCloud

OS commands should not rely on PATH resolution Low

Make sure the "PATH" variable only contains fixed, unwriteable directories. See more on SonarQube Cloud
Comment on lines +71 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid spawning one Git process per untracked file

When a working tree contains hundreds or thousands of non-ignored untracked files, this synchronous map launches a separate git diff process for every file before prompting or applying the configured diff truncation. In the inspected Linux environment, 1,000 tiny files took about 14 seconds, so an untracked generated or dependency tree can make suggest appear hung for minutes; generate the untracked patch in a bounded/batched operation or stop once the usable diff limit is reached.

Useful? React with 👍 / 👎.

encoding: 'utf-8',
maxBuffer: GIT_DIFF_MAX_BUFFER,
});
if (result.error) throw result.error;
if (result.status !== 0 && result.status !== 1) {
throw new Error(result.stderr.trim() || `git diff --no-index exited with code ${result.status}`);
}
return result.stdout;
Comment on lines +76 to +80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not treat every exit status 1 as a valid diff

When the untracked entry is an embedded Git repository, git ls-files --others returns a directory such as nested/, and git diff --no-index /dev/null nested/ emits Could not access 'nested/null' while also exiting with status 1. This condition accepts that access error as an ordinary difference, discards the empty stdout, and ultimately reports no changes, so the original untracked-only failure remains for repositories adding a submodule or embedded repository; distinguish an actual patch from status-1 errors or handle directory entries explicitly.

Useful? React with 👍 / 👎.

})
.filter(Boolean)
.join('\n');
}

export function getUnstagedDiff(): DiffResult {
const diff = execSync('git diff', {
const trackedDiff = execSync('git diff', {

Check warning on line 87 in src/git/diff.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure the "PATH" variable only contains fixed, unwriteable directories.

See more on https://sonarcloud.io/project/issues?id=404-PF_commit-echo&issues=AZ-wccNE4CZyFP_GwVjW&open=AZ-wccNE4CZyFP_GwVjW&pullRequest=250

Check notice

Code scanning / SonarCloud

OS commands should not rely on PATH resolution Low

Make sure the "PATH" variable only contains fixed, unwriteable directories. See more on SonarQube Cloud
encoding: 'utf-8',
maxBuffer: GIT_DIFF_MAX_BUFFER,
});
const diff = [trackedDiff, getUntrackedDiff()].filter(Boolean).join('\n').trim();
return {
diff: diff.trim(),
hasChanges: diff.trim().length > 0,
diff,
hasChanges: diff.length > 0,
staged: false,
};
}
Expand Down
20 changes: 20 additions & 0 deletions tests/git-diff.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,26 @@ test("getUnstagedDiff returns diff for unstaged changes", () => {
}
});

test("getUnstagedDiff includes untracked files", () => {
const repoDir = initRepo();

try {
git(["commit", "--allow-empty", "-m", "initial commit"], repoDir);
writeFileSync(join(repoDir, "new-file.txt"), "untracked content\n", "utf-8");

withCwd(repoDir, () => {
const result = getUnstagedDiff();

assert.equal(result.hasChanges, true);
assert.equal(result.staged, false);
assert.ok(result.diff.includes("new-file.txt"));
assert.ok(result.diff.includes("+untracked content"));
});
} finally {
rmSync(repoDir, { recursive: true, force: true });
}
});

test("getUnstagedDiff handles diffs larger than the default execSync buffer", () => {
const repoDir = initRepo();

Expand Down