@spaceflow/core 0.1.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +1176 -0
  2. package/README.md +105 -0
  3. package/nest-cli.json +10 -0
  4. package/package.json +128 -0
  5. package/rspack.config.mjs +62 -0
  6. package/src/__mocks__/@opencode-ai/sdk.js +9 -0
  7. package/src/__mocks__/c12.ts +3 -0
  8. package/src/app.module.ts +18 -0
  9. package/src/config/ci.config.ts +29 -0
  10. package/src/config/config-loader.ts +101 -0
  11. package/src/config/config-reader.module.ts +16 -0
  12. package/src/config/config-reader.service.ts +133 -0
  13. package/src/config/feishu.config.ts +35 -0
  14. package/src/config/git-provider.config.ts +29 -0
  15. package/src/config/index.ts +29 -0
  16. package/src/config/llm.config.ts +110 -0
  17. package/src/config/schema-generator.service.ts +129 -0
  18. package/src/config/spaceflow.config.ts +292 -0
  19. package/src/config/storage.config.ts +33 -0
  20. package/src/extension-system/extension.interface.ts +221 -0
  21. package/src/extension-system/index.ts +1 -0
  22. package/src/index.ts +80 -0
  23. package/src/locales/en/translation.json +11 -0
  24. package/src/locales/zh-cn/translation.json +11 -0
  25. package/src/shared/claude-setup/claude-setup.module.ts +8 -0
  26. package/src/shared/claude-setup/claude-setup.service.ts +131 -0
  27. package/src/shared/claude-setup/index.ts +2 -0
  28. package/src/shared/editor-config/index.ts +23 -0
  29. package/src/shared/feishu-sdk/feishu-sdk.module.ts +77 -0
  30. package/src/shared/feishu-sdk/feishu-sdk.service.ts +130 -0
  31. package/src/shared/feishu-sdk/fieshu-card.service.ts +139 -0
  32. package/src/shared/feishu-sdk/index.ts +4 -0
  33. package/src/shared/feishu-sdk/types/card-action.ts +132 -0
  34. package/src/shared/feishu-sdk/types/card.ts +64 -0
  35. package/src/shared/feishu-sdk/types/common.ts +22 -0
  36. package/src/shared/feishu-sdk/types/index.ts +46 -0
  37. package/src/shared/feishu-sdk/types/message.ts +35 -0
  38. package/src/shared/feishu-sdk/types/module.ts +21 -0
  39. package/src/shared/feishu-sdk/types/user.ts +77 -0
  40. package/src/shared/git-provider/adapters/gitea.adapter.spec.ts +473 -0
  41. package/src/shared/git-provider/adapters/gitea.adapter.ts +499 -0
  42. package/src/shared/git-provider/adapters/github.adapter.spec.ts +341 -0
  43. package/src/shared/git-provider/adapters/github.adapter.ts +830 -0
  44. package/src/shared/git-provider/adapters/gitlab.adapter.ts +839 -0
  45. package/src/shared/git-provider/adapters/index.ts +3 -0
  46. package/src/shared/git-provider/detect-provider.spec.ts +195 -0
  47. package/src/shared/git-provider/detect-provider.ts +112 -0
  48. package/src/shared/git-provider/git-provider.interface.ts +188 -0
  49. package/src/shared/git-provider/git-provider.module.ts +73 -0
  50. package/src/shared/git-provider/git-provider.service.spec.ts +282 -0
  51. package/src/shared/git-provider/git-provider.service.ts +309 -0
  52. package/src/shared/git-provider/index.ts +7 -0
  53. package/src/shared/git-provider/parse-repo-url.spec.ts +221 -0
  54. package/src/shared/git-provider/parse-repo-url.ts +155 -0
  55. package/src/shared/git-provider/types.ts +434 -0
  56. package/src/shared/git-sdk/git-sdk-diff.utils.spec.ts +344 -0
  57. package/src/shared/git-sdk/git-sdk-diff.utils.ts +151 -0
  58. package/src/shared/git-sdk/git-sdk.module.ts +8 -0
  59. package/src/shared/git-sdk/git-sdk.service.ts +235 -0
  60. package/src/shared/git-sdk/git-sdk.types.ts +25 -0
  61. package/src/shared/git-sdk/index.ts +4 -0
  62. package/src/shared/i18n/i18n.spec.ts +96 -0
  63. package/src/shared/i18n/i18n.ts +86 -0
  64. package/src/shared/i18n/index.ts +1 -0
  65. package/src/shared/i18n/locale-detect.ts +134 -0
  66. package/src/shared/llm-jsonput/index.ts +94 -0
  67. package/src/shared/llm-jsonput/types.ts +17 -0
  68. package/src/shared/llm-proxy/adapters/claude-code.adapter.spec.ts +131 -0
  69. package/src/shared/llm-proxy/adapters/claude-code.adapter.ts +208 -0
  70. package/src/shared/llm-proxy/adapters/index.ts +4 -0
  71. package/src/shared/llm-proxy/adapters/llm-adapter.interface.ts +23 -0
  72. package/src/shared/llm-proxy/adapters/open-code.adapter.ts +342 -0
  73. package/src/shared/llm-proxy/adapters/openai.adapter.spec.ts +215 -0
  74. package/src/shared/llm-proxy/adapters/openai.adapter.ts +153 -0
  75. package/src/shared/llm-proxy/index.ts +6 -0
  76. package/src/shared/llm-proxy/interfaces/config.interface.ts +32 -0
  77. package/src/shared/llm-proxy/interfaces/index.ts +4 -0
  78. package/src/shared/llm-proxy/interfaces/message.interface.ts +48 -0
  79. package/src/shared/llm-proxy/interfaces/session.interface.ts +28 -0
  80. package/src/shared/llm-proxy/llm-proxy.module.ts +140 -0
  81. package/src/shared/llm-proxy/llm-proxy.service.spec.ts +303 -0
  82. package/src/shared/llm-proxy/llm-proxy.service.ts +132 -0
  83. package/src/shared/llm-proxy/llm-session.spec.ts +111 -0
  84. package/src/shared/llm-proxy/llm-session.ts +109 -0
  85. package/src/shared/llm-proxy/stream-logger.ts +97 -0
  86. package/src/shared/logger/index.ts +11 -0
  87. package/src/shared/logger/logger.interface.ts +93 -0
  88. package/src/shared/logger/logger.spec.ts +178 -0
  89. package/src/shared/logger/logger.ts +175 -0
  90. package/src/shared/logger/renderers/plain.renderer.ts +116 -0
  91. package/src/shared/logger/renderers/tui.renderer.ts +162 -0
  92. package/src/shared/mcp/index.ts +332 -0
  93. package/src/shared/output/index.ts +2 -0
  94. package/src/shared/output/output.module.ts +9 -0
  95. package/src/shared/output/output.service.ts +97 -0
  96. package/src/shared/package-manager/index.ts +115 -0
  97. package/src/shared/parallel/index.ts +1 -0
  98. package/src/shared/parallel/parallel-executor.ts +169 -0
  99. package/src/shared/rspack-config/index.ts +1 -0
  100. package/src/shared/rspack-config/rspack-config.ts +157 -0
  101. package/src/shared/source-utils/index.ts +130 -0
  102. package/src/shared/spaceflow-dir/index.ts +158 -0
  103. package/src/shared/storage/adapters/file.adapter.ts +113 -0
  104. package/src/shared/storage/adapters/index.ts +3 -0
  105. package/src/shared/storage/adapters/memory.adapter.ts +50 -0
  106. package/src/shared/storage/adapters/storage-adapter.interface.ts +48 -0
  107. package/src/shared/storage/index.ts +4 -0
  108. package/src/shared/storage/storage.module.ts +150 -0
  109. package/src/shared/storage/storage.service.ts +293 -0
  110. package/src/shared/storage/types.ts +51 -0
  111. package/src/shared/verbose/index.ts +73 -0
  112. package/test/app.e2e-spec.ts +22 -0
  113. package/tsconfig.build.json +4 -0
  114. package/tsconfig.json +25 -0
  115. package/tsconfig.skill.json +18 -0
  116. package/vitest.config.ts +58 -0
