difit 3.1.17 → 4.0.0
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 +30 -3
- package/README.ko.md +30 -3
- package/README.md +30 -3
- package/README.zh.md +30 -3
- 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 +26 -1
- package/dist/cli/index.test.js +206 -4
- package/dist/cli/utils.d.ts +2 -8
- package/dist/cli/utils.js +43 -56
- package/dist/cli/utils.test.js +61 -67
- package/dist/client/assets/{_basePickBy-r8KiD0PT.js → _basePickBy-B9N-f0iT.js} +1 -1
- package/dist/client/assets/{_baseUniq-WYpg9s_f.js → _baseUniq-tbL7nVvN.js} +1 -1
- package/dist/client/assets/{arc-BZWd656X.js → arc-BOY-7mep.js} +1 -1
- package/dist/client/assets/{architectureDiagram-2XIMDMQ5-BiaoV1Oc.js → architectureDiagram-2XIMDMQ5-59AvHaSB.js} +1 -1
- package/dist/client/assets/{blockDiagram-WCTKOSBZ-T1RU4TI6.js → blockDiagram-WCTKOSBZ-DXIlumQk.js} +1 -1
- package/dist/client/assets/{c4Diagram-IC4MRINW-C1aQSMsj.js → c4Diagram-IC4MRINW-BbfZ0uRn.js} +1 -1
- package/dist/client/assets/channel-cZXsTJxA.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-DFcwtPlK.js → chunk-4BX2VUAB-l7rcB2IW.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-Bl3vvNDx.js → chunk-55IACEB6-CrZL3qv9.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-B_2obFwM.js → chunk-FMBD7UC4-CrKv7ndg.js} +1 -1
- package/dist/client/assets/{chunk-JSJVCQXG-BrSq4jyX.js → chunk-JSJVCQXG-DyBDhAEM.js} +1 -1
- package/dist/client/assets/{chunk-KX2RTZJC-18m3UONJ.js → chunk-KX2RTZJC-By5mkZmU.js} +1 -1
- package/dist/client/assets/{chunk-NQ4KR5QH-hFDbMzZU.js → chunk-NQ4KR5QH-C30p9xRx.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-CyCFXX2j.js → chunk-QZHKN3VN-DVlhR2wU.js} +1 -1
- package/dist/client/assets/{chunk-WL4C6EOR-BDdHa7t1.js → chunk-WL4C6EOR-Cn7a6CO3.js} +1 -1
- package/dist/client/assets/classDiagram-VBA2DB6C-B_coIPEy.js +1 -0
- package/dist/client/assets/classDiagram-v2-RAHNMMFH-B_coIPEy.js +1 -0
- package/dist/client/assets/clone-BjaT2HOk.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-D7t718Sq.js → cose-bilkent-S5V4N54A-LyauIk_9.js} +1 -1
- package/dist/client/assets/{dagre-KLK3FWXG-DJXcjsV8.js → dagre-KLK3FWXG-DRWb2KE3.js} +1 -1
- package/dist/client/assets/{diagram-E7M64L7V-DL8ck_Al.js → diagram-E7M64L7V-ChT6mNWK.js} +1 -1
- package/dist/client/assets/{diagram-IFDJBPK2-NTxUWyD3.js → diagram-IFDJBPK2-CqbTduoP.js} +1 -1
- package/dist/client/assets/{diagram-P4PSJMXO-CGkcnGxk.js → diagram-P4PSJMXO-Bzv5Z3ri.js} +1 -1
- package/dist/client/assets/{erDiagram-INFDFZHY-BqpbHQrZ.js → erDiagram-INFDFZHY-CvXfUZ4L.js} +1 -1
- package/dist/client/assets/{flowDiagram-PKNHOUZH-B-DK3_9I.js → flowDiagram-PKNHOUZH-CxmpNUKq.js} +1 -1
- package/dist/client/assets/{ganttDiagram-A5KZAMGK-BK1C57ll.js → ganttDiagram-A5KZAMGK-9LpZCsg6.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-K3NZZRJ6-Duxlcz8R.js → gitGraphDiagram-K3NZZRJ6-C6yZOrQJ.js} +1 -1
- package/dist/client/assets/{graph-C7r58m4O.js → graph-bUZ7uHLW.js} +1 -1
- package/dist/client/assets/index-BLNN1bfE.js +98 -0
- package/dist/client/assets/index-VxkpzDXr.css +1 -0
- package/dist/client/assets/{infoDiagram-LFFYTUFH-Bqt-4V9X.js → infoDiagram-LFFYTUFH-Djdy3W21.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-PHBUUO56-B1ZVSkls.js → ishikawaDiagram-PHBUUO56-oOdwCpeS.js} +1 -1
- package/dist/client/assets/{journeyDiagram-4ABVD52K-LSEcxqrO.js → journeyDiagram-4ABVD52K-DTb_nGAw.js} +1 -1
- package/dist/client/assets/{kanban-definition-K7BYSVSG-CldPadPs.js → kanban-definition-K7BYSVSG-CMtP7pHA.js} +1 -1
- package/dist/client/assets/{layout-NpxIVVkp.js → layout-CXr5MatK.js} +1 -1
- package/dist/client/assets/{linear-JpKpxaS-.js → linear-pOMS9pjV.js} +1 -1
- package/dist/client/assets/{mermaid.core-gANNEmg0.js → mermaid.core-DV5JJ1Ie.js} +4 -4
- package/dist/client/assets/{mindmap-definition-YRQLILUH-ewFI1yc5.js → mindmap-definition-YRQLILUH-DN-sbonc.js} +1 -1
- package/dist/client/assets/{pieDiagram-SKSYHLDU-CWlAr2t8.js → pieDiagram-SKSYHLDU-tAHCkgh1.js} +1 -1
- package/dist/client/assets/{prism-csharp-CxRfePTX.js → prism-csharp-5CQ0RcEE.js} +1 -1
- package/dist/client/assets/{prism-elixir-B0H1PC_E.js → prism-elixir-BSOTyVg2.js} +1 -1
- package/dist/client/assets/{prism-hcl-Csmcce-t.js → prism-hcl-BYvi1mtM.js} +1 -1
- package/dist/client/assets/{prism-java-BRzwomgj.js → prism-java-DMU2FM4X.js} +1 -1
- package/dist/client/assets/{prism-perl-DQMRA6u_.js → prism-perl-CpfvaEQk.js} +1 -1
- package/dist/client/assets/{prism-php-C6fR1C7-.js → prism-php-SC920LoD.js} +1 -1
- package/dist/client/assets/{prism-ruby-CWeh27h1.js → prism-ruby-DZph-YiO.js} +1 -1
- package/dist/client/assets/{prism-solidity-3wCU4ra_.js → prism-solidity-qTLbmiAT.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-337W2JSQ-D76E3PCD.js → quadrantDiagram-337W2JSQ-B0wODmgR.js} +1 -1
- package/dist/client/assets/{requirementDiagram-Z7DCOOCP-C49LvKzR.js → requirementDiagram-Z7DCOOCP-A3aeHC06.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-WA2Y5GQK-DOvEhLMf.js → sankeyDiagram-WA2Y5GQK-BWa6kZhG.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-2WXFIKYE-BR6dsfEq.js → sequenceDiagram-2WXFIKYE-Cx_COX9G.js} +1 -1
- package/dist/client/assets/{stateDiagram-RAJIS63D-CHII26YE.js → stateDiagram-RAJIS63D-BXGnN6rZ.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-CMw3xNha.js +1 -0
- package/dist/client/assets/{timeline-definition-YZTLITO2-DhUTiAsW.js → timeline-definition-YZTLITO2-DbqaUm9k.js} +1 -1
- package/dist/client/assets/{treemap-KZPCXAKY-C0Rh3R0y.js → treemap-KZPCXAKY-CfEujPCR.js} +1 -1
- package/dist/client/assets/{vennDiagram-LZ73GAT5-CWt3wBDG.js → vennDiagram-LZ73GAT5-CqJE8CAD.js} +1 -1
- package/dist/client/assets/{xychartDiagram-JWTSCODW-DhwJwxGz.js → xychartDiagram-JWTSCODW-CfdDvzHC.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.d.ts +1 -0
- package/dist/server/git-diff.js +33 -6
- package/dist/server/git-diff.test.js +45 -0
- package/dist/server/server.d.ts +2 -0
- package/dist/server/server.js +107 -34
- package/dist/server/server.test.js +120 -0
- package/dist/types/diff.d.ts +73 -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 +3 -3
- package/dist/client/assets/channel-C081SflL.js +0 -1
- package/dist/client/assets/classDiagram-VBA2DB6C-CD8hB8X7.js +0 -1
- package/dist/client/assets/classDiagram-v2-RAHNMMFH-CD8hB8X7.js +0 -1
- package/dist/client/assets/clone-DL1yO1kL.js +0 -1
- package/dist/client/assets/index-DcsVWNsS.css +0 -1
- package/dist/client/assets/index-Igyd6olF.js +0 -92
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-DtCFGPiV.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);
|
|
@@ -6,6 +6,7 @@ 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
|
+
private normalizeRepositoryRelativePath;
|
|
9
10
|
parseDiff(targetCommitish: string, baseCommitish: string, ignoreWhitespace?: boolean): Promise<DiffResponse>;
|
|
10
11
|
private parseUnifiedDiff;
|
|
11
12
|
private decodeGitPath;
|
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,6 +12,22 @@ export class GitDiffParser {
|
|
|
11
12
|
this.repoPath = repoPath;
|
|
12
13
|
this.git = simpleGit(repoPath);
|
|
13
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
|
+
}
|
|
14
31
|
async parseDiff(targetCommitish, baseCommitish, ignoreWhitespace = false) {
|
|
15
32
|
try {
|
|
16
33
|
// Validate arguments
|
|
@@ -339,12 +356,18 @@ export class GitDiffParser {
|
|
|
339
356
|
// For working directory, read directly from filesystem
|
|
340
357
|
if (ref === 'working' || ref === '.') {
|
|
341
358
|
const fs = await import('fs');
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
359
|
+
if (filepath.length === 0) {
|
|
360
|
+
throw new Error('Invalid file path');
|
|
361
|
+
}
|
|
362
|
+
const repositoryPath = fs.realpathSync(resolve(this.repoPath));
|
|
363
|
+
const repositoryRoot = `${repositoryPath}${sep}`;
|
|
364
|
+
const absolutePath = fs.realpathSync(resolve(repositoryRoot, filepath.replace(/\\/g, '/')));
|
|
365
|
+
if (!absolutePath.startsWith(repositoryRoot)) {
|
|
366
|
+
throw new Error('File path outside repository');
|
|
367
|
+
}
|
|
346
368
|
return fs.readFileSync(absolutePath);
|
|
347
369
|
}
|
|
370
|
+
const normalizedFilepath = this.normalizeRepositoryRelativePath(filepath);
|
|
348
371
|
// For git refs, we need to use child_process to execute git cat-file
|
|
349
372
|
// to properly handle binary data
|
|
350
373
|
const { execFileSync } = await import('child_process');
|
|
@@ -352,14 +375,14 @@ export class GitDiffParser {
|
|
|
352
375
|
if (ref === 'staged') {
|
|
353
376
|
// For staged files, use git show :filepath
|
|
354
377
|
// Using execFileSync to prevent command injection
|
|
355
|
-
const buffer = execFileSync('git', ['show', `:${
|
|
378
|
+
const buffer = execFileSync('git', ['show', `:${normalizedFilepath}`], {
|
|
356
379
|
maxBuffer: 10 * 1024 * 1024, // 10MB limit
|
|
357
380
|
});
|
|
358
381
|
return buffer;
|
|
359
382
|
}
|
|
360
383
|
// First, get the blob hash for the file at the given ref
|
|
361
384
|
// Using execFileSync to prevent command injection
|
|
362
|
-
const blobHash = execFileSync('git', ['rev-parse', `${ref}:${
|
|
385
|
+
const blobHash = execFileSync('git', ['rev-parse', `${ref}:${normalizedFilepath}`], {
|
|
363
386
|
encoding: 'utf8',
|
|
364
387
|
maxBuffer: 10 * 1024 * 1024,
|
|
365
388
|
}).trim();
|
|
@@ -376,6 +399,10 @@ export class GitDiffParser {
|
|
|
376
399
|
(error.message.includes('ENOBUFS') || error.message.includes('maxBuffer'))) {
|
|
377
400
|
throw new Error(`Image file ${filepath} is too large to display (over 10MB limit)`);
|
|
378
401
|
}
|
|
402
|
+
if (error instanceof Error &&
|
|
403
|
+
(error.message === 'Invalid file path' || error.message === 'File path outside repository')) {
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
379
406
|
throw new Error(`Failed to get blob content for ${filepath} at ${ref}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
380
407
|
}
|
|
381
408
|
}
|
|
@@ -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', () => {
|
|
@@ -1044,4 +1070,23 @@ index abc123..def456 100644
|
|
|
1044
1070
|
expect(result.isGenerated).toBe(true);
|
|
1045
1071
|
});
|
|
1046
1072
|
});
|
|
1073
|
+
describe('parseDiff', () => {
|
|
1074
|
+
it('accepts branch refs with revision suffixes', 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('codex/comment-thread', 'codex/comment-thread^');
|
|
1082
|
+
expect(gitRevparse).toHaveBeenNthCalledWith(1, ['codex/comment-thread']);
|
|
1083
|
+
expect(gitRevparse).toHaveBeenNthCalledWith(2, ['codex/comment-thread^']);
|
|
1084
|
+
expect(gitDiff).toHaveBeenCalledWith(['abcdef1...1234567', '--no-ext-diff', '--color=never']);
|
|
1085
|
+
expect(response).toEqual({
|
|
1086
|
+
commit: 'abcdef1...1234567',
|
|
1087
|
+
files: [],
|
|
1088
|
+
isEmpty: true,
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1047
1092
|
});
|
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,6 +11,7 @@ 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;
|
package/dist/server/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import open from 'open';
|
|
|
7
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
8
|
const __dirname = dirname(__filename);
|
|
9
9
|
import { formatCommentsOutput } from '../utils/commentFormatting.js';
|
|
10
|
+
import { serializeCommentImports } from '../utils/commentImports.js';
|
|
10
11
|
import { normalizeDiffViewMode } from '../utils/diffMode.js';
|
|
11
12
|
import { resolveEditorOption } from '../utils/editorOptions.js';
|
|
12
13
|
import { getFileExtension } from '../utils/fileUtils.js';
|
|
@@ -17,6 +18,12 @@ export async function startServer(options) {
|
|
|
17
18
|
const app = express();
|
|
18
19
|
const repositoryPath = resolve(options.repoPath ?? process.cwd());
|
|
19
20
|
const repositoryId = createHash('sha256').update(repositoryPath).digest('hex');
|
|
21
|
+
const initialCommentImports = options.commentImports || [];
|
|
22
|
+
const initialBaseCommitish = options.baseCommitish ?? '';
|
|
23
|
+
const initialTargetCommitish = options.targetCommitish ?? '';
|
|
24
|
+
const commentImportId = initialCommentImports.length > 0
|
|
25
|
+
? createHash('sha256').update(serializeCommentImports(initialCommentImports)).digest('hex')
|
|
26
|
+
: undefined;
|
|
20
27
|
const parser = new GitDiffParser(repositoryPath);
|
|
21
28
|
const fileWatcher = new FileWatcherService();
|
|
22
29
|
const generatedStatusCache = new Map();
|
|
@@ -55,10 +62,28 @@ export async function startServer(options) {
|
|
|
55
62
|
// Track current revisions for cache invalidation
|
|
56
63
|
let currentBaseCommitish = options.baseCommitish ?? '';
|
|
57
64
|
let currentTargetCommitish = options.targetCommitish ?? '';
|
|
65
|
+
function parseRepositoryRelativePath(filepath) {
|
|
66
|
+
if (typeof filepath !== 'string' || filepath.length === 0) {
|
|
67
|
+
return { ok: false, error: 'Invalid file path' };
|
|
68
|
+
}
|
|
69
|
+
const normalizedFilepath = filepath.replace(/\\/g, '/');
|
|
70
|
+
const hasParentTraversal = normalizedFilepath.split('/').some((segment) => segment === '..');
|
|
71
|
+
if (isAbsolute(filepath) || normalizedFilepath.startsWith('/') || hasParentTraversal) {
|
|
72
|
+
return { ok: false, error: 'File path outside repository' };
|
|
73
|
+
}
|
|
74
|
+
const resolvedPath = resolve(repositoryPath, normalizedFilepath);
|
|
75
|
+
if (resolvedPath !== repositoryPath && !resolvedPath.startsWith(`${repositoryPath}${sep}`)) {
|
|
76
|
+
return { ok: false, error: 'File path outside repository' };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, path: normalizedFilepath };
|
|
79
|
+
}
|
|
58
80
|
app.get('/api/diff', async (req, res) => {
|
|
59
81
|
const ignoreWhitespace = req.query.ignoreWhitespace === 'true';
|
|
60
82
|
const requestedBase = req.query.base || options.baseCommitish || '';
|
|
61
83
|
const requestedTarget = req.query.target || options.targetCommitish || '';
|
|
84
|
+
const shouldIncludeCommentImports = initialCommentImports.length > 0 &&
|
|
85
|
+
(Boolean(options.stdinDiff) ||
|
|
86
|
+
(requestedBase === initialBaseCommitish && requestedTarget === initialTargetCommitish));
|
|
62
87
|
// Check if revisions or whitespace setting changed
|
|
63
88
|
const revisionsChanged = requestedBase !== currentBaseCommitish || requestedTarget !== currentTargetCommitish;
|
|
64
89
|
const whitespaceChanged = ignoreWhitespace !== currentIgnoreWhitespace;
|
|
@@ -106,6 +131,8 @@ export async function startServer(options) {
|
|
|
106
131
|
requestedTargetCommitish,
|
|
107
132
|
clearComments: options.clearComments,
|
|
108
133
|
repositoryId,
|
|
134
|
+
commentImports: shouldIncludeCommentImports ? initialCommentImports : undefined,
|
|
135
|
+
commentImportId: shouldIncludeCommentImports ? commentImportId : undefined,
|
|
109
136
|
});
|
|
110
137
|
});
|
|
111
138
|
app.get(/^\/api\/generated-status\/(.*)$/, async (req, res) => {
|
|
@@ -114,22 +141,12 @@ export async function startServer(options) {
|
|
|
114
141
|
return;
|
|
115
142
|
}
|
|
116
143
|
try {
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
res.status(400).json({ error:
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const normalizedFilepath = filepath.replace(/\\/g, '/');
|
|
123
|
-
const hasParentTraversal = normalizedFilepath.split('/').some((segment) => segment === '..');
|
|
124
|
-
if (isAbsolute(filepath) || normalizedFilepath.startsWith('/') || hasParentTraversal) {
|
|
125
|
-
res.status(400).json({ error: 'File path outside repository' });
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const resolvedPath = resolve(repositoryPath, normalizedFilepath);
|
|
129
|
-
if (resolvedPath !== repositoryPath && !resolvedPath.startsWith(`${repositoryPath}${sep}`)) {
|
|
130
|
-
res.status(400).json({ error: 'File path outside repository' });
|
|
144
|
+
const filepathResult = parseRepositoryRelativePath(req.params[0]);
|
|
145
|
+
if (!filepathResult.ok) {
|
|
146
|
+
res.status(400).json({ error: filepathResult.error });
|
|
131
147
|
return;
|
|
132
148
|
}
|
|
149
|
+
const normalizedFilepath = filepathResult.path;
|
|
133
150
|
const ref = req.query.ref || currentTargetCommitish || 'HEAD';
|
|
134
151
|
const cacheKey = `${ref}:${normalizedFilepath}`;
|
|
135
152
|
const now = Date.now();
|
|
@@ -187,10 +204,22 @@ export async function startServer(options) {
|
|
|
187
204
|
res.status(404).json({ error: 'Line count not available for stdin diff' });
|
|
188
205
|
return;
|
|
189
206
|
}
|
|
190
|
-
const
|
|
207
|
+
const filepathResult = parseRepositoryRelativePath(req.params[0]);
|
|
208
|
+
if (!filepathResult.ok) {
|
|
209
|
+
res.status(400).json({ error: filepathResult.error });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const filepath = filepathResult.path;
|
|
191
213
|
const oldRef = req.query.oldRef;
|
|
214
|
+
const oldPathResult = req.query.oldPath
|
|
215
|
+
? parseRepositoryRelativePath(req.query.oldPath)
|
|
216
|
+
: { ok: true, path: filepath };
|
|
217
|
+
if (!oldPathResult.ok) {
|
|
218
|
+
res.status(400).json({ error: oldPathResult.error });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
192
221
|
const newRef = req.query.newRef;
|
|
193
|
-
const oldPath =
|
|
222
|
+
const oldPath = oldPathResult.path;
|
|
194
223
|
const result = {};
|
|
195
224
|
if (oldRef) {
|
|
196
225
|
try {
|
|
@@ -222,7 +251,12 @@ export async function startServer(options) {
|
|
|
222
251
|
res.status(404).json({ error: 'Blob content not available for stdin diff' });
|
|
223
252
|
return;
|
|
224
253
|
}
|
|
225
|
-
const
|
|
254
|
+
const filepathResult = parseRepositoryRelativePath(req.params[0]);
|
|
255
|
+
if (!filepathResult.ok) {
|
|
256
|
+
res.status(400).json({ error: filepathResult.error });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const filepath = filepathResult.path;
|
|
226
260
|
const ref = req.query.ref || 'HEAD';
|
|
227
261
|
const blob = await parser.getBlobContent(filepath, ref);
|
|
228
262
|
// Determine content type based on file extension
|
|
@@ -254,18 +288,59 @@ export async function startServer(options) {
|
|
|
254
288
|
res.status(404).json({ error: 'File not found' });
|
|
255
289
|
}
|
|
256
290
|
});
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
291
|
+
let finalThreads = [];
|
|
292
|
+
function normalizeComment(comment) {
|
|
293
|
+
return {
|
|
294
|
+
id: comment.id,
|
|
295
|
+
file: comment.file,
|
|
296
|
+
line: comment.line,
|
|
297
|
+
side: comment.side,
|
|
298
|
+
createdAt: comment.timestamp,
|
|
299
|
+
updatedAt: comment.timestamp,
|
|
300
|
+
codeContent: comment.codeContent,
|
|
301
|
+
messages: [
|
|
302
|
+
{
|
|
303
|
+
id: comment.id,
|
|
304
|
+
body: comment.body,
|
|
305
|
+
author: comment.author,
|
|
306
|
+
createdAt: comment.timestamp,
|
|
307
|
+
updatedAt: comment.timestamp,
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function normalizeThreadPayload(thread) {
|
|
313
|
+
if ('file' in thread && 'line' in thread) {
|
|
314
|
+
return thread;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
id: thread.id,
|
|
318
|
+
file: thread.filePath,
|
|
319
|
+
line: typeof thread.position.line === 'number'
|
|
320
|
+
? thread.position.line
|
|
321
|
+
: [thread.position.line.start, thread.position.line.end],
|
|
322
|
+
side: thread.position.side,
|
|
323
|
+
createdAt: thread.createdAt,
|
|
324
|
+
updatedAt: thread.updatedAt,
|
|
325
|
+
codeContent: thread.codeSnapshot?.content,
|
|
326
|
+
messages: thread.messages,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
260
329
|
function parseCommentsPayload(body) {
|
|
261
330
|
const payload = typeof body === 'string'
|
|
262
331
|
? JSON.parse(body)
|
|
263
332
|
: body;
|
|
264
|
-
|
|
333
|
+
if (Array.isArray(payload.threads)) {
|
|
334
|
+
return payload.threads.map(normalizeThreadPayload);
|
|
335
|
+
}
|
|
336
|
+
if (Array.isArray(payload.comments)) {
|
|
337
|
+
return payload.comments.map(normalizeComment);
|
|
338
|
+
}
|
|
339
|
+
return [];
|
|
265
340
|
}
|
|
266
341
|
app.post('/api/comments', (req, res) => {
|
|
267
342
|
try {
|
|
268
|
-
|
|
343
|
+
finalThreads = parseCommentsPayload(req.body);
|
|
269
344
|
res.json({ success: true });
|
|
270
345
|
}
|
|
271
346
|
catch (error) {
|
|
@@ -274,8 +349,9 @@ export async function startServer(options) {
|
|
|
274
349
|
}
|
|
275
350
|
});
|
|
276
351
|
app.get('/api/comments-output', (_req, res) => {
|
|
277
|
-
|
|
278
|
-
|
|
352
|
+
res.type('text/plain');
|
|
353
|
+
if (finalThreads.length > 0) {
|
|
354
|
+
const output = formatCommentsOutput(finalThreads);
|
|
279
355
|
res.send(output);
|
|
280
356
|
}
|
|
281
357
|
else {
|
|
@@ -292,12 +368,12 @@ export async function startServer(options) {
|
|
|
292
368
|
res.status(400).json({ error: 'Invalid request payload' });
|
|
293
369
|
return;
|
|
294
370
|
}
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
res.status(400).json({ error: 'File path outside repository' });
|
|
371
|
+
const filepathResult = parseRepositoryRelativePath(filePath);
|
|
372
|
+
if (!filepathResult.ok) {
|
|
373
|
+
res.status(400).json({ error: filepathResult.error });
|
|
299
374
|
return;
|
|
300
375
|
}
|
|
376
|
+
const resolvedPath = resolve(repositoryPath, filepathResult.path);
|
|
301
377
|
const editorInput = typeof editor === 'string' ? editor : (process.env.DIFIT_EDITOR ?? process.env.EDITOR);
|
|
302
378
|
const resolvedEditor = resolveEditorOption(editorInput);
|
|
303
379
|
if (resolvedEditor.protocol === null) {
|
|
@@ -324,7 +400,7 @@ export async function startServer(options) {
|
|
|
324
400
|
else {
|
|
325
401
|
args.push(resolvedPath);
|
|
326
402
|
}
|
|
327
|
-
args.push(
|
|
403
|
+
args.push(repositoryPath);
|
|
328
404
|
return await new Promise((resolvePromise) => {
|
|
329
405
|
const child = spawn(resolvedEditor.cliCommand, args, { stdio: 'ignore', detached: true });
|
|
330
406
|
child.once('error', (error) => {
|
|
@@ -357,8 +433,8 @@ export async function startServer(options) {
|
|
|
357
433
|
});
|
|
358
434
|
// Function to output comments when server shuts down
|
|
359
435
|
function outputFinalComments() {
|
|
360
|
-
if (
|
|
361
|
-
console.log(formatCommentsOutput(
|
|
436
|
+
if (finalThreads.length > 0) {
|
|
437
|
+
console.log(formatCommentsOutput(finalThreads));
|
|
362
438
|
}
|
|
363
439
|
}
|
|
364
440
|
// SSE endpoint for file watching
|
|
@@ -413,9 +489,6 @@ export async function startServer(options) {
|
|
|
413
489
|
// Find client files relative to the CLI executable location
|
|
414
490
|
const distPath = join(__dirname, '..', 'client');
|
|
415
491
|
app.use(express.static(distPath));
|
|
416
|
-
app.get('/{*splat}', (_req, res) => {
|
|
417
|
-
res.sendFile(join(distPath, 'index.html'));
|
|
418
|
-
});
|
|
419
492
|
}
|
|
420
493
|
else {
|
|
421
494
|
app.get('/', (_req, res) => {
|