codeant-cli 0.5.12 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/utils/gitDiffHelper.js +68 -13
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
+
import { constants as fsConstants } from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { promisify } from 'util';
|
|
4
|
-
import { exec } from 'child_process';
|
|
5
|
+
import { exec, execFile } from 'child_process';
|
|
5
6
|
|
|
6
7
|
const execAsync = promisify(exec);
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Expand untracked paths from `git status --porcelain`.
|
|
@@ -468,18 +470,58 @@ class GitDiffHelper {
|
|
|
468
470
|
return diffs;
|
|
469
471
|
}
|
|
470
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Read a working-tree file. Returns null if it resolves outside the repository (a symlink
|
|
475
|
+
* pointing elsewhere) or isn't a regular file, so those contents are never uploaded.
|
|
476
|
+
*/
|
|
477
|
+
async _readRepoFile(relPath) {
|
|
478
|
+
const realRoot = await fs.realpath(this.gitRoot);
|
|
479
|
+
const isInside = p => {
|
|
480
|
+
const rel = path.relative(realRoot, p);
|
|
481
|
+
return !!rel && rel !== '..' && !rel.startsWith('..' + path.sep) && !path.isAbsolute(rel);
|
|
482
|
+
};
|
|
483
|
+
const realPath = await fs.realpath(path.resolve(this.gitRoot, relPath));
|
|
484
|
+
if (!isInside(realPath)) return null;
|
|
485
|
+
|
|
486
|
+
// Open first and then check what was actually opened, so a symlink swapped in after the
|
|
487
|
+
// check above can't redirect the read. O_NONBLOCK keeps a FIFO from blocking the open.
|
|
488
|
+
const { O_RDONLY, O_NOFOLLOW = 0, O_NONBLOCK = 0 } = fsConstants;
|
|
489
|
+
const handle = await fs.open(realPath, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
490
|
+
try {
|
|
491
|
+
const opened = await handle.stat();
|
|
492
|
+
if (!opened.isFile()) return null;
|
|
493
|
+
// The opened file must still be the file at this path, and the path must still resolve
|
|
494
|
+
// inside the repo (a parent directory may have been swapped for a symlink)
|
|
495
|
+
const nowPath = await fs.realpath(realPath);
|
|
496
|
+
const now = await fs.stat(nowPath);
|
|
497
|
+
if (!isInside(nowPath) || now.dev !== opened.dev || now.ino !== opened.ino) return null;
|
|
498
|
+
return await handle.readFile('utf8');
|
|
499
|
+
} finally {
|
|
500
|
+
await handle.close();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
471
504
|
/**
|
|
472
505
|
* Build a synthetic "new file" diff for an untracked file, which git diff can't see.
|
|
473
506
|
* Returns null if the file can't be read.
|
|
474
507
|
*/
|
|
475
508
|
async _buildUntrackedFileDiff(file) {
|
|
476
509
|
try {
|
|
477
|
-
|
|
478
|
-
|
|
510
|
+
let content = await this._readRepoFile(file).catch(() => null);
|
|
511
|
+
let mode = '100644';
|
|
512
|
+
if (content === null) {
|
|
513
|
+
// A symlink we won't follow (it points outside the repo or nowhere): show the link
|
|
514
|
+
// itself, as git would once it's committed, using its target path and never the
|
|
515
|
+
// target's contents
|
|
516
|
+
const absolutePath = path.resolve(this.gitRoot, file);
|
|
517
|
+
if (!(await fs.lstat(absolutePath)).isSymbolicLink()) return null;
|
|
518
|
+
content = await fs.readlink(absolutePath);
|
|
519
|
+
mode = '120000';
|
|
520
|
+
}
|
|
479
521
|
const lines = content.split('\n');
|
|
480
522
|
|
|
481
523
|
let patchStr = `diff --git a/${file} b/${file}\n`;
|
|
482
|
-
patchStr += `new file mode
|
|
524
|
+
patchStr += `new file mode ${mode}\n`;
|
|
483
525
|
patchStr += `index 0000000..${this._generateRandomHash()}\n`;
|
|
484
526
|
patchStr += `--- /dev/null\n`;
|
|
485
527
|
patchStr += `+++ b/${file}\n`;
|
|
@@ -549,8 +591,7 @@ class GitDiffHelper {
|
|
|
549
591
|
let resolvedRef;
|
|
550
592
|
for (const candidate of [branch, `origin/${branch}`]) {
|
|
551
593
|
try {
|
|
552
|
-
|
|
553
|
-
resolvedRef = stdout.trim();
|
|
594
|
+
resolvedRef = await this._resolveCommitSha(candidate);
|
|
554
595
|
break;
|
|
555
596
|
} catch (err) {
|
|
556
597
|
// try next
|
|
@@ -589,8 +630,7 @@ class GitDiffHelper {
|
|
|
589
630
|
// Resolve the commit ref (supports HEAD~3, sha, tag, etc.)
|
|
590
631
|
let resolvedCommit;
|
|
591
632
|
try {
|
|
592
|
-
|
|
593
|
-
resolvedCommit = stdout.trim();
|
|
633
|
+
resolvedCommit = await this._resolveCommitSha(commit);
|
|
594
634
|
} catch (err) {
|
|
595
635
|
throw new Error(`Could not resolve commit: ${commit}`);
|
|
596
636
|
}
|
|
@@ -598,6 +638,21 @@ class GitDiffHelper {
|
|
|
598
638
|
return this._getWorkingTreeDiffAgainst(resolvedCommit);
|
|
599
639
|
}
|
|
600
640
|
|
|
641
|
+
/**
|
|
642
|
+
* Resolve a user-supplied ref (from the CLI or MCP) to a commit SHA without going through a
|
|
643
|
+
* shell, so later git commands only ever see the SHA. Throws if it doesn't name a commit.
|
|
644
|
+
*/
|
|
645
|
+
async _resolveCommitSha(ref) {
|
|
646
|
+
// A leading "-" would be parsed as a git option (e.g. --output=<file>)
|
|
647
|
+
if (ref.startsWith('-')) throw new Error(`Invalid ref: ${ref}`);
|
|
648
|
+
const { stdout } = await execFileAsync(
|
|
649
|
+
'git',
|
|
650
|
+
['rev-parse', '--verify', '--quiet', `${ref}^{commit}`],
|
|
651
|
+
{ cwd: this.gitRoot }
|
|
652
|
+
);
|
|
653
|
+
return stdout.trim();
|
|
654
|
+
}
|
|
655
|
+
|
|
601
656
|
/**
|
|
602
657
|
* Diff a base commit against the working tree: commits since base, staged and unstaged
|
|
603
658
|
* changes (including staged new files), plus untracked files not ignored by .gitignore.
|
|
@@ -1011,8 +1066,8 @@ class GitDiffHelper {
|
|
|
1011
1066
|
} catch (err) {
|
|
1012
1067
|
// Handle untracked files
|
|
1013
1068
|
try {
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1069
|
+
const headFileStr = await this._readRepoFile(filePath);
|
|
1070
|
+
if (headFileStr === null) throw new Error('Not a regular file inside the repository');
|
|
1016
1071
|
const lines = headFileStr.split('\n');
|
|
1017
1072
|
|
|
1018
1073
|
let patchStr = `diff --git a/${filePath} b/${filePath}\n`;
|
|
@@ -1069,9 +1124,9 @@ class GitDiffHelper {
|
|
|
1069
1124
|
{ cwd: this.gitRoot }
|
|
1070
1125
|
).then(r => r.stdout).catch(() => '');
|
|
1071
1126
|
} else {
|
|
1072
|
-
headFileStr = await
|
|
1073
|
-
|
|
1074
|
-
|
|
1127
|
+
headFileStr = await this._readRepoFile(filePath)
|
|
1128
|
+
.then(content => content ?? '')
|
|
1129
|
+
.catch(() => '');
|
|
1075
1130
|
}
|
|
1076
1131
|
|
|
1077
1132
|
// Get patch
|