@@ -0,0 +1,344 @@
1
+ import {
2
+ mapGitStatus,
3
+ parseChangedLinesFromPatch,
4
+ parseHunksFromPatch,
5
+ calculateNewLineNumber,
6
+ calculateLineOffsets,
7
+ parseDiffText,
8
+ } from "./git-sdk-diff.utils";
9
+
10
+ describe("git-sdk-diff.utils", () => {
11
+ describe("parseHunksFromPatch", () => {
12
+ it("should parse single hunk", () => {
13
+ const patch = `@@ -1,3 +1,4 @@
14
+ line1
15
+ +new line
16
+ line2
17
+ line3`;
18
+ const hunks = parseHunksFromPatch(patch);
19
+ expect(hunks).toHaveLength(1);
20
+ expect(hunks[0]).toEqual({
21
+ oldStart: 1,
22
+ oldCount: 3,
23
+ newStart: 1,
24
+ newCount: 4,
25
+ });
26
+ });
27
+
28
+ it("should parse multiple hunks", () => {
29
+ const patch = `@@ -1,3 +1,4 @@
30
+ line1
31
+ +new line
32
+ line2
33
+ line3
34
+ @@ -10,2 +11,3 @@
35
+ line10
36
+ +another new line
37
+ line11`;
38
+ const hunks = parseHunksFromPatch(patch);
39
+ expect(hunks).toHaveLength(2);
40
+ expect(hunks[0]).toEqual({
41
+ oldStart: 1,
42
+ oldCount: 3,
43
+ newStart: 1,
44
+ newCount: 4,
45
+ });
46
+ expect(hunks[1]).toEqual({
47
+ oldStart: 10,
48
+ oldCount: 2,
49
+ newStart: 11,
50
+ newCount: 3,
51
+ });
52
+ });
53
+
54
+ it("should handle single line hunk without count", () => {
55
+ const patch = `@@ -5 +5,2 @@
56
+ line5
57
+ +new line`;
58
+ const hunks = parseHunksFromPatch(patch);
59
+ expect(hunks).toHaveLength(1);
60
+ expect(hunks[0]).toEqual({
61
+ oldStart: 5,
62
+ oldCount: 1,
63
+ newStart: 5,
64
+ newCount: 2,
65
+ });
66
+ });
67
+
68
+ it("should return empty array for undefined patch", () => {
69
+ expect(parseHunksFromPatch(undefined)).toEqual([]);
70
+ });
71
+ });
72
+
73
+ describe("calculateNewLineNumber", () => {
74
+ it("should return same line number when no hunks", () => {
75
+ expect(calculateNewLineNumber(5, [])).toBe(5);
76
+ });
77
+
78
+ it("should offset line number after insertion", () => {
79
+ // 在第1行后插入1行,原来的第5行变成第6行
80
+ const hunks = [{ oldStart: 1, oldCount: 1, newStart: 1, newCount: 2 }];
81
+ expect(calculateNewLineNumber(5, hunks)).toBe(6);
82
+ });
83
+
84
+ it("should offset line number after multiple insertions", () => {
85
+ // 在第1行插入2行,原来的第5行变成第7行
86
+ const hunks = [{ oldStart: 1, oldCount: 1, newStart: 1, newCount: 3 }];
87
+ expect(calculateNewLineNumber(5, hunks)).toBe(7);
88
+ });
89
+
90
+ it("should offset line number after deletion", () => {
91
+ // 删除第1-2行,原来的第5行变成第3行
92
+ const hunks = [{ oldStart: 1, oldCount: 2, newStart: 1, newCount: 0 }];
93
+ expect(calculateNewLineNumber(5, hunks)).toBe(3);
94
+ });
95
+
96
+ it("should return null when line is deleted", () => {
97
+ // 删除第5行
98
+ const hunks = [{ oldStart: 5, oldCount: 1, newStart: 5, newCount: 0 }];
99
+ expect(calculateNewLineNumber(5, hunks)).toBeNull();
100
+ });
101
+
102
+ it("should handle line before any hunk", () => {
103
+ // 第10行有变更,第5行不受影响
104
+ const hunks = [{ oldStart: 10, oldCount: 1, newStart: 10, newCount: 2 }];
105
+ expect(calculateNewLineNumber(5, hunks)).toBe(5);
106
+ });
107
+
108
+ it("should handle multiple hunks with cumulative offset", () => {
109
+ // 第1行插入1行,第10行插入1行
110
+ // 原来的第15行:经过第一个hunk偏移+1,经过第二个hunk偏移+1,变成17行
111
+ const hunks = [
112
+ { oldStart: 1, oldCount: 1, newStart: 1, newCount: 2 },
113
+ { oldStart: 10, oldCount: 1, newStart: 11, newCount: 2 },
114
+ ];
115
+ expect(calculateNewLineNumber(15, hunks)).toBe(17);
116
+ });
117
+
118
+ it("should handle line within modified hunk", () => {
119
+ // 第5-7行被修改为5-8行(3行变4行)
120
+ const hunks = [{ oldStart: 5, oldCount: 3, newStart: 5, newCount: 4 }];
121
+ expect(calculateNewLineNumber(5, hunks)).toBe(5);
122
+ expect(calculateNewLineNumber(6, hunks)).toBe(6);
123
+ expect(calculateNewLineNumber(7, hunks)).toBe(7);
124
+ // 第8行在hunk之后,偏移+1
125
+ expect(calculateNewLineNumber(8, hunks)).toBe(9);
126
+ });
127
+ });
128
+
129
+ describe("calculateLineOffsets", () => {
130
+ it("should calculate offsets for multiple lines", () => {
131
+ // 在第1行后插入2行:原1-3行变成1-5行
132
+ // 原第1行 -> 新第1行(在hunk内,位置0)
133
+ // 原第2行 -> 新第2行(在hunk内,位置1)
134
+ // 原第3行 -> 新第3行(在hunk内,位置2)
135
+ // 原第5行 -> 新第7行(在hunk后,偏移+2)
136
+ // 原第10行 -> 新第12行(在hunk后,偏移+2)
137
+ const patch = `@@ -1,3 +1,5 @@
138
+ line1
139
+ +new line 1
140
+ +new line 2
141
+ line2
142
+ line3`;
143
+ const result = calculateLineOffsets([1, 2, 3, 5, 10], patch);
144
+ expect(result.get(1)).toBe(1);
145
+ expect(result.get(2)).toBe(2); // 原第2行在hunk内,位置1
146
+ expect(result.get(3)).toBe(3); // 原第3行在hunk内,位置2
147
+ expect(result.get(5)).toBe(7); // 原第5行在hunk后,偏移+2
148
+ expect(result.get(10)).toBe(12); // 原第10行在hunk后,偏移+2
149
+ });
150
+
151
+ it("should mark deleted lines as null", () => {
152
+ const patch = `@@ -5,2 +5,0 @@
153
+ -deleted line 1
154
+ -deleted line 2`;
155
+ const result = calculateLineOffsets([4, 5, 6, 7], patch);
156
+ expect(result.get(4)).toBe(4); // 不受影响
157
+ expect(result.get(5)).toBeNull(); // 被删除
158
+ expect(result.get(6)).toBeNull(); // 被删除
159
+ expect(result.get(7)).toBe(5); // 偏移-2
160
+ });
161
+ });
162
+
163
+ describe("issue line number update scenarios", () => {
164
+ // 模拟 ReviewService.updateIssueLineNumbers 的核心逻辑
165
+ function updateIssueLine(
166
+ oldLine: string,
167
+ patch: string,
168
+ ): { newLine: string | null; deleted: boolean } {
169
+ const lineMatch = oldLine.match(/^(\d+)(?:-(\d+))?$/);
170
+ if (!lineMatch) return { newLine: oldLine, deleted: false };
171
+
172
+ const startLine = parseInt(lineMatch[1], 10);
173
+ const endLine = lineMatch[2] ? parseInt(lineMatch[2], 10) : startLine;
174
+ const hunks = parseHunksFromPatch(patch);
175
+
176
+ const newStartLine = calculateNewLineNumber(startLine, hunks);
177
+ if (newStartLine === null) {
178
+ return { newLine: null, deleted: true };
179
+ }
180
+
181
+ if (startLine === endLine) {
182
+ return { newLine: String(newStartLine), deleted: false };
183
+ }
184
+
185
+ const newEndLine = calculateNewLineNumber(endLine, hunks);
186
+ if (newEndLine === null) {
187
+ return { newLine: String(newStartLine), deleted: false };
188
+ }
189
+ return { newLine: `${newStartLine}-${newEndLine}`, deleted: false };
190
+ }
191
+
192
+ it("should update line when code is inserted before issue", () => {
193
+ // 在第1行插入2行,原第5行变成第7行
194
+ const patch = `@@ -1,3 +1,5 @@
195
+ line1
196
+ +new line 1
197
+ +new line 2
198
+ line2
199
+ line3`;
200
+ const result = updateIssueLine("5", patch);
201
+ expect(result.newLine).toBe("7");
202
+ expect(result.deleted).toBe(false);
203
+ });
204
+
205
+ it("should update line when code is deleted before issue", () => {
206
+ // 删除第1-2行,原第5行变成第3行
207
+ const patch = `@@ -1,4 +1,2 @@
208
+ -line1
209
+ -line2
210
+ line3
211
+ line4`;
212
+ const result = updateIssueLine("5", patch);
213
+ expect(result.newLine).toBe("3");
214
+ expect(result.deleted).toBe(false);
215
+ });
216
+
217
+ it("should mark as deleted when issue line is removed", () => {
218
+ // 删除第5行
219
+ const patch = `@@ -5,1 +5,0 @@
220
+ -deleted line`;
221
+ const result = updateIssueLine("5", patch);
222
+ expect(result.deleted).toBe(true);
223
+ });
224
+
225
+ it("should handle range line numbers", () => {
226
+ // 在第1行插入2行,原第5-7行变成第7-9行
227
+ const patch = `@@ -1,3 +1,5 @@
228
+ line1
229
+ +new line 1
230
+ +new line 2
231
+ line2
232
+ line3`;
233
+ const result = updateIssueLine("5-7", patch);
234
+ expect(result.newLine).toBe("7-9");
235
+ expect(result.deleted).toBe(false);
236
+ });
237
+
238
+ it("should handle range when end line is deleted", () => {
239
+ // 删除第7行,原第5-7行变成第5行
240
+ const patch = `@@ -7,1 +7,0 @@
241
+ -deleted line`;
242
+ const result = updateIssueLine("5-7", patch);
243
+ expect(result.newLine).toBe("5");
244
+ expect(result.deleted).toBe(false);
245
+ });
246
+
247
+ it("should not change line when no relevant changes", () => {
248
+ // 在第10行插入,原第5行不变
249
+ const patch = `@@ -10,1 +10,2 @@
250
+ line10
251
+ +new line`;
252
+ const result = updateIssueLine("5", patch);
253
+ expect(result.newLine).toBe("5");
254
+ expect(result.deleted).toBe(false);
255
+ });
256
+ });
257
+
258
+ describe("mapGitStatus", () => {
259
+ it("should map known statuses", () => {
260
+ expect(mapGitStatus("A")).toBe("added");
261
+ expect(mapGitStatus("M")).toBe("modified");
262
+ expect(mapGitStatus("D")).toBe("deleted");
263
+ expect(mapGitStatus("R")).toBe("renamed");
264
+ expect(mapGitStatus("C")).toBe("copied");
265
+ });
266
+
267
+ it("should default to modified for unknown status", () => {
268
+ expect(mapGitStatus("X")).toBe("modified");
269
+ expect(mapGitStatus("")).toBe("modified");
270
+ });
271
+ });
272
+
273
+ describe("parseChangedLinesFromPatch", () => {
274
+ it("should return empty set for undefined patch", () => {
275
+ expect(parseChangedLinesFromPatch(undefined)).toEqual(new Set());
276
+ });
277
+
278
+ it("should parse added lines", () => {
279
+ const patch = `@@ -1,3 +1,5 @@
280
+ line1
281
+ +added1
282
+ +added2
283
+ line2
284
+ line3`;
285
+ const result = parseChangedLinesFromPatch(patch);
286
+ expect(result).toEqual(new Set([2, 3]));
287
+ });
288
+
289
+ it("should skip deleted lines", () => {
290
+ const patch = `@@ -1,3 +1,1 @@
291
+ -deleted1
292
+ -deleted2
293
+ line3`;
294
+ const result = parseChangedLinesFromPatch(patch);
295
+ expect(result.size).toBe(0);
296
+ });
297
+
298
+ it("should handle mixed additions and deletions", () => {
299
+ const patch = `@@ -1,4 +1,4 @@
300
+ line1
301
+ -old line
302
+ +new line
303
+ line3
304
+ line4`;
305
+ const result = parseChangedLinesFromPatch(patch);
306
+ expect(result).toEqual(new Set([2]));
307
+ });
308
+ });
309
+
310
+ describe("parseDiffText", () => {
311
+ it("should parse diff text into files", () => {
312
+ const diffText = `diff --git a/file1.ts b/file1.ts
313
+ --- a/file1.ts
314
+ +++ b/file1.ts
315
+ @@ -1,3 +1,4 @@
316
+ line1
317
+ +new line
318
+ line2
319
+ line3
320
+ diff --git a/file2.ts b/file2.ts
321
+ --- a/file2.ts
322
+ +++ b/file2.ts
323
+ @@ -1,2 +1,1 @@
324
+ -old
325
+ kept`;
326
+ const files = parseDiffText(diffText);
327
+ expect(files).toHaveLength(2);
328
+ expect(files[0].filename).toBe("file1.ts");
329
+ expect(files[0].patch).toContain("@@ -1,3 +1,4 @@");
330
+ expect(files[1].filename).toBe("file2.ts");
331
+ });
332
+
333
+ it("should skip files without patch", () => {
334
+ const diffText = `diff --git a/binary.png b/binary.png
335
+ Binary files differ`;
336
+ const files = parseDiffText(diffText);
337
+ expect(files).toHaveLength(0);
338
+ });
339
+
340
+ it("should return empty array for empty input", () => {
341
+ expect(parseDiffText("")).toEqual([]);
342
+ });
343
+ });
344
+ });
@@ -0,0 +1,151 @@
1
+ import type { GitDiffFile } from "./git-sdk.types";
2
+
3
+ const GIT_STATUS_MAP: Record<string, string> = {
4
+ A: "added",
5
+ M: "modified",
6
+ D: "deleted",
7
+ R: "renamed",
8
+ C: "copied",
9
+ };
10
+
11
+ export function mapGitStatus(status: string): string {
12
+ return GIT_STATUS_MAP[status] || "modified";
13
+ }
14
+
15
+ export function parseChangedLinesFromPatch(patch?: string): Set<number> {
16
+ const changedLines = new Set<number>();
17
+ if (!patch) return changedLines;
18
+
19
+ const lines = patch.split("\n");
20
+ let currentLine = 0;
21
+
22
+ for (const line of lines) {
23
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
24
+ if (hunkMatch) {
25
+ currentLine = parseInt(hunkMatch[1], 10);
26
+ continue;
27
+ }
28
+
29
+ if (line.startsWith("+") && !line.startsWith("+++")) {
30
+ changedLines.add(currentLine);
31
+ currentLine++;
32
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
33
+ // 删除行,不增加 currentLine
34
+ } else if (!line.startsWith("\\")) {
35
+ currentLine++;
36
+ }
37
+ }
38
+
39
+ return changedLines;
40
+ }
41
+
42
+ /**
43
+ * 表示一个 diff hunk 的行号变更信息
44
+ */
45
+ export interface DiffHunk {
46
+ oldStart: number;
47
+ oldCount: number;
48
+ newStart: number;
49
+ newCount: number;
50
+ }
51
+
52
+ /**
53
+ * 从 patch 中解析所有 hunk 信息
54
+ */
55
+ export function parseHunksFromPatch(patch?: string): DiffHunk[] {
56
+ const hunks: DiffHunk[] = [];
57
+ if (!patch) return hunks;
58
+
59
+ const lines = patch.split("\n");
60
+ for (const line of lines) {
61
+ const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
62
+ if (hunkMatch) {
63
+ hunks.push({
64
+ oldStart: parseInt(hunkMatch[1], 10),
65
+ oldCount: parseInt(hunkMatch[2] ?? "1", 10),
66
+ newStart: parseInt(hunkMatch[3], 10),
67
+ newCount: parseInt(hunkMatch[4] ?? "1", 10),
68
+ });
69
+ }
70
+ }
71
+ return hunks;
72
+ }
73
+
74
+ /**
75
+ * 根据 diff hunks 计算旧行号对应的新行号
76
+ * @param oldLine 旧文件中的行号
77
+ * @param hunks diff hunk 列表
78
+ * @returns 新行号,如果该行被删除则返回 null
79
+ */
80
+ export function calculateNewLineNumber(oldLine: number, hunks: DiffHunk[]): number | null {
81
+ let offset = 0;
82
+
83
+ for (const hunk of hunks) {
84
+ const oldEnd = hunk.oldStart + hunk.oldCount - 1;
85
+
86
+ if (oldLine < hunk.oldStart) {
87
+ // 行号在这个 hunk 之前,应用之前累积的偏移
88
+ break;
89
+ }
90
+
91
+ if (oldLine >= hunk.oldStart && oldLine <= oldEnd) {
92
+ // 行号在这个 hunk 的删除范围内
93
+ // 需要检查这一行是否被删除
94
+ // 简化处理:如果 hunk 有删除(oldCount > 0),且行在范围内,认为可能被修改
95
+ // 返回对应的新位置(基于 hunk 的起始位置)
96
+ const lineOffsetInHunk = oldLine - hunk.oldStart;
97
+ if (lineOffsetInHunk < hunk.newCount) {
98
+ // 行仍然存在(可能被修改)
99
+ return hunk.newStart + lineOffsetInHunk + offset;
100
+ } else {
101
+ // 行被删除
102
+ return null;
103
+ }
104
+ }
105
+
106
+ // 计算这个 hunk 带来的偏移
107
+ offset += hunk.newCount - hunk.oldCount;
108
+ }
109
+
110
+ // 行号在所有 hunk 之后,应用总偏移
111
+ return oldLine + offset;
112
+ }
113
+
114
+ /**
115
+ * 批量计算文件中多个旧行号对应的新行号
116
+ * @param oldLines 旧行号数组
117
+ * @param patch diff patch 文本
118
+ * @returns Map<旧行号, 新行号>,被删除的行不在结果中
119
+ */
120
+ export function calculateLineOffsets(
121
+ oldLines: number[],
122
+ patch?: string,
123
+ ): Map<number, number | null> {
124
+ const result = new Map<number, number | null>();
125
+ const hunks = parseHunksFromPatch(patch);
126
+
127
+ for (const oldLine of oldLines) {
128
+ result.set(oldLine, calculateNewLineNumber(oldLine, hunks));
129
+ }
130
+
131
+ return result;
132
+ }
133
+
134
+ export function parseDiffText(diffText: string): GitDiffFile[] {
135
+ const files: GitDiffFile[] = [];
136
+ const fileDiffs = diffText.split(/^diff --git /m).filter(Boolean);
137
+
138
+ for (const fileDiff of fileDiffs) {
139
+ const headerMatch = fileDiff.match(/^a\/(.+?) b\/(.+?)[\r\n]/);
140
+ if (!headerMatch) continue;
141
+
142
+ const filename = headerMatch[2];
143
+ const patchStart = fileDiff.indexOf("@@");
144
+ if (patchStart === -1) continue;
145
+
146
+ const patch = fileDiff.slice(patchStart);
147
+ files.push({ filename, patch });
148
+ }
149
+
150
+ return files;
151
+ }
@@ -0,0 +1,8 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { GitSdkService } from "./git-sdk.service";
3
+
4
+ @Module({
5
+ providers: [GitSdkService],
6
+ exports: [GitSdkService],
7
+ })
8
+ export class GitSdkModule {}