gitnexus 1.6.12-rc.27 → 1.6.12-rc.29
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/dist/mcp/local/local-backend.js +13 -5
- package/dist/storage/git.d.ts +17 -0
- package/dist/storage/git.js +221 -4
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@ import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/l
|
|
|
25
25
|
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
|
26
26
|
// git utilities available if needed
|
|
27
27
|
// import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
|
|
28
|
-
import {
|
|
28
|
+
import { parseDiffHunksResult, coalesceHunksByPath, hunksOverlapRange, findGitRootByDotGit, getCanonicalRepoRoot, getGitRoot, } from '../../storage/git.js';
|
|
29
29
|
import { realpathSync } from 'fs';
|
|
30
30
|
import { listRegisteredRepos, cleanupOldKuzuFiles, canonicalizePath, getStoragePaths, loadMeta, RegistryAmbiguousTargetError, } from '../../storage/repo-manager.js';
|
|
31
31
|
import { GroupService, } from '../../core/group/service.js';
|
|
@@ -795,6 +795,12 @@ export function buildDetectChangesDiffArgs(scope, baseRef) {
|
|
|
795
795
|
// not `--default-prefix`, which needs git >= 2.42. `--no-ext-diff` stops a
|
|
796
796
|
// configured external diff driver from replacing the unified output we parse.
|
|
797
797
|
const args = [
|
|
798
|
+
// Before the subcommand: default core.quotePath C-quotes non-ASCII so
|
|
799
|
+
// `diff --git` / `+++` tokens no longer match the unquoted `a/` `b/` forms
|
|
800
|
+
// the parser also accepts. The parser still decodes quoted tokens; this
|
|
801
|
+
// pin keeps production git from emitting them.
|
|
802
|
+
'-c',
|
|
803
|
+
'core.quotePath=false',
|
|
798
804
|
'diff',
|
|
799
805
|
'--ignore-cr-at-eol',
|
|
800
806
|
'--no-ext-diff',
|
|
@@ -4464,10 +4470,10 @@ export class LocalBackend {
|
|
|
4464
4470
|
catch (err) {
|
|
4465
4471
|
return { error: `Git diff failed: ${err.message}` };
|
|
4466
4472
|
}
|
|
4467
|
-
const fileDiffs =
|
|
4473
|
+
const { files: fileDiffs, unparsedGitHeaders } = parseDiffHunksResult(diffOutput);
|
|
4468
4474
|
if (fileDiffs.length === 0) {
|
|
4469
|
-
// Git printed a diff but none of it parsed:
|
|
4470
|
-
//
|
|
4475
|
+
// Git printed a diff but none of it parsed: no `diff --git` / `+++`
|
|
4476
|
+
// file header was recognised. That is a PARSE failure, not a clean tree,
|
|
4471
4477
|
// and the clean branch below would report it to the pre-commit gate as
|
|
4472
4478
|
// `risk_level:'none'`, no `partial`, exit 0 — a false all-clear (#2915).
|
|
4473
4479
|
const parseFailed = diffOutput.trim().length > 0;
|
|
@@ -4499,7 +4505,9 @@ export class LocalBackend {
|
|
|
4499
4505
|
const changedSymbols = new Map();
|
|
4500
4506
|
// Set if a swallowed graph query fails below — surfaces `partial:true` so a
|
|
4501
4507
|
// degraded run cannot report a false-clean `risk_level:'low'` (#2283).
|
|
4502
|
-
|
|
4508
|
+
// An unparsed `diff --git` is the same class: later hunks must not make
|
|
4509
|
+
// the gate look complete.
|
|
4510
|
+
let queryDegraded = unparsedGitHeaders > 0;
|
|
4503
4511
|
// Hunks arrive grouped per path and already in the graph's 0-based line
|
|
4504
4512
|
// space, so every comparison below is base-neutral (#2377).
|
|
4505
4513
|
const hunksByPath = coalesceHunksByPath(fileDiffs);
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -268,10 +268,20 @@ export interface FileDiff {
|
|
|
268
268
|
filePath: string;
|
|
269
269
|
hunks: DiffHunk[];
|
|
270
270
|
}
|
|
271
|
+
/** `parseDiffHunks` plus how many `diff --git` headers could not be decoded. */
|
|
272
|
+
export interface DiffHunkParseResult {
|
|
273
|
+
files: FileDiff[];
|
|
274
|
+
unparsedGitHeaders: number;
|
|
275
|
+
}
|
|
271
276
|
/**
|
|
272
277
|
* Parse unified diff output (with -U0) into per-file hunk ranges.
|
|
273
278
|
* Extracts the new-file line ranges from @@ hunk headers.
|
|
274
279
|
*
|
|
280
|
+
* The `diff --git` header is also retained as a file entry. This matters for
|
|
281
|
+
* binary, rename-only, and mode-only changes, which have no `+++ b/` header.
|
|
282
|
+
* Such entries intentionally have no hunks: callers can count the changed
|
|
283
|
+
* path without pretending that a symbol line range was touched.
|
|
284
|
+
*
|
|
275
285
|
* A pure deletion adds no new lines, and unified diff spells that empty range
|
|
276
286
|
* as the line BEFORE it: `@@ -4,2 +3,0 @@` removed old lines 4–5 from between
|
|
277
287
|
* new lines 3 and 4 (git emits `+0,0` when the deletion is at the head of the
|
|
@@ -287,6 +297,13 @@ export interface FileDiff {
|
|
|
287
297
|
* gap — the widening {@link coalesceHunks} is careful never to do.
|
|
288
298
|
*/
|
|
289
299
|
export declare function parseDiffHunks(diffOutput: string): FileDiff[];
|
|
300
|
+
/**
|
|
301
|
+
* Same as {@link parseDiffHunks}, plus a count of `diff --git` lines that
|
|
302
|
+
* could not be decoded. `detect_changes` uses the count to fail closed
|
|
303
|
+
* (`partial` + `risk_level:'unknown'`) instead of attaching later hunks to a
|
|
304
|
+
* previous file.
|
|
305
|
+
*/
|
|
306
|
+
export declare function parseDiffHunksResult(diffOutput: string): DiffHunkParseResult;
|
|
290
307
|
/**
|
|
291
308
|
* Merge a file's hunks into sorted, non-touching ranges.
|
|
292
309
|
*
|
package/dist/storage/git.js
CHANGED
|
@@ -696,10 +696,183 @@ export const parseRepoNameFromUrl = (url) => {
|
|
|
696
696
|
export const getInferredRepoName = (repoPath) => {
|
|
697
697
|
return parseRepoNameFromUrl(getRemoteOriginUrl(repoPath));
|
|
698
698
|
};
|
|
699
|
+
const DIFF_GIT_PREFIX = 'diff --git ';
|
|
700
|
+
/**
|
|
701
|
+
* Decode one Git C-quoted token (`"a/\\344\\270\\255.png"`). Returns
|
|
702
|
+
* `undefined` when the quotes are unbalanced or a trailing escape is bare.
|
|
703
|
+
*/
|
|
704
|
+
function unquoteCStyleGitToken(quoted) {
|
|
705
|
+
if (quoted.length < 2 || quoted[0] !== '"' || quoted[quoted.length - 1] !== '"') {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
// Git C-quotes are byte-oriented: non-ASCII is `\nnn` octal of the UTF-8
|
|
709
|
+
// code units, not JS UTF-16 characters.
|
|
710
|
+
const bytes = [];
|
|
711
|
+
const pushChar = (ch) => {
|
|
712
|
+
const code = ch.charCodeAt(0);
|
|
713
|
+
if (code < 0x80)
|
|
714
|
+
bytes.push(code);
|
|
715
|
+
else
|
|
716
|
+
bytes.push(...new TextEncoder().encode(ch));
|
|
717
|
+
};
|
|
718
|
+
for (let i = 1; i < quoted.length - 1; i++) {
|
|
719
|
+
const ch = quoted[i];
|
|
720
|
+
if (ch !== '\\') {
|
|
721
|
+
pushChar(ch);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const next = quoted[++i];
|
|
725
|
+
if (next === undefined)
|
|
726
|
+
return undefined;
|
|
727
|
+
switch (next) {
|
|
728
|
+
case '\\':
|
|
729
|
+
case '"':
|
|
730
|
+
pushChar(next);
|
|
731
|
+
break;
|
|
732
|
+
case 'n':
|
|
733
|
+
bytes.push(0x0a);
|
|
734
|
+
break;
|
|
735
|
+
case 't':
|
|
736
|
+
bytes.push(0x09);
|
|
737
|
+
break;
|
|
738
|
+
case 'r':
|
|
739
|
+
bytes.push(0x0d);
|
|
740
|
+
break;
|
|
741
|
+
case 'a':
|
|
742
|
+
bytes.push(0x07);
|
|
743
|
+
break;
|
|
744
|
+
case 'b':
|
|
745
|
+
bytes.push(0x08);
|
|
746
|
+
break;
|
|
747
|
+
case 'v':
|
|
748
|
+
bytes.push(0x0b);
|
|
749
|
+
break;
|
|
750
|
+
case 'f':
|
|
751
|
+
bytes.push(0x0c);
|
|
752
|
+
break;
|
|
753
|
+
default: {
|
|
754
|
+
if (next < '0' || next > '7') {
|
|
755
|
+
pushChar(next);
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
let oct = next;
|
|
759
|
+
while (oct.length < 3 && i + 1 < quoted.length - 1) {
|
|
760
|
+
const digit = quoted[i + 1];
|
|
761
|
+
if (digit < '0' || digit > '7')
|
|
762
|
+
break;
|
|
763
|
+
oct += digit;
|
|
764
|
+
i++;
|
|
765
|
+
}
|
|
766
|
+
bytes.push(parseInt(oct, 8));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return new TextDecoder('utf-8').decode(Uint8Array.from(bytes));
|
|
771
|
+
}
|
|
772
|
+
function takeCQuotedToken(source, start) {
|
|
773
|
+
if (source[start] !== '"')
|
|
774
|
+
return undefined;
|
|
775
|
+
for (let i = start + 1; i < source.length; i++) {
|
|
776
|
+
if (source[i] === '\\') {
|
|
777
|
+
i++;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (source[i] === '"')
|
|
781
|
+
return { token: source.slice(start, i + 1), end: i + 1 };
|
|
782
|
+
}
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
function stripGitDstPrefix(raw) {
|
|
786
|
+
return raw.startsWith('b/') ? raw.slice(2) : undefined;
|
|
787
|
+
}
|
|
788
|
+
/** Unified-diff paths end at the first TAB (timestamp / empty terminator). */
|
|
789
|
+
function stripUnifiedDiffTab(pathWithOptionalTab) {
|
|
790
|
+
const tab = pathWithOptionalTab.indexOf('\t');
|
|
791
|
+
return tab === -1 ? pathWithOptionalTab : pathWithOptionalTab.slice(0, tab);
|
|
792
|
+
}
|
|
793
|
+
function decodeGitPathToken(raw) {
|
|
794
|
+
const trimmed = stripUnifiedDiffTab(raw);
|
|
795
|
+
if (trimmed.startsWith('"'))
|
|
796
|
+
return unquoteCStyleGitToken(trimmed);
|
|
797
|
+
return trimmed;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Destination path from `diff --git a/<src> b/<dst>`.
|
|
801
|
+
*
|
|
802
|
+
* Same-path headers recover `name` from `a/${name} b/${name}` so a dest that
|
|
803
|
+
* itself contains ` b/` is not split at the last occurrence. C-quoted tokens
|
|
804
|
+
* (default `core.quotePath`) are decoded. Renames that the greedy split would
|
|
805
|
+
* mis-parse stay a best-effort dest; `rename to` / `+++` correct them.
|
|
806
|
+
*/
|
|
807
|
+
function takeGitHeaderPathToken(source, start) {
|
|
808
|
+
if (start >= source.length)
|
|
809
|
+
return undefined;
|
|
810
|
+
if (source[start] === '"') {
|
|
811
|
+
const tok = takeCQuotedToken(source, start);
|
|
812
|
+
if (!tok)
|
|
813
|
+
return undefined;
|
|
814
|
+
const path = unquoteCStyleGitToken(tok.token);
|
|
815
|
+
if (path === undefined)
|
|
816
|
+
return undefined;
|
|
817
|
+
return { path, end: tok.end };
|
|
818
|
+
}
|
|
819
|
+
if (source.startsWith('b/', start)) {
|
|
820
|
+
return { path: stripUnifiedDiffTab(source.slice(start)), end: source.length };
|
|
821
|
+
}
|
|
822
|
+
if (source.startsWith('a/', start)) {
|
|
823
|
+
for (let i = start + 2; i < source.length; i++) {
|
|
824
|
+
if (source.startsWith(' b/', i) || source.startsWith(' "', i)) {
|
|
825
|
+
return { path: source.slice(start, i), end: i };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return undefined;
|
|
830
|
+
}
|
|
831
|
+
function filePathFromGitHeader(line) {
|
|
832
|
+
if (!line.startsWith(DIFF_GIT_PREFIX))
|
|
833
|
+
return undefined;
|
|
834
|
+
const rest = line.slice(DIFF_GIT_PREFIX.length);
|
|
835
|
+
if (rest.startsWith('a/')) {
|
|
836
|
+
for (let i = 2; i < rest.length; i++) {
|
|
837
|
+
if (!rest.startsWith(' b/', i))
|
|
838
|
+
continue;
|
|
839
|
+
const nameA = rest.slice(2, i);
|
|
840
|
+
const nameB = rest.slice(i + 3);
|
|
841
|
+
if (nameA.length > 0 && nameA === nameB)
|
|
842
|
+
return nameA;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const src = takeGitHeaderPathToken(rest, 0);
|
|
846
|
+
if (!src)
|
|
847
|
+
return undefined;
|
|
848
|
+
let i = src.end;
|
|
849
|
+
while (rest[i] === ' ')
|
|
850
|
+
i++;
|
|
851
|
+
const dest = takeGitHeaderPathToken(rest, i);
|
|
852
|
+
return dest ? stripGitDstPrefix(dest.path) : undefined;
|
|
853
|
+
}
|
|
854
|
+
function pathFromPlusPlusPlus(line) {
|
|
855
|
+
if (!line.startsWith('+++ '))
|
|
856
|
+
return undefined;
|
|
857
|
+
const raw = decodeGitPathToken(line.slice(4));
|
|
858
|
+
if (!raw || raw === '/dev/null')
|
|
859
|
+
return undefined;
|
|
860
|
+
return stripGitDstPrefix(raw);
|
|
861
|
+
}
|
|
862
|
+
function pathFromRenameTo(line) {
|
|
863
|
+
if (!line.startsWith('rename to '))
|
|
864
|
+
return undefined;
|
|
865
|
+
return decodeGitPathToken(line.slice('rename to '.length));
|
|
866
|
+
}
|
|
699
867
|
/**
|
|
700
868
|
* Parse unified diff output (with -U0) into per-file hunk ranges.
|
|
701
869
|
* Extracts the new-file line ranges from @@ hunk headers.
|
|
702
870
|
*
|
|
871
|
+
* The `diff --git` header is also retained as a file entry. This matters for
|
|
872
|
+
* binary, rename-only, and mode-only changes, which have no `+++ b/` header.
|
|
873
|
+
* Such entries intentionally have no hunks: callers can count the changed
|
|
874
|
+
* path without pretending that a symbol line range was touched.
|
|
875
|
+
*
|
|
703
876
|
* A pure deletion adds no new lines, and unified diff spells that empty range
|
|
704
877
|
* as the line BEFORE it: `@@ -4,2 +3,0 @@` removed old lines 4–5 from between
|
|
705
878
|
* new lines 3 and 4 (git emits `+0,0` when the deletion is at the head of the
|
|
@@ -715,14 +888,58 @@ export const getInferredRepoName = (repoPath) => {
|
|
|
715
888
|
* gap — the widening {@link coalesceHunks} is careful never to do.
|
|
716
889
|
*/
|
|
717
890
|
export function parseDiffHunks(diffOutput) {
|
|
891
|
+
return parseDiffHunksResult(diffOutput).files;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Same as {@link parseDiffHunks}, plus a count of `diff --git` lines that
|
|
895
|
+
* could not be decoded. `detect_changes` uses the count to fail closed
|
|
896
|
+
* (`partial` + `risk_level:'unknown'`) instead of attaching later hunks to a
|
|
897
|
+
* previous file.
|
|
898
|
+
*/
|
|
899
|
+
export function parseDiffHunksResult(diffOutput) {
|
|
718
900
|
const files = [];
|
|
719
901
|
let current = null;
|
|
902
|
+
let unparsedGitHeaders = 0;
|
|
903
|
+
// `+++` after the first `@@` of a file is hunk body (`+` plus source text
|
|
904
|
+
// that itself starts `++ …`), not another file header.
|
|
905
|
+
let inHunk = false;
|
|
720
906
|
for (const line of diffOutput.split('\n')) {
|
|
721
|
-
if (line.startsWith(
|
|
722
|
-
|
|
723
|
-
|
|
907
|
+
if (line.startsWith(DIFF_GIT_PREFIX)) {
|
|
908
|
+
// Drop the previous file first: an unparsed header must not leave
|
|
909
|
+
// `current` live for a later `@@` / quoted `+++` to steal.
|
|
910
|
+
current = null;
|
|
911
|
+
inHunk = false;
|
|
912
|
+
const filePath = filePathFromGitHeader(line);
|
|
913
|
+
if (filePath) {
|
|
914
|
+
current = { filePath, hunks: [] };
|
|
915
|
+
files.push(current);
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
unparsedGitHeaders++;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
else if (line.startsWith('rename to ')) {
|
|
922
|
+
const filePath = pathFromRenameTo(line);
|
|
923
|
+
if (!filePath)
|
|
924
|
+
continue;
|
|
925
|
+
if (current)
|
|
926
|
+
current.filePath = filePath;
|
|
927
|
+
else {
|
|
928
|
+
current = { filePath, hunks: [] };
|
|
929
|
+
files.push(current);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
else if (!inHunk && line.startsWith('+++ ')) {
|
|
933
|
+
const filePath = pathFromPlusPlusPlus(line);
|
|
934
|
+
if (!filePath)
|
|
935
|
+
continue;
|
|
936
|
+
if (!current || current.filePath !== filePath) {
|
|
937
|
+
current = { filePath, hunks: [] };
|
|
938
|
+
files.push(current);
|
|
939
|
+
}
|
|
724
940
|
}
|
|
725
941
|
else if (line.startsWith('@@') && current) {
|
|
942
|
+
inHunk = true;
|
|
726
943
|
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
727
944
|
if (match) {
|
|
728
945
|
const start = parseInt(match[1], 10);
|
|
@@ -739,7 +956,7 @@ export function parseDiffHunks(diffOutput) {
|
|
|
739
956
|
}
|
|
740
957
|
}
|
|
741
958
|
}
|
|
742
|
-
return files;
|
|
959
|
+
return { files, unparsedGitHeaders };
|
|
743
960
|
}
|
|
744
961
|
/**
|
|
745
962
|
* Merge a file's hunks into sorted, non-touching ranges.
|
package/package.json
CHANGED