difit 3.1.18 → 4.0.1
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/README.ja.md +44 -16
- package/README.ko.md +44 -16
- package/README.md +44 -16
- package/README.zh.md +44 -16
- package/dist/cli/github.d.ts +65 -0
- package/dist/cli/github.js +296 -0
- package/dist/cli/github.test.d.ts +1 -0
- package/dist/cli/github.test.js +341 -0
- package/dist/cli/index.js +42 -1
- package/dist/cli/index.test.js +330 -4
- package/dist/cli/utils.d.ts +2 -8
- package/dist/cli/utils.js +4 -43
- package/dist/cli/utils.test.js +50 -67
- package/dist/client/assets/{_basePickBy-DyiQWUmK.js → _basePickBy-ChXFkTMC.js} +1 -1
- package/dist/client/assets/{_baseUniq-DivSZEOF.js → _baseUniq-Mj_sFFQW.js} +1 -1
- package/dist/client/assets/{arc-c0kacVOL.js → arc-BMA6S9F1.js} +1 -1
- package/dist/client/assets/{architectureDiagram-2XIMDMQ5-ubymLNEe.js → architectureDiagram-2XIMDMQ5-0uiM_v5K.js} +1 -1
- package/dist/client/assets/{blockDiagram-WCTKOSBZ-F9D8w4_S.js → blockDiagram-WCTKOSBZ-CM7ZLL6F.js} +1 -1
- package/dist/client/assets/{c4Diagram-IC4MRINW-JE9Kx4yQ.js → c4Diagram-IC4MRINW-DKtCnVwn.js} +1 -1
- package/dist/client/assets/channel-D057yzDp.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-CYOCoDMc.js → chunk-4BX2VUAB-Wsl8DxEB.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-PRBuiJg9.js → chunk-55IACEB6-CHm9X5i7.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-C0eJ7JsI.js → chunk-FMBD7UC4-BSa8SHgd.js} +1 -1
- package/dist/client/assets/{chunk-JSJVCQXG-QZotPSqo.js → chunk-JSJVCQXG-Cpk76oJ3.js} +1 -1
- package/dist/client/assets/{chunk-KX2RTZJC-B8du3tt8.js → chunk-KX2RTZJC-D8YvfZVu.js} +1 -1
- package/dist/client/assets/{chunk-NQ4KR5QH-B10ldi5m.js → chunk-NQ4KR5QH-BogviJOv.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-CpwW9rUQ.js → chunk-QZHKN3VN-DwLJYu26.js} +1 -1
- package/dist/client/assets/{chunk-WL4C6EOR-DwKPHpbL.js → chunk-WL4C6EOR-BFDpGxW2.js} +1 -1
- package/dist/client/assets/classDiagram-VBA2DB6C---D4iOts.js +1 -0
- package/dist/client/assets/classDiagram-v2-RAHNMMFH---D4iOts.js +1 -0
- package/dist/client/assets/clone-xSR3otEf.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-p76yal75.js → cose-bilkent-S5V4N54A-oEosZ_5y.js} +1 -1
- package/dist/client/assets/{dagre-KLK3FWXG-CdDyed3V.js → dagre-KLK3FWXG-gFld4u1H.js} +1 -1
- package/dist/client/assets/{diagram-E7M64L7V-BaC8dXuW.js → diagram-E7M64L7V-gJq3kSrf.js} +1 -1
- package/dist/client/assets/{diagram-IFDJBPK2-BGf8xwJI.js → diagram-IFDJBPK2-BsUm_q22.js} +1 -1
- package/dist/client/assets/{diagram-P4PSJMXO-D3j16gBZ.js → diagram-P4PSJMXO-juB-sfcR.js} +1 -1
- package/dist/client/assets/{erDiagram-INFDFZHY-DFpDdocf.js → erDiagram-INFDFZHY-Dn77qXAt.js} +1 -1
- package/dist/client/assets/{flowDiagram-PKNHOUZH-Cz4mb4IF.js → flowDiagram-PKNHOUZH-DtmvDYdN.js} +1 -1
- package/dist/client/assets/{ganttDiagram-A5KZAMGK-CNzY9ua5.js → ganttDiagram-A5KZAMGK-BlDaKLbQ.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-K3NZZRJ6-DCSxL8EQ.js → gitGraphDiagram-K3NZZRJ6-DeAAeuMS.js} +1 -1
- package/dist/client/assets/{graph-BC2BV1-T.js → graph-NX9gBP47.js} +1 -1
- package/dist/client/assets/index-VxkpzDXr.css +1 -0
- package/dist/client/assets/index-kJdw4DY-.js +98 -0
- package/dist/client/assets/{infoDiagram-LFFYTUFH-BKSspZbH.js → infoDiagram-LFFYTUFH-CAaX023c.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-PHBUUO56-DZ2IRYwc.js → ishikawaDiagram-PHBUUO56-CmiTQStv.js} +1 -1
- package/dist/client/assets/{journeyDiagram-4ABVD52K-BrjXAkii.js → journeyDiagram-4ABVD52K-B0SHC7mz.js} +1 -1
- package/dist/client/assets/{kanban-definition-K7BYSVSG-B1mfOekw.js → kanban-definition-K7BYSVSG-IfRdhzz7.js} +1 -1
- package/dist/client/assets/{layout-CWTG02uT.js → layout-l3OdNQhJ.js} +1 -1
- package/dist/client/assets/{linear-CGgOKp1d.js → linear-CQ0hx5Qs.js} +1 -1
- package/dist/client/assets/{mermaid.core-DTPtVBG7.js → mermaid.core-DqlPTabt.js} +4 -4
- package/dist/client/assets/{mindmap-definition-YRQLILUH-DByVRPFT.js → mindmap-definition-YRQLILUH-DIgSmG_f.js} +1 -1
- package/dist/client/assets/{pieDiagram-SKSYHLDU-DEgvAxAy.js → pieDiagram-SKSYHLDU-FzM5qoIB.js} +1 -1
- package/dist/client/assets/{prism-csharp-DqTrHqwJ.js → prism-csharp-DCfUUOUs.js} +1 -1
- package/dist/client/assets/{prism-elixir-DEJaM00V.js → prism-elixir-riuOL1mm.js} +1 -1
- package/dist/client/assets/{prism-hcl-HvJ0aPiH.js → prism-hcl-CizuX1s4.js} +1 -1
- package/dist/client/assets/{prism-java-DDUFERTh.js → prism-java-DYCKrDUh.js} +1 -1
- package/dist/client/assets/{prism-perl-CNA3SNC9.js → prism-perl-BJwBYR3Y.js} +1 -1
- package/dist/client/assets/{prism-php-hBQuhE2A.js → prism-php-BMhFuA7y.js} +1 -1
- package/dist/client/assets/{prism-ruby-BKap8imy.js → prism-ruby-Bcu0cDEh.js} +1 -1
- package/dist/client/assets/{prism-solidity-DHc7LZHq.js → prism-solidity-DDDs3w-w.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-337W2JSQ-DTtikTvc.js → quadrantDiagram-337W2JSQ-BBrApyD7.js} +1 -1
- package/dist/client/assets/{requirementDiagram-Z7DCOOCP-B34R-xD0.js → requirementDiagram-Z7DCOOCP-CLXiwUaA.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-WA2Y5GQK-Dts1ZXRC.js → sankeyDiagram-WA2Y5GQK-9Y3Ly5qe.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-2WXFIKYE-DzM3WhEY.js → sequenceDiagram-2WXFIKYE-DEpX1BA5.js} +1 -1
- package/dist/client/assets/{stateDiagram-RAJIS63D-B2dF8YnK.js → stateDiagram-RAJIS63D-Ck3ullwA.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-X6UiDsar.js +1 -0
- package/dist/client/assets/{timeline-definition-YZTLITO2-BO4OtcEm.js → timeline-definition-YZTLITO2-CMezf3XV.js} +1 -1
- package/dist/client/assets/{treemap-KZPCXAKY-DaXnvVRH.js → treemap-KZPCXAKY-DqrcV0gQ.js} +1 -1
- package/dist/client/assets/{vennDiagram-LZ73GAT5-AIMhd8Js.js → vennDiagram-LZ73GAT5-eQg945Fz.js} +1 -1
- package/dist/client/assets/{xychartDiagram-JWTSCODW-Ch6W1f7P.js → xychartDiagram-JWTSCODW-_hqdXeX1.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/generated-file-check.js +113 -58
- package/dist/server/generated-file-check.test.js +2 -0
- package/dist/server/git-diff-tui.d.ts +1 -1
- package/dist/server/git-diff-tui.js +7 -5
- package/dist/server/git-diff-tui.test.d.ts +1 -0
- package/dist/server/git-diff-tui.test.js +60 -0
- package/dist/server/git-diff.d.ts +4 -1
- package/dist/server/git-diff.js +73 -9
- package/dist/server/git-diff.test.js +46 -0
- package/dist/server/server.d.ts +3 -0
- package/dist/server/server.js +111 -37
- package/dist/server/server.test.js +152 -0
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +2 -2
- package/dist/types/diff.d.ts +74 -14
- package/dist/utils/commentFormatting.d.ts +4 -2
- package/dist/utils/commentFormatting.js +57 -19
- package/dist/utils/commentImports.d.ts +9 -0
- package/dist/utils/commentImports.js +264 -0
- package/dist/utils/commentImports.test.d.ts +1 -0
- package/dist/utils/commentImports.test.js +197 -0
- package/package.json +1 -1
- package/dist/client/assets/channel-Ca4c0q8d.js +0 -1
- package/dist/client/assets/classDiagram-VBA2DB6C-CJLw9sK7.js +0 -1
- package/dist/client/assets/classDiagram-v2-RAHNMMFH-CJLw9sK7.js +0 -1
- package/dist/client/assets/clone-D0mDLEir.js +0 -1
- package/dist/client/assets/index-DHt9OwVU.css +0 -1
- package/dist/client/assets/index-mE8CA51x.js +0 -95
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-ReD0hBzH.js +0 -1
|
@@ -1,57 +1,114 @@
|
|
|
1
|
+
const EXACT_FILENAMES = new Set([
|
|
2
|
+
'package-lock.json',
|
|
3
|
+
'pnpm-lock.yaml',
|
|
4
|
+
'yarn.lock',
|
|
5
|
+
'Gemfile.lock',
|
|
6
|
+
'Pipfile.lock',
|
|
7
|
+
'composer.lock',
|
|
8
|
+
'Cargo.lock',
|
|
9
|
+
'poetry.lock',
|
|
10
|
+
'go.sum',
|
|
11
|
+
'go.mod',
|
|
12
|
+
'pubspec.lock',
|
|
13
|
+
'flake.lock',
|
|
14
|
+
'Package.resolved',
|
|
15
|
+
'packages.lock.json',
|
|
16
|
+
'.terraform.lock.hcl',
|
|
17
|
+
'bun.lockb',
|
|
18
|
+
'gradle.lockfile',
|
|
19
|
+
'uv.lock',
|
|
20
|
+
'pdm.lock',
|
|
21
|
+
]);
|
|
22
|
+
const FILENAME_SUFFIXES = [
|
|
23
|
+
'.min.js',
|
|
24
|
+
'.min.css',
|
|
25
|
+
'.bundle.js',
|
|
26
|
+
'.bundle.css',
|
|
27
|
+
'-min.js',
|
|
28
|
+
'.map',
|
|
29
|
+
'.pb.go',
|
|
30
|
+
'.pb.rb',
|
|
31
|
+
'.pb.py',
|
|
32
|
+
'.pb.js',
|
|
33
|
+
'.pb.cc',
|
|
34
|
+
'.pb.h',
|
|
35
|
+
'.pb2.py',
|
|
36
|
+
'_pb2.py',
|
|
37
|
+
'.grpc.pb.go',
|
|
38
|
+
'_string.go',
|
|
39
|
+
'.graphql.ts',
|
|
40
|
+
'.graphql.js',
|
|
41
|
+
'.openapi.ts',
|
|
42
|
+
'.openapi.js',
|
|
43
|
+
'.msw.ts',
|
|
44
|
+
'.zod.ts',
|
|
45
|
+
'.api.ts',
|
|
46
|
+
'.g.dart',
|
|
47
|
+
'.freezed.dart',
|
|
48
|
+
'.g.cs',
|
|
49
|
+
'.designer.cs',
|
|
50
|
+
'_ide_helper.php',
|
|
51
|
+
];
|
|
52
|
+
const ROOT_PATH_PREFIXES = ['vendor/', 'node_modules/', 'dist/', 'build/', 'out/'];
|
|
53
|
+
const PATH_SEGMENTS = ['/generated/', '/gen/', '/__generated__/'];
|
|
54
|
+
function matchesAnySuffix(fileName, suffixes) {
|
|
55
|
+
return suffixes.some((suffix) => fileName.endsWith(suffix));
|
|
56
|
+
}
|
|
57
|
+
function isWordCharacter(char) {
|
|
58
|
+
const code = char.charCodeAt(0);
|
|
59
|
+
return ((code >= 48 && code <= 57) ||
|
|
60
|
+
(code >= 65 && code <= 90) ||
|
|
61
|
+
(code >= 97 && code <= 122) ||
|
|
62
|
+
code === 95);
|
|
63
|
+
}
|
|
64
|
+
function isWordString(text) {
|
|
65
|
+
if (text.length === 0) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
for (const char of text) {
|
|
69
|
+
if (!isWordCharacter(char)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
function hasGeneratedSuffix(fileName, marker) {
|
|
76
|
+
const markerIndex = fileName.lastIndexOf(marker);
|
|
77
|
+
if (markerIndex === -1) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return isWordString(fileName.slice(markerIndex + marker.length));
|
|
81
|
+
}
|
|
1
82
|
// Layer 1: Path/Filename Patterns
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/\.pb\.(go|rb|py|js|cc|h)$/, // protobuf
|
|
32
|
-
/\.pb2\.py$/, // protobuf python
|
|
33
|
-
/_pb2\.py$/,
|
|
34
|
-
/\.grpc\.pb\.go$/,
|
|
35
|
-
/_string\.go$/, // go generate stringer
|
|
36
|
-
/\.graphql\.(ts|js)$/, // GraphQL codegen
|
|
37
|
-
/\.openapi\.(ts|js)$/,
|
|
38
|
-
/mock_.*\.go$/, // mockgen
|
|
39
|
-
/mocks\/.*\.go$/,
|
|
40
|
-
/\.msw\.ts$/,
|
|
41
|
-
/\.zod\.ts$/,
|
|
42
|
-
/\.api\.ts$/,
|
|
43
|
-
/\.g\.dart$/,
|
|
44
|
-
/\.freezed\.dart$/,
|
|
45
|
-
/\.g\.cs$/,
|
|
46
|
-
/\.designer\.cs$/,
|
|
47
|
-
/_ide_helper\.php$/,
|
|
48
|
-
// Directories
|
|
49
|
-
/^vendor\//,
|
|
50
|
-
/^node_modules\//,
|
|
51
|
-
/^(dist|build|out)\//,
|
|
52
|
-
/\/generated\//,
|
|
53
|
-
/\/gen\//,
|
|
54
|
-
/\/__generated__\//, // Relay
|
|
83
|
+
const PATH_MATCHERS = [
|
|
84
|
+
{
|
|
85
|
+
description: 'exact-filename',
|
|
86
|
+
matches: (_parsedPath, fileName) => EXACT_FILENAMES.has(fileName),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
description: 'minified-or-generated-suffix',
|
|
90
|
+
matches: (_parsedPath, fileName) => matchesAnySuffix(fileName, FILENAME_SUFFIXES),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
description: 'generated-extension',
|
|
94
|
+
matches: (_parsedPath, fileName) => hasGeneratedSuffix(fileName, '.generated.') || hasGeneratedSuffix(fileName, '.gen.'),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
description: 'mockgen-go',
|
|
98
|
+
matches: (_parsedPath, fileName) => fileName.startsWith('mock_') && fileName.endsWith('.go'),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
description: 'mocks-go-directory',
|
|
102
|
+
matches: (parsedPath, fileName) => parsedPath.includes('mocks/') && fileName.endsWith('.go'),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
description: 'root-generated-directory',
|
|
106
|
+
matches: (parsedPath) => ROOT_PATH_PREFIXES.some((prefix) => parsedPath.startsWith(prefix)),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
description: 'generated-path-segment',
|
|
110
|
+
matches: (parsedPath) => PATH_SEGMENTS.some((segment) => parsedPath.includes(segment)),
|
|
111
|
+
},
|
|
55
112
|
];
|
|
56
113
|
// Layer 2: Universal Header Patterns
|
|
57
114
|
const UNIVERSAL_PATTERNS = [
|
|
@@ -172,14 +229,12 @@ export function isGeneratedFile(parsedPath, getHeaderLines) {
|
|
|
172
229
|
// We check if the filename matches any known generated patterns
|
|
173
230
|
// or if the path is in a known generated directory
|
|
174
231
|
const fileName = parsedPath.split('/').pop() || '';
|
|
175
|
-
for (const
|
|
176
|
-
|
|
177
|
-
const textToCheck = pattern.source.includes('/') ? parsedPath : fileName;
|
|
178
|
-
if (pattern.test(textToCheck)) {
|
|
232
|
+
for (const matcher of PATH_MATCHERS) {
|
|
233
|
+
if (matcher.matches(parsedPath, fileName)) {
|
|
179
234
|
return {
|
|
180
235
|
isGenerated: true,
|
|
181
236
|
reason: 'path',
|
|
182
|
-
matchedPattern:
|
|
237
|
+
matchedPattern: matcher.description,
|
|
183
238
|
};
|
|
184
239
|
}
|
|
185
240
|
}
|
|
@@ -20,6 +20,8 @@ describe('isGeneratedFile', () => {
|
|
|
20
20
|
expect(isGeneratedFile('api.gen.go').isGenerated).toBe(true);
|
|
21
21
|
expect(isGeneratedFile('service.pb.go').isGenerated).toBe(true);
|
|
22
22
|
expect(isGeneratedFile('schema.graphql.ts').isGenerated).toBe(true);
|
|
23
|
+
expect(isGeneratedFile('mock_service.go').isGenerated).toBe(true);
|
|
24
|
+
expect(isGeneratedFile('src/mocks/service.go').isGenerated).toBe(true);
|
|
23
25
|
});
|
|
24
26
|
it('detects generated directories', () => {
|
|
25
27
|
expect(isGeneratedFile('node_modules/package/index.js').isGenerated).toBe(true);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { FileDiff } from '../types/diff.js';
|
|
2
|
-
export declare function loadGitDiff(targetCommitish: string, baseCommitish: string, repoPath?: string): Promise<FileDiff[]>;
|
|
2
|
+
export declare function loadGitDiff(targetCommitish: string, baseCommitish: string, repoPath?: string, contextLines?: number): Promise<FileDiff[]>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import simpleGit from 'simple-git';
|
|
2
2
|
import { validateDiffArguments, createCommitRangeString } from '../cli/utils.js';
|
|
3
|
-
export async function loadGitDiff(targetCommitish, baseCommitish, repoPath) {
|
|
3
|
+
export async function loadGitDiff(targetCommitish, baseCommitish, repoPath, contextLines) {
|
|
4
4
|
// Validate arguments
|
|
5
5
|
const validation = validateDiffArguments(targetCommitish, baseCommitish);
|
|
6
6
|
if (!validation.valid) {
|
|
@@ -44,34 +44,36 @@ export async function loadGitDiff(targetCommitish, baseCommitish, repoPath) {
|
|
|
44
44
|
const path = pathParts.join('\t');
|
|
45
45
|
return { status, path };
|
|
46
46
|
});
|
|
47
|
+
const contextArgs = contextLines !== undefined ? [`-U${contextLines}`] : [];
|
|
47
48
|
// Get diff for each file individually
|
|
48
49
|
const fileDiffs = await Promise.all(fileChanges.map(async ({ status, path }) => {
|
|
49
50
|
let fileDiff = '';
|
|
50
51
|
// Handle individual file diffs (base is always a regular commit)
|
|
51
52
|
if (targetCommitish === 'working') {
|
|
52
53
|
// Show unstaged changes (working vs staged)
|
|
53
|
-
fileDiff = await git.diff(['--', path]);
|
|
54
|
+
fileDiff = await git.diff([...contextArgs, '--', path]);
|
|
54
55
|
}
|
|
55
56
|
else if (targetCommitish === 'staged') {
|
|
56
57
|
// Show staged changes against base commit
|
|
57
|
-
fileDiff = await git.diff(['--cached', baseCommitish, '--', path]);
|
|
58
|
+
fileDiff = await git.diff(['--cached', baseCommitish, ...contextArgs, '--', path]);
|
|
58
59
|
}
|
|
59
60
|
else if (targetCommitish === '.') {
|
|
60
61
|
// Show all uncommitted changes against base commit
|
|
61
|
-
fileDiff = await git.diff([baseCommitish, '--', path]);
|
|
62
|
+
fileDiff = await git.diff([baseCommitish, ...contextArgs, '--', path]);
|
|
62
63
|
}
|
|
63
64
|
else {
|
|
64
65
|
try {
|
|
65
66
|
// Both are regular commits: standard commit-to-commit comparison
|
|
66
67
|
fileDiff = await git.diff([
|
|
67
68
|
createCommitRangeString(baseCommitish, targetCommitish),
|
|
69
|
+
...contextArgs,
|
|
68
70
|
'--',
|
|
69
71
|
path,
|
|
70
72
|
]);
|
|
71
73
|
}
|
|
72
74
|
catch {
|
|
73
75
|
// For new files or if parent doesn't exist
|
|
74
|
-
fileDiff = await git.diff([targetCommitish, '--', path]);
|
|
76
|
+
fileDiff = await git.diff([targetCommitish, ...contextArgs, '--', path]);
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
const lines = fileDiff.split('\n');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { loadGitDiff } from './git-diff-tui.js';
|
|
3
|
+
const mockDiff = vi.hoisted(() => vi.fn());
|
|
4
|
+
const mockSimpleGit = vi.hoisted(() => vi.fn(() => ({ diff: mockDiff })));
|
|
5
|
+
vi.mock('simple-git', () => ({
|
|
6
|
+
default: mockSimpleGit,
|
|
7
|
+
}));
|
|
8
|
+
describe('loadGitDiff', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockDiff.mockReset();
|
|
11
|
+
mockSimpleGit.mockClear();
|
|
12
|
+
});
|
|
13
|
+
it.each([
|
|
14
|
+
{
|
|
15
|
+
name: 'working tree diffs',
|
|
16
|
+
targetCommitish: 'working',
|
|
17
|
+
baseCommitish: 'staged',
|
|
18
|
+
expectedListArgs: ['--name-status'],
|
|
19
|
+
expectedFileArgs: ['-U5', '--', 'src/file.ts'],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'staged diffs',
|
|
23
|
+
targetCommitish: 'staged',
|
|
24
|
+
baseCommitish: 'HEAD',
|
|
25
|
+
expectedListArgs: ['--cached', 'HEAD', '--name-status'],
|
|
26
|
+
expectedFileArgs: ['--cached', 'HEAD', '-U5', '--', 'src/file.ts'],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'working tree against a base commit',
|
|
30
|
+
targetCommitish: '.',
|
|
31
|
+
baseCommitish: 'HEAD',
|
|
32
|
+
expectedListArgs: ['HEAD', '--name-status'],
|
|
33
|
+
expectedFileArgs: ['HEAD', '-U5', '--', 'src/file.ts'],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'commit comparisons',
|
|
37
|
+
targetCommitish: 'HEAD',
|
|
38
|
+
baseCommitish: 'HEAD^',
|
|
39
|
+
expectedListArgs: ['HEAD^...HEAD', '--name-status'],
|
|
40
|
+
expectedFileArgs: ['HEAD^...HEAD', '-U5', '--', 'src/file.ts'],
|
|
41
|
+
},
|
|
42
|
+
])('passes context lines for $name', async ({ targetCommitish, baseCommitish, expectedListArgs, expectedFileArgs }) => {
|
|
43
|
+
mockDiff
|
|
44
|
+
.mockResolvedValueOnce('M\tsrc/file.ts')
|
|
45
|
+
.mockResolvedValueOnce('@@ -1 +1 @@\n-old line\n+new line\n');
|
|
46
|
+
const result = await loadGitDiff(targetCommitish, baseCommitish, '/repo', 5);
|
|
47
|
+
expect(mockSimpleGit).toHaveBeenCalledWith('/repo');
|
|
48
|
+
expect(mockDiff).toHaveBeenNthCalledWith(1, expectedListArgs);
|
|
49
|
+
expect(mockDiff).toHaveBeenNthCalledWith(2, expectedFileArgs);
|
|
50
|
+
expect(result).toEqual([
|
|
51
|
+
{
|
|
52
|
+
path: 'src/file.ts',
|
|
53
|
+
status: 'M',
|
|
54
|
+
diff: '@@ -1 +1 @@\n-old line\n+new line\n',
|
|
55
|
+
additions: 1,
|
|
56
|
+
deletions: 1,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -6,7 +6,8 @@ export declare class GitDiffParser {
|
|
|
6
6
|
private static readonly RESOLVED_COMMIT_CACHE_TTL_MS;
|
|
7
7
|
private static readonly GENERATED_HEADER_SCAN_BYTES;
|
|
8
8
|
constructor(repoPath?: string);
|
|
9
|
-
|
|
9
|
+
private normalizeRepositoryRelativePath;
|
|
10
|
+
parseDiff(targetCommitish: string, baseCommitish: string, ignoreWhitespace?: boolean, contextLines?: number): Promise<DiffResponse>;
|
|
10
11
|
private parseUnifiedDiff;
|
|
11
12
|
private decodeGitPath;
|
|
12
13
|
private extractPathFromLine;
|
|
@@ -26,6 +27,7 @@ export declare class GitDiffParser {
|
|
|
26
27
|
resolveCommitish(commitish: string): Promise<string>;
|
|
27
28
|
clearResolvedCommitCache(): void;
|
|
28
29
|
getDefaultBranch(): Promise<string | null>;
|
|
30
|
+
getOriginDefaultBranch(): Promise<string | null>;
|
|
29
31
|
getRevisionOptions(currentBase?: string, currentTarget?: string): Promise<{
|
|
30
32
|
branches: Array<{
|
|
31
33
|
name: string;
|
|
@@ -36,6 +38,7 @@ export declare class GitDiffParser {
|
|
|
36
38
|
shortHash: string;
|
|
37
39
|
message: string;
|
|
38
40
|
}>;
|
|
41
|
+
originDefaultBranch?: string;
|
|
39
42
|
resolvedBase?: string;
|
|
40
43
|
resolvedTarget?: string;
|
|
41
44
|
}>;
|
package/dist/server/git-diff.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { simpleGit } from 'simple-git';
|
|
2
|
+
import { isAbsolute, resolve, sep } from 'path';
|
|
2
3
|
import { validateDiffArguments, shortHash, createCommitRangeString } from '../cli/utils.js';
|
|
3
4
|
import { isGeneratedFile } from './generated-file-check.js';
|
|
4
5
|
export class GitDiffParser {
|
|
@@ -11,7 +12,23 @@ export class GitDiffParser {
|
|
|
11
12
|
this.repoPath = repoPath;
|
|
12
13
|
this.git = simpleGit(repoPath);
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
normalizeRepositoryRelativePath(filepath) {
|
|
16
|
+
if (filepath.length === 0) {
|
|
17
|
+
throw new Error('Invalid file path');
|
|
18
|
+
}
|
|
19
|
+
const normalizedFilepath = filepath.replace(/\\/g, '/');
|
|
20
|
+
const hasParentTraversal = normalizedFilepath.split('/').some((segment) => segment === '..');
|
|
21
|
+
if (isAbsolute(filepath) || normalizedFilepath.startsWith('/') || hasParentTraversal) {
|
|
22
|
+
throw new Error('File path outside repository');
|
|
23
|
+
}
|
|
24
|
+
const repositoryPath = resolve(this.repoPath);
|
|
25
|
+
const resolvedPath = resolve(repositoryPath, normalizedFilepath);
|
|
26
|
+
if (resolvedPath !== repositoryPath && !resolvedPath.startsWith(`${repositoryPath}${sep}`)) {
|
|
27
|
+
throw new Error('File path outside repository');
|
|
28
|
+
}
|
|
29
|
+
return normalizedFilepath;
|
|
30
|
+
}
|
|
31
|
+
async parseDiff(targetCommitish, baseCommitish, ignoreWhitespace = false, contextLines) {
|
|
15
32
|
try {
|
|
16
33
|
// Validate arguments
|
|
17
34
|
const validation = validateDiffArguments(targetCommitish, baseCommitish);
|
|
@@ -48,6 +65,9 @@ export class GitDiffParser {
|
|
|
48
65
|
if (ignoreWhitespace) {
|
|
49
66
|
diffArgs.push('-w');
|
|
50
67
|
}
|
|
68
|
+
if (contextLines !== undefined) {
|
|
69
|
+
diffArgs.push(`-U${contextLines}`);
|
|
70
|
+
}
|
|
51
71
|
// Ignore external diff-tools to unify output.
|
|
52
72
|
// https://github.com/yoshiko-pg/difit/issues/19
|
|
53
73
|
diffArgs.push('--no-ext-diff', '--color=never');
|
|
@@ -339,12 +359,18 @@ export class GitDiffParser {
|
|
|
339
359
|
// For working directory, read directly from filesystem
|
|
340
360
|
if (ref === 'working' || ref === '.') {
|
|
341
361
|
const fs = await import('fs');
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
362
|
+
if (filepath.length === 0) {
|
|
363
|
+
throw new Error('Invalid file path');
|
|
364
|
+
}
|
|
365
|
+
const repositoryPath = fs.realpathSync(resolve(this.repoPath));
|
|
366
|
+
const repositoryRoot = `${repositoryPath}${sep}`;
|
|
367
|
+
const absolutePath = fs.realpathSync(resolve(repositoryRoot, filepath.replace(/\\/g, '/')));
|
|
368
|
+
if (!absolutePath.startsWith(repositoryRoot)) {
|
|
369
|
+
throw new Error('File path outside repository');
|
|
370
|
+
}
|
|
346
371
|
return fs.readFileSync(absolutePath);
|
|
347
372
|
}
|
|
373
|
+
const normalizedFilepath = this.normalizeRepositoryRelativePath(filepath);
|
|
348
374
|
// For git refs, we need to use child_process to execute git cat-file
|
|
349
375
|
// to properly handle binary data
|
|
350
376
|
const { execFileSync } = await import('child_process');
|
|
@@ -352,14 +378,14 @@ export class GitDiffParser {
|
|
|
352
378
|
if (ref === 'staged') {
|
|
353
379
|
// For staged files, use git show :filepath
|
|
354
380
|
// Using execFileSync to prevent command injection
|
|
355
|
-
const buffer = execFileSync('git', ['show', `:${
|
|
381
|
+
const buffer = execFileSync('git', ['show', `:${normalizedFilepath}`], {
|
|
356
382
|
maxBuffer: 10 * 1024 * 1024, // 10MB limit
|
|
357
383
|
});
|
|
358
384
|
return buffer;
|
|
359
385
|
}
|
|
360
386
|
// First, get the blob hash for the file at the given ref
|
|
361
387
|
// Using execFileSync to prevent command injection
|
|
362
|
-
const blobHash = execFileSync('git', ['rev-parse', `${ref}:${
|
|
388
|
+
const blobHash = execFileSync('git', ['rev-parse', `${ref}:${normalizedFilepath}`], {
|
|
363
389
|
encoding: 'utf8',
|
|
364
390
|
maxBuffer: 10 * 1024 * 1024,
|
|
365
391
|
}).trim();
|
|
@@ -376,6 +402,10 @@ export class GitDiffParser {
|
|
|
376
402
|
(error.message.includes('ENOBUFS') || error.message.includes('maxBuffer'))) {
|
|
377
403
|
throw new Error(`Image file ${filepath} is too large to display (over 10MB limit)`);
|
|
378
404
|
}
|
|
405
|
+
if (error instanceof Error &&
|
|
406
|
+
(error.message === 'Invalid file path' || error.message === 'File path outside repository')) {
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
379
409
|
throw new Error(`Failed to get blob content for ${filepath} at ${ref}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
380
410
|
}
|
|
381
411
|
}
|
|
@@ -451,11 +481,39 @@ export class GitDiffParser {
|
|
|
451
481
|
}
|
|
452
482
|
return null;
|
|
453
483
|
}
|
|
484
|
+
async getOriginDefaultBranch() {
|
|
485
|
+
try {
|
|
486
|
+
const result = await this.git.raw(['symbolic-ref', 'refs/remotes/origin/HEAD']);
|
|
487
|
+
const match = result.trim().match(/refs\/remotes\/origin\/(.+)/);
|
|
488
|
+
if (match) {
|
|
489
|
+
return `origin/${match[1]}`;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
const commonDefaults = ['main', 'master'];
|
|
494
|
+
for (const defaultName of commonDefaults) {
|
|
495
|
+
try {
|
|
496
|
+
await this.git.raw([
|
|
497
|
+
'show-ref',
|
|
498
|
+
'--verify',
|
|
499
|
+
'--quiet',
|
|
500
|
+
`refs/remotes/origin/${defaultName}`,
|
|
501
|
+
]);
|
|
502
|
+
return `origin/${defaultName}`;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// Ignore missing refs and continue checking common defaults.
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
454
511
|
async getRevisionOptions(currentBase, currentTarget) {
|
|
455
|
-
const [branchResult, logResult, defaultBranch] = await Promise.all([
|
|
512
|
+
const [branchResult, logResult, defaultBranch, originDefaultBranch] = await Promise.all([
|
|
456
513
|
this.git.branchLocal(),
|
|
457
514
|
this.git.log({ maxCount: 20 }),
|
|
458
515
|
this.getDefaultBranch(),
|
|
516
|
+
this.getOriginDefaultBranch(),
|
|
459
517
|
]);
|
|
460
518
|
const branches = Object.entries(branchResult.branches).map(([name, data]) => ({
|
|
461
519
|
name,
|
|
@@ -499,6 +557,12 @@ export class GitDiffParser {
|
|
|
499
557
|
// If resolution fails, leave undefined
|
|
500
558
|
}
|
|
501
559
|
}
|
|
502
|
-
return {
|
|
560
|
+
return {
|
|
561
|
+
branches,
|
|
562
|
+
commits,
|
|
563
|
+
originDefaultBranch: originDefaultBranch ?? undefined,
|
|
564
|
+
resolvedBase,
|
|
565
|
+
resolvedTarget,
|
|
566
|
+
};
|
|
503
567
|
}
|
|
504
568
|
}
|
|
@@ -23,12 +23,14 @@ vi.mock('fs', async (importOriginal) => {
|
|
|
23
23
|
return {
|
|
24
24
|
...actual,
|
|
25
25
|
readFileSync: vi.fn(),
|
|
26
|
+
realpathSync: vi.fn((path) => path),
|
|
26
27
|
};
|
|
27
28
|
});
|
|
28
29
|
describe('GitDiffParser', () => {
|
|
29
30
|
let parser;
|
|
30
31
|
let mockExecFileSync;
|
|
31
32
|
let mockReadFileSync;
|
|
33
|
+
let mockRealpathSync;
|
|
32
34
|
beforeEach(async () => {
|
|
33
35
|
parser = new GitDiffParser('/test/repo');
|
|
34
36
|
vi.clearAllMocks();
|
|
@@ -37,6 +39,8 @@ describe('GitDiffParser', () => {
|
|
|
37
39
|
const fs = await import('fs');
|
|
38
40
|
mockExecFileSync = childProcess.execFileSync;
|
|
39
41
|
mockReadFileSync = fs.readFileSync;
|
|
42
|
+
mockRealpathSync = fs.realpathSync;
|
|
43
|
+
mockRealpathSync.mockImplementation((path) => path);
|
|
40
44
|
});
|
|
41
45
|
afterEach(() => {
|
|
42
46
|
vi.restoreAllMocks();
|
|
@@ -102,6 +106,28 @@ describe('GitDiffParser', () => {
|
|
|
102
106
|
});
|
|
103
107
|
await expect(parser.getBlobContent('missing.txt', 'HEAD')).rejects.toThrow('Failed to get blob content for missing.txt at HEAD: fatal: Path does not exist');
|
|
104
108
|
});
|
|
109
|
+
it('rejects working tree paths outside the repository', async () => {
|
|
110
|
+
await expect(parser.getBlobContent('../outside.txt', 'working')).rejects.toThrow('File path outside repository');
|
|
111
|
+
expect(mockReadFileSync).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
it('rejects absolute working tree paths', async () => {
|
|
114
|
+
await expect(parser.getBlobContent('/etc/passwd', 'working')).rejects.toThrow('File path outside repository');
|
|
115
|
+
expect(mockReadFileSync).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
it('rejects working tree symlinks that resolve outside the repository', async () => {
|
|
118
|
+
mockRealpathSync.mockImplementation((path) => {
|
|
119
|
+
if (path === '/test/repo/link.txt') {
|
|
120
|
+
return '/etc/passwd';
|
|
121
|
+
}
|
|
122
|
+
return path;
|
|
123
|
+
});
|
|
124
|
+
await expect(parser.getBlobContent('link.txt', 'working')).rejects.toThrow('File path outside repository');
|
|
125
|
+
expect(mockReadFileSync).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
it('rejects git ref paths outside the repository before invoking git', async () => {
|
|
128
|
+
await expect(parser.getBlobContent('../outside.txt', 'HEAD')).rejects.toThrow('File path outside repository');
|
|
129
|
+
expect(mockExecFileSync).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
105
131
|
});
|
|
106
132
|
describe('parseFileBlock with binary files', () => {
|
|
107
133
|
it('parses added binary file correctly', () => {
|
|
@@ -1045,6 +1071,26 @@ index abc123..def456 100644
|
|
|
1045
1071
|
});
|
|
1046
1072
|
});
|
|
1047
1073
|
describe('parseDiff', () => {
|
|
1074
|
+
it('passes context lines through to git diff', async () => {
|
|
1075
|
+
const gitDiff = parser.git.diff;
|
|
1076
|
+
const gitRevparse = parser.git.revparse;
|
|
1077
|
+
gitRevparse
|
|
1078
|
+
.mockResolvedValueOnce('1234567890abcdef1234567890abcdef12345678')
|
|
1079
|
+
.mockResolvedValueOnce('abcdef1234567890abcdef1234567890abcdef12');
|
|
1080
|
+
gitDiff.mockResolvedValue('');
|
|
1081
|
+
const response = await parser.parseDiff('HEAD', 'HEAD~1', false, 5);
|
|
1082
|
+
expect(gitDiff).toHaveBeenCalledWith([
|
|
1083
|
+
'abcdef1...1234567',
|
|
1084
|
+
'-U5',
|
|
1085
|
+
'--no-ext-diff',
|
|
1086
|
+
'--color=never',
|
|
1087
|
+
]);
|
|
1088
|
+
expect(response).toEqual({
|
|
1089
|
+
commit: 'abcdef1...1234567',
|
|
1090
|
+
files: [],
|
|
1091
|
+
isEmpty: true,
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1048
1094
|
it('accepts branch refs with revision suffixes', async () => {
|
|
1049
1095
|
const gitDiff = parser.git.diff;
|
|
1050
1096
|
const gitRevparse = parser.git.revparse;
|
package/dist/server/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Server } from 'http';
|
|
2
2
|
import { type DiffMode } from '../types/watch.js';
|
|
3
|
+
import { type CommentImport } from '@/types/diff.js';
|
|
3
4
|
interface ServerOptions {
|
|
4
5
|
targetCommitish?: string;
|
|
5
6
|
baseCommitish?: string;
|
|
@@ -10,9 +11,11 @@ interface ServerOptions {
|
|
|
10
11
|
mode?: string;
|
|
11
12
|
ignoreWhitespace?: boolean;
|
|
12
13
|
clearComments?: boolean;
|
|
14
|
+
commentImports?: CommentImport[];
|
|
13
15
|
keepAlive?: boolean;
|
|
14
16
|
diffMode?: DiffMode;
|
|
15
17
|
repoPath?: string;
|
|
18
|
+
contextLines?: number;
|
|
16
19
|
}
|
|
17
20
|
export declare function startServer(options: ServerOptions): Promise<{
|
|
18
21
|
port: number;
|