ai-git-tools 2.0.66 → 2.0.68
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/bin/cli.js +2 -2
- package/package.json +1 -1
- package/src/commands/write-and-test.js +177 -133
package/bin/cli.js
CHANGED
|
@@ -164,8 +164,8 @@ program
|
|
|
164
164
|
.option('--file <path>', '原始碼路徑(對沒特別指定則自動從上次 dev-from-issue 讀取)')
|
|
165
165
|
.option('--issue <number>', '完成後發佈報告到此 Issue(對沒特別指定則自動從上次 dev-from-issue 讀取)')
|
|
166
166
|
.option('--test-type <type>', '測試類型:auto、unit、component 或 both(可用逗號指定多個)', 'auto')
|
|
167
|
-
.option('--
|
|
168
|
-
.option('--
|
|
167
|
+
.option('--test-dir <path>', '測試輸出目錄(預設 __tests__;指定 tests 則放在專案根目錄)', '__tests__')
|
|
168
|
+
.option('--max-fixes <number>', '最大自動修復次數(預設 3)', '3')
|
|
169
169
|
.option('--model <model>', '指定 AI 模型')
|
|
170
170
|
.action(async (options) => {
|
|
171
171
|
try {
|
package/package.json
CHANGED
|
@@ -7,7 +7,6 @@ import { resolve, basename, dirname, join } from 'path';
|
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
9
|
import { tmpdir } from 'os';
|
|
10
|
-
import inquirer from 'inquirer';
|
|
11
10
|
import { TestGenerator } from '../core/test-generator.js';
|
|
12
11
|
import { TestRunner } from '../core/test-runner.js';
|
|
13
12
|
import { AIClient } from '../core/ai-client.js';
|
|
@@ -108,69 +107,78 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
108
107
|
|
|
109
108
|
// 自動從 state 檔讀取 file / issue(指令列將覆蓋)
|
|
110
109
|
const state = loadDevState();
|
|
111
|
-
let filePath = options.file || state?.filePath;
|
|
112
110
|
const issueNumber = options.issue || state?.issueNumber;
|
|
113
|
-
const
|
|
114
|
-
const maxFixes = parseInt(options.maxFixes || '2', 10);
|
|
111
|
+
const maxFixes = parseInt(options.maxFixes || '3', 10);
|
|
115
112
|
|
|
116
|
-
//
|
|
113
|
+
// 決定要處理的檔案清單
|
|
114
|
+
let sourceFiles = [];
|
|
115
|
+
let fromState = false;
|
|
117
116
|
let fromGit = false;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
|
|
118
|
+
if (options.file) {
|
|
119
|
+
// 明確指定單一檔案
|
|
120
|
+
sourceFiles = [options.file];
|
|
121
|
+
} else if (state?.filePath) {
|
|
122
|
+
// 從上一步 dev-from-issue 讀取
|
|
123
|
+
sourceFiles = [state.filePath];
|
|
124
|
+
fromState = true;
|
|
125
|
+
} else {
|
|
126
|
+
// 從 git 偵測所有異動的原始碼檔案
|
|
127
|
+
sourceFiles = await detectChangedSourceFiles(logger);
|
|
128
|
+
fromGit = sourceFiles.length > 0;
|
|
121
129
|
}
|
|
122
130
|
|
|
123
|
-
if (
|
|
131
|
+
if (sourceFiles.length === 0) {
|
|
124
132
|
logger.error('偵測不到異動的原始碼,請用 --file 指定目標檔案。');
|
|
125
133
|
throw new Error('缺少原始碼路徑');
|
|
126
134
|
}
|
|
127
135
|
|
|
136
|
+
logger.header('write-and-test');
|
|
137
|
+
if (fromState) {
|
|
138
|
+
logger.info('從上一步 dev-from-issue 自動讀取');
|
|
139
|
+
} else if (fromGit) {
|
|
140
|
+
logger.info(`從 git 異動記錄偵測到 ${sourceFiles.length} 個檔案`);
|
|
141
|
+
}
|
|
142
|
+
console.log('');
|
|
143
|
+
|
|
144
|
+
const allResults = [];
|
|
145
|
+
for (const filePath of sourceFiles) {
|
|
146
|
+
const result = await processOneFile({ filePath, maxFixes, fromState, fromGit, options, logger });
|
|
147
|
+
allResults.push({ filePath, ...result });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 所有檔案處理完成後,才發射一則聚合報告
|
|
151
|
+
if (issueNumber) {
|
|
152
|
+
await postAggregatedReport(issueNumber, allResults, options.model, logger);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return allResults.length === 1 ? allResults[0] : allResults;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function processOneFile({ filePath, maxFixes, fromState, fromGit, options, logger }) {
|
|
159
|
+
const testDir = options.testDir || '__tests__';
|
|
128
160
|
const absoluteSourcePath = resolve(process.cwd(), filePath);
|
|
129
161
|
if (!existsSync(absoluteSourcePath)) {
|
|
130
162
|
logger.error(`原始碼不存在:${absoluteSourcePath}`);
|
|
131
163
|
throw new Error(`找不到原始碼:${absoluteSourcePath}`);
|
|
132
164
|
}
|
|
133
165
|
|
|
134
|
-
//
|
|
166
|
+
// 自動偵測時預設 both,手動指定時依 --test-type
|
|
135
167
|
const resolvedTestTypes = resolveRequestedTestTypes(absoluteSourcePath, options.testType || (fromState || fromGit ? 'both' : 'auto'));
|
|
136
168
|
const testTargets = resolvedTestTypes.map((testType) => ({
|
|
137
169
|
testType,
|
|
138
|
-
testFilePath: deriveTestFilePath(absoluteSourcePath, testType),
|
|
170
|
+
testFilePath: deriveTestFilePath(absoluteSourcePath, testType, testDir),
|
|
139
171
|
}));
|
|
140
172
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
logger.info('從上一步 dev-from-issue 自動讀取');
|
|
144
|
-
} else if (fromGit) {
|
|
145
|
-
logger.info('從 git 異動記錄自動偵測');
|
|
146
|
-
}
|
|
147
|
-
console.log(`原始碼 :${absoluteSourcePath}`);
|
|
148
|
-
console.log(`測試類型:${formatTestTypes(resolvedTestTypes)}`);
|
|
173
|
+
console.log(`── ${filePath}`);
|
|
174
|
+
console.log(` 測試類型:${formatTestTypes(resolvedTestTypes)}`);
|
|
149
175
|
testTargets.forEach(({ testType, testFilePath }) => {
|
|
150
|
-
console.log(
|
|
176
|
+
console.log(` ${testType === 'component' ? '元件測試' : '單元測試'}:${testFilePath}`);
|
|
151
177
|
});
|
|
152
|
-
console.log(`最大修復:${maxFixes} 次`);
|
|
153
|
-
if (issueNumber) {
|
|
154
|
-
console.log(`報告 Issue:#${issueNumber}`);
|
|
155
|
-
}
|
|
156
178
|
console.log('');
|
|
157
179
|
|
|
158
|
-
// 1. 詢問是否繼續(自動從 state 讀取時跳過)
|
|
159
|
-
if (!options.noConfirm && !fromState) {
|
|
160
|
-
const { confirmed } = await inquirer.prompt([{
|
|
161
|
-
type: 'confirm',
|
|
162
|
-
name: 'confirmed',
|
|
163
|
-
message: `確認為 ${filePath} 生成 ${formatTestTypes(resolvedTestTypes)} 並執行?`,
|
|
164
|
-
default: true,
|
|
165
|
-
}]);
|
|
166
|
-
|
|
167
|
-
if (!confirmed) {
|
|
168
|
-
logger.info('已取消。');
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
180
|
|
|
173
|
-
//
|
|
181
|
+
// 1. 生成測試
|
|
174
182
|
logger.step('正在生成測試代碼...');
|
|
175
183
|
const generatedTests = [];
|
|
176
184
|
for (const { testType, testFilePath } of testTargets) {
|
|
@@ -185,7 +193,7 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
185
193
|
}
|
|
186
194
|
logger.info(`實際測試類型:${formatTestTypes(generatedTests.map(({ testType }) => testType))}`);
|
|
187
195
|
|
|
188
|
-
//
|
|
196
|
+
// 2. 執行測試 + 自動修復循環
|
|
189
197
|
let lastErrors = [];
|
|
190
198
|
for (let attempt = 0; attempt <= maxFixes; attempt++) {
|
|
191
199
|
if (attempt > 0) {
|
|
@@ -199,18 +207,12 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
199
207
|
|
|
200
208
|
if (result.success) {
|
|
201
209
|
logger.success('所有測試通過! 🎉');
|
|
202
|
-
|
|
210
|
+
return {
|
|
203
211
|
success: true,
|
|
204
212
|
testFilePaths: generatedTests.map(({ testFilePath }) => testFilePath),
|
|
205
213
|
testTypes: generatedTests.map(({ testType }) => testType),
|
|
206
214
|
attempts: attempt,
|
|
207
215
|
};
|
|
208
|
-
if (options.issue) {
|
|
209
|
-
await postTestReportToIssue(options.issue, absoluteSourcePath, testResult, options.model, logger);
|
|
210
|
-
} else if (issueNumber) {
|
|
211
|
-
await postTestReportToIssue(issueNumber, absoluteSourcePath, testResult, options.model, logger);
|
|
212
|
-
}
|
|
213
|
-
return testResult;
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
lastErrors = result.errors;
|
|
@@ -242,11 +244,6 @@ export async function writeAndTestCommand(options = {}) {
|
|
|
242
244
|
testTypes: generatedTests.map(({ testType }) => testType),
|
|
243
245
|
attempts: maxFixes,
|
|
244
246
|
};
|
|
245
|
-
if (options.issue) {
|
|
246
|
-
await postTestReportToIssue(options.issue, absoluteSourcePath, failedResult, options.model, logger);
|
|
247
|
-
} else if (issueNumber) {
|
|
248
|
-
await postTestReportToIssue(issueNumber, absoluteSourcePath, failedResult, options.model, logger);
|
|
249
|
-
}
|
|
250
247
|
return failedResult;
|
|
251
248
|
}
|
|
252
249
|
}
|
|
@@ -292,59 +289,84 @@ ${sourceCode}
|
|
|
292
289
|
}
|
|
293
290
|
|
|
294
291
|
/**
|
|
295
|
-
*
|
|
292
|
+
* 將所有檔案的測試結果彙整為一則 Issue 留言,並為通過的檔案附上 mermaid 流程圖
|
|
293
|
+
* @param {Array<{filePath,success,testFilePaths,testTypes,attempts,errors?}>} allResults
|
|
296
294
|
*/
|
|
297
|
-
async function
|
|
295
|
+
async function postAggregatedReport(issueNumber, allResults, model, logger) {
|
|
296
|
+
if (!allResults || allResults.length === 0) return;
|
|
297
|
+
|
|
298
298
|
logger.step(`正在將測試報告發佈到 Issue #${issueNumber}...`);
|
|
299
299
|
|
|
300
|
+
const cwd = process.cwd();
|
|
301
|
+
const total = allResults.length;
|
|
302
|
+
const passed = allResults.filter((r) => r.success).length;
|
|
303
|
+
const failed = total - passed;
|
|
300
304
|
const typeLabels = { unit: 'Jest 單元測試', component: '元件測試', auto: '自動判斷' };
|
|
301
|
-
const testTypesStr = testResult.testTypes.map((t) => typeLabels[t] || t).join('、');
|
|
302
|
-
const statusEmoji = testResult.success ? '✅' : '❌';
|
|
303
|
-
const statusText = testResult.success
|
|
304
|
-
? `通過(${testResult.attempts > 0 ? `經過 ${testResult.attempts} 次自動修復` : '一次通過'})`
|
|
305
|
-
: `失敗(已嘗試自動修復 ${testResult.attempts} 次)`;
|
|
306
|
-
|
|
307
|
-
const relativeSourcePath = sourceFilePath.replace(process.cwd() + '/', '');
|
|
308
|
-
const relativeTestPaths = testResult.testFilePaths
|
|
309
|
-
.map((p) => p.replace(process.cwd() + '/', ''))
|
|
310
|
-
.map((p) => `- \`${p}\``)
|
|
311
|
-
.join('\n');
|
|
312
|
-
|
|
313
|
-
let errorSection = '';
|
|
314
|
-
if (!testResult.success && testResult.errors?.length) {
|
|
315
|
-
const errorSummary = testResult.errors
|
|
316
|
-
.slice(0, 3)
|
|
317
|
-
.map((e, i) => `**錯誤 ${i + 1}**\n\`\`\`\n${e.split('\n').slice(0, 15).join('\n')}\n\`\`\``)
|
|
318
|
-
.join('\n\n');
|
|
319
|
-
errorSection = `\n### 錯誤摘要\n\n${errorSummary}\n`;
|
|
320
|
-
}
|
|
321
305
|
|
|
322
|
-
//
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
306
|
+
// 摘要表格
|
|
307
|
+
const summaryEmoji = failed === 0 ? '✅' : '⚠️';
|
|
308
|
+
const header =
|
|
309
|
+
`## ${summaryEmoji} 測試報告總覽\n\n` +
|
|
310
|
+
`**異動檔案**:${total} 個|**通過**:${passed}|**失敗**:${failed}\n\n` +
|
|
311
|
+
`| 狀態 | 檔案 | 測試類型 | 結果 |\n` +
|
|
312
|
+
`| :---: | --- | --- | --- |\n`;
|
|
313
|
+
|
|
314
|
+
const tableRows = allResults.map((r) => {
|
|
315
|
+
const relPath = r.filePath.replace(cwd + '/', '');
|
|
316
|
+
const testTypesStr = (r.testTypes || []).map((t) => typeLabels[t] || t).join('、');
|
|
317
|
+
const statusEmoji = r.success ? '✅' : '❌';
|
|
318
|
+
const resultStr = r.success
|
|
319
|
+
? r.attempts > 0 ? `通過(修復 ${r.attempts} 次)` : '一次通過'
|
|
320
|
+
: `失敗(已嘗試修復 ${r.attempts} 次)`;
|
|
321
|
+
return `| ${statusEmoji} | \`${relPath}\` | ${testTypesStr} | ${resultStr} |`;
|
|
322
|
+
});
|
|
329
323
|
|
|
330
|
-
|
|
324
|
+
// 個別檔案詳情
|
|
325
|
+
const fileDetails = allResults.map((r) => {
|
|
326
|
+
const relPath = r.filePath.replace(cwd + '/', '');
|
|
327
|
+
const statusEmoji = r.success ? '✅' : '❌';
|
|
328
|
+
const relTestPaths = (r.testFilePaths || [])
|
|
329
|
+
.map((p) => `- \`${p.replace(cwd + '/', '')}\``)
|
|
330
|
+
.join('\n');
|
|
331
|
+
|
|
332
|
+
let section = `\n---\n\n### ${statusEmoji} \`${relPath}\`\n\n**測試檔案**:\n${relTestPaths}\n`;
|
|
333
|
+
|
|
334
|
+
if (!r.success && r.errors?.length) {
|
|
335
|
+
const errorSummary = r.errors
|
|
336
|
+
.slice(0, 2)
|
|
337
|
+
.map((e, i) => `**錯誤 ${i + 1}**\n\`\`\`\n${e.split('\n').slice(0, 12).join('\n')}\n\`\`\``)
|
|
338
|
+
.join('\n\n');
|
|
339
|
+
section += `\n**錯誤摘要**:\n\n${errorSummary}\n`;
|
|
340
|
+
}
|
|
331
341
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
342
|
+
return section;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// 為通過的檔案生成 mermaid 流程圖
|
|
346
|
+
const mermaidSections = [];
|
|
347
|
+
for (const r of allResults.filter((r) => r.success)) {
|
|
348
|
+
const absPath = resolve(cwd, r.filePath);
|
|
349
|
+
const diagram = await generateMermaidDiagram(absPath, model);
|
|
350
|
+
if (diagram) {
|
|
351
|
+
const relPath = r.filePath.replace(cwd + '/', '');
|
|
352
|
+
mermaidSections.push(`\n### \`${relPath}\`\n\n${diagram}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
335
355
|
|
|
336
|
-
|
|
356
|
+
const mermaidBlock =
|
|
357
|
+
mermaidSections.length > 0
|
|
358
|
+
? `\n---\n\n## 📊 邏輯流程圖\n${mermaidSections.join('\n')}\n`
|
|
359
|
+
: '';
|
|
337
360
|
|
|
338
|
-
|
|
339
|
-
${errorSection}${mermaidSection}`;
|
|
361
|
+
const body = header + tableRows.join('\n') + fileDetails.join('') + mermaidBlock;
|
|
340
362
|
|
|
341
|
-
const tmpFile = join(tmpdir(), `ai-git-tools-
|
|
363
|
+
const tmpFile = join(tmpdir(), `ai-git-tools-report-${Date.now()}.md`);
|
|
342
364
|
try {
|
|
343
365
|
writeFileSync(tmpFile, body, 'utf-8');
|
|
344
366
|
execSync(`gh issue comment ${issueNumber} --body-file "${tmpFile}"`, {
|
|
345
367
|
encoding: 'utf-8',
|
|
346
368
|
stdio: 'pipe',
|
|
347
|
-
cwd
|
|
369
|
+
cwd,
|
|
348
370
|
});
|
|
349
371
|
logger.success(`測試報告已發佈到 Issue #${issueNumber}`);
|
|
350
372
|
} catch (error) {
|
|
@@ -356,70 +378,92 @@ ${errorSection}${mermaidSection}`;
|
|
|
356
378
|
|
|
357
379
|
/**
|
|
358
380
|
* 依據原始檔案路徑推導測試檔案路徑
|
|
381
|
+
* @param {string} testDir - 測試輸出目錄:'__tests__'(預設) 或 相對專案根的路徑(如 'tests')
|
|
359
382
|
*/
|
|
360
|
-
export function deriveTestFilePath(sourceFilePath, testType = 'unit') {
|
|
361
|
-
const
|
|
383
|
+
export function deriveTestFilePath(sourceFilePath, testType = 'unit', testDir = '__tests__') {
|
|
384
|
+
const cwd = process.cwd();
|
|
362
385
|
const name = basename(sourceFilePath).replace(/\.(js|jsx|ts|tsx|mjs|cjs)$/, '');
|
|
363
386
|
const ext = /\.(ts|tsx)$/.test(sourceFilePath) ? '.ts' : '.js';
|
|
364
387
|
const suffix = testType === 'component' ? 'component' : 'unit';
|
|
365
|
-
|
|
388
|
+
|
|
389
|
+
if (testDir === '__tests__') {
|
|
390
|
+
// 預設:放在原始檔旁的 __tests__ 子目錄
|
|
391
|
+
return join(dirname(sourceFilePath), '__tests__', `${name}.${suffix}.test${ext}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 自訂目錄:放在專案根 testDir 底下,保留原始檔的相對路徑結構
|
|
395
|
+
const relDir = dirname(sourceFilePath).replace(cwd, '').replace(/^\//, '');
|
|
396
|
+
return join(cwd, testDir, relDir, `${name}.${suffix}.test${ext}`);
|
|
366
397
|
}
|
|
367
398
|
|
|
368
399
|
/**
|
|
369
|
-
* 透過 git
|
|
370
|
-
* 優先順序:
|
|
400
|
+
* 透過 git 偵測所有異動的原始碼檔案(回傳陣列)
|
|
401
|
+
* 優先順序:staged → 工作區(unstaged+untracked) → 整個 branch(vs main/master) → 最近 10 commits
|
|
371
402
|
* 排除測試檔、設定檔、lock 檔
|
|
372
403
|
*/
|
|
373
|
-
async function
|
|
404
|
+
async function detectChangedSourceFiles(logger) {
|
|
374
405
|
const SOURCE_EXTENSIONS = /\.(js|jsx|ts|tsx|mjs|cjs)$/;
|
|
375
406
|
const EXCLUDED = /(__tests__|\.test\.|\.spec\.|\.config\.|node_modules|dist\/|\.lock$|package\.json)/;
|
|
407
|
+
const cwd = process.cwd();
|
|
376
408
|
|
|
377
|
-
function filterSourceFiles(
|
|
378
|
-
return
|
|
409
|
+
function filterSourceFiles(raw) {
|
|
410
|
+
return raw
|
|
379
411
|
.split('\n')
|
|
380
|
-
.map((f) => f.trim().replace(/^[MADRCU?!\s]+/, ''))
|
|
381
|
-
.filter((f) => f && SOURCE_EXTENSIONS.test(f) && !EXCLUDED.test(f) && existsSync(resolve(
|
|
412
|
+
.map((f) => f.trim().replace(/^[MADRCU?!\s]+/, '').replace(/^"(.*)"$/, '$1'))
|
|
413
|
+
.filter((f) => f && SOURCE_EXTENSIONS.test(f) && !EXCLUDED.test(f) && existsSync(resolve(cwd, f)));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function run(cmd) {
|
|
417
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: 'pipe', cwd });
|
|
382
418
|
}
|
|
383
419
|
|
|
384
420
|
try {
|
|
385
421
|
// 1. staged 檔案
|
|
386
|
-
const staged = filterSourceFiles(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
422
|
+
const staged = filterSourceFiles(run('git diff --cached --name-only'));
|
|
423
|
+
|
|
424
|
+
// 2. unstaged + untracked
|
|
425
|
+
const unstaged = filterSourceFiles(run('git diff --name-only'));
|
|
426
|
+
const untracked = filterSourceFiles(run('git ls-files --others --exclude-standard'));
|
|
427
|
+
const working = [...new Set([...unstaged, ...untracked])];
|
|
428
|
+
|
|
429
|
+
// 3. 整個 branch 相對於 upstream / main / master 的變更
|
|
430
|
+
const branchFiles = getBranchChangedFiles(filterSourceFiles, run);
|
|
431
|
+
|
|
432
|
+
// 合併全部(去重);staged > working > branch
|
|
433
|
+
const all = [...new Set([...staged, ...working, ...branchFiles])];
|
|
434
|
+
|
|
435
|
+
if (all.length > 0) {
|
|
436
|
+
const breakdown = [
|
|
437
|
+
staged.length && `staged ${staged.length}`,
|
|
438
|
+
working.length && `工作區 ${working.length}`,
|
|
439
|
+
branchFiles.length && `branch ${branchFiles.length}`,
|
|
440
|
+
].filter(Boolean).join('、');
|
|
441
|
+
logger.info(`偵測到 ${all.length} 個異動檔案(${breakdown})`);
|
|
442
|
+
return all;
|
|
401
443
|
}
|
|
444
|
+
} catch {
|
|
445
|
+
// git 取得失敗,直接回傳空陣列
|
|
446
|
+
}
|
|
402
447
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
execSync('git ls-files --others --exclude-standard', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() })
|
|
406
|
-
);
|
|
407
|
-
if (untracked.length > 0) {
|
|
408
|
-
logger.info(`偵測到新增檔案:${untracked[0]}`);
|
|
409
|
-
return untracked[0];
|
|
410
|
-
}
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
411
450
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
451
|
+
function getBranchChangedFiles(filterSourceFiles, run) {
|
|
452
|
+
// 依序嘗試找到 merge-base,再比較 HEAD
|
|
453
|
+
const baseCandidates = ['@{u}', 'origin/main', 'origin/master', 'main', 'master'];
|
|
454
|
+
for (const base of baseCandidates) {
|
|
455
|
+
try {
|
|
456
|
+
const mergeBase = run(`git merge-base HEAD ${base}`).trim();
|
|
457
|
+
const files = filterSourceFiles(run(`git diff --name-only ${mergeBase}...HEAD`));
|
|
458
|
+
if (files.length > 0) return files;
|
|
459
|
+
} catch {
|
|
460
|
+
continue;
|
|
419
461
|
}
|
|
462
|
+
}
|
|
463
|
+
// 最後 fallback:最近 10 commits
|
|
464
|
+
try {
|
|
465
|
+
return filterSourceFiles(run('git diff --name-only HEAD~10...HEAD'));
|
|
420
466
|
} catch {
|
|
421
|
-
|
|
467
|
+
return [];
|
|
422
468
|
}
|
|
423
|
-
|
|
424
|
-
return null;
|
|
425
469
|
}
|