@itcase/config 1.6.77 → 1.6.78
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/package.json +1 -1
- package/smart-commit/cli.js +139 -32
package/package.json
CHANGED
package/smart-commit/cli.js
CHANGED
|
@@ -71,16 +71,16 @@ async function main(argv) {
|
|
|
71
71
|
const cwd = process.cwd()
|
|
72
72
|
assertItcaseConfigPackage(cwd)
|
|
73
73
|
const { scopes, types } = await loadCommitlintConfig(cwd)
|
|
74
|
-
const
|
|
75
|
-
if (!
|
|
74
|
+
const changedFiles = listFiles(cwd, option)
|
|
75
|
+
if (!changedFiles.length) {
|
|
76
76
|
process.stdout.write('No changes to commit.\n')
|
|
77
77
|
return
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const prompt = buildPrompt(scopes, types,
|
|
80
|
+
const prompt = buildPrompt(scopes, types, changedFiles, cwd, option.type)
|
|
81
81
|
const raw = await withAgentProgress(
|
|
82
82
|
{
|
|
83
|
-
fileCount:
|
|
83
|
+
fileCount: changedFiles.length,
|
|
84
84
|
sprite: pickSprite(option.sprite),
|
|
85
85
|
verbose: option.verbose,
|
|
86
86
|
},
|
|
@@ -93,7 +93,8 @@ async function main(argv) {
|
|
|
93
93
|
progress.onAgentActivity,
|
|
94
94
|
),
|
|
95
95
|
)
|
|
96
|
-
|
|
96
|
+
let plans = parsePlan(raw, changedFiles)
|
|
97
|
+
plans = assignOmittedFiles(plans, changedFiles)
|
|
97
98
|
await validateAndNormalizePlans(cwd, plans)
|
|
98
99
|
|
|
99
100
|
process.stdout.write('\nCommits:\n')
|
|
@@ -131,21 +132,21 @@ async function main(argv) {
|
|
|
131
132
|
|
|
132
133
|
let created = 0
|
|
133
134
|
for (const plan of activePlans) {
|
|
134
|
-
const
|
|
135
|
-
if (!
|
|
135
|
+
const planFiles = plan.files.filter((file) => fileHasChanges(cwd, file))
|
|
136
|
+
if (!planFiles.length) {
|
|
136
137
|
process.stdout.write(`Skipping empty commit: ${plan.message}\n`)
|
|
137
138
|
continue
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
// Drop leftover index entries so only this plan's files are committed.
|
|
141
142
|
git(cwd, ['reset', '-q', 'HEAD'])
|
|
142
|
-
|
|
143
|
+
stageFiles(cwd, planFiles)
|
|
143
144
|
const staged = lines(cwd, ['diff', '--cached', '--name-only'])
|
|
144
|
-
const stagedForPlan =
|
|
145
|
+
const stagedForPlan = planFiles.filter((file) => staged.includes(file))
|
|
145
146
|
if (!stagedForPlan.length) {
|
|
146
147
|
throw new Error(
|
|
147
148
|
`Nothing staged for commit: ${plan.message}\n` +
|
|
148
|
-
`Files: ${
|
|
149
|
+
`Files: ${planFiles.join(', ')}`,
|
|
149
150
|
)
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -159,6 +160,14 @@ async function main(argv) {
|
|
|
159
160
|
created++
|
|
160
161
|
}
|
|
161
162
|
process.stdout.write(`Created ${created} commit(s).\n`)
|
|
163
|
+
|
|
164
|
+
const remaining = changedFiles.filter((file) => fileHasChanges(cwd, file))
|
|
165
|
+
if (remaining.length) {
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
`\nUncommitted changes (${remaining.length}):\n` +
|
|
168
|
+
`${remaining.map((file) => ` ${file}`).join('\n')}\n`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
162
171
|
}
|
|
163
172
|
|
|
164
173
|
function parseArgs(argv) {
|
|
@@ -306,15 +315,17 @@ function readReleaseBranches(cwd) {
|
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
function listFiles(cwd, option) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
318
|
+
if (option.staged) {
|
|
319
|
+
return lines(cwd, [
|
|
320
|
+
'diff',
|
|
321
|
+
'--cached',
|
|
322
|
+
'--name-only',
|
|
323
|
+
'--diff-filter=ACDMR',
|
|
324
|
+
])
|
|
325
|
+
}
|
|
316
326
|
return [
|
|
317
327
|
...new Set([
|
|
328
|
+
...lines(cwd, ['diff', '--cached', '--name-only', '--diff-filter=ACDMR']),
|
|
318
329
|
...lines(cwd, ['diff', '--name-only', '--diff-filter=ACDMR']),
|
|
319
330
|
...lines(cwd, ['ls-files', '--others', '--exclude-standard']),
|
|
320
331
|
]),
|
|
@@ -322,10 +333,14 @@ function listFiles(cwd, option) {
|
|
|
322
333
|
}
|
|
323
334
|
|
|
324
335
|
function buildPrompt(scopes, types, files, cwd, type) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
336
|
+
const statusByFile = new Map(
|
|
337
|
+
lines(cwd, ['status', '--short', '--porcelain', '--', ...files]).map(
|
|
338
|
+
(line) => [line.slice(3), line.slice(0, 2).trim()],
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
let diff = files
|
|
343
|
+
.map((file) => buildFileDiff(cwd, file))
|
|
329
344
|
.filter(Boolean)
|
|
330
345
|
.join('\n\n')
|
|
331
346
|
if (diff.length > MAX_DIFF) diff = `${diff.slice(0, MAX_DIFF)}\n…truncated`
|
|
@@ -337,11 +352,17 @@ Scopes (exact): ${buildScopeEnum(scopes).join(', ')}
|
|
|
337
352
|
Subject rules:
|
|
338
353
|
${promptSubjectRules.map((rule) => `- ${rule}`).join('\n')}
|
|
339
354
|
Prefer one commit when changes belong to the same feature.
|
|
340
|
-
Split by scope. Each listed file in exactly one commit.
|
|
355
|
+
Split by scope. Each listed file in exactly one commit — including new/untracked files (??).
|
|
341
356
|
Do not add Co-authored-by or Made-with trailers.
|
|
342
357
|
${type ? `Prefer type "${type}".\n` : ''}SCOPES: ${JSON.stringify(scopes)}
|
|
343
358
|
Files:
|
|
344
|
-
${files
|
|
359
|
+
${files
|
|
360
|
+
.map((file) => {
|
|
361
|
+
const status =
|
|
362
|
+
statusByFile.get(file) || (isUntracked(cwd, file) ? '??' : ' M')
|
|
363
|
+
return `- ${file} (${status})`
|
|
364
|
+
})
|
|
365
|
+
.join('\n')}
|
|
345
366
|
status:
|
|
346
367
|
${out(cwd, ['status', '--short', '--', ...files]) || '(empty)'}
|
|
347
368
|
diff:
|
|
@@ -349,6 +370,31 @@ ${diff || '(none)'}
|
|
|
349
370
|
`
|
|
350
371
|
}
|
|
351
372
|
|
|
373
|
+
function buildFileDiff(cwd, file) {
|
|
374
|
+
const filePath = path.join(cwd, file)
|
|
375
|
+
if (!fs.existsSync(filePath)) {
|
|
376
|
+
return (
|
|
377
|
+
out(cwd, ['diff', '--cached', '--', file]) ||
|
|
378
|
+
out(cwd, ['diff', '--', file])
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (isUntracked(cwd, file)) {
|
|
383
|
+
const nullPath = process.platform === 'win32' ? 'NUL' : '/dev/null'
|
|
384
|
+
const untrackedDiff = out(cwd, ['diff', '--no-index', nullPath, file])
|
|
385
|
+
return untrackedDiff
|
|
386
|
+
? `new file ${file}:\n${untrackedDiff}`
|
|
387
|
+
: `new file ${file}: (empty)`
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return [
|
|
391
|
+
out(cwd, ['diff', '--cached', '--', file]),
|
|
392
|
+
out(cwd, ['diff', '--', file]),
|
|
393
|
+
]
|
|
394
|
+
.filter(Boolean)
|
|
395
|
+
.join('\n')
|
|
396
|
+
}
|
|
397
|
+
|
|
352
398
|
function buildAgentProgressSteps(fileCount) {
|
|
353
399
|
const fileLabel =
|
|
354
400
|
fileCount === 1 ? '1 changed file' : `${fileCount} changed files`
|
|
@@ -555,12 +601,60 @@ function parsePlan(raw, allowedFiles) {
|
|
|
555
601
|
}
|
|
556
602
|
|
|
557
603
|
if (!plans.length) throw new Error(`Empty commit plan:\n${raw}`)
|
|
558
|
-
const missing = allowedFiles.filter((file) => !seen.has(file))
|
|
559
|
-
if (missing.length)
|
|
560
|
-
throw new Error(`Cursor omitted files:\n${missing.join('\n')}`)
|
|
561
604
|
return plans
|
|
562
605
|
}
|
|
563
606
|
|
|
607
|
+
function assignOmittedFiles(plans, allowedFiles) {
|
|
608
|
+
const assigned = new Set(plans.flatMap((plan) => plan.files))
|
|
609
|
+
const missing = allowedFiles.filter((file) => !assigned.has(file))
|
|
610
|
+
if (!missing.length) return plans
|
|
611
|
+
|
|
612
|
+
for (const file of missing) {
|
|
613
|
+
const target = findBestCommitForFile(plans, file)
|
|
614
|
+
target.files.push(file)
|
|
615
|
+
process.stderr.write(
|
|
616
|
+
`Auto-assigned omitted file to commit: ${file} → ${target.message}\n`,
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
return plans
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function findBestCommitForFile(plans, file) {
|
|
623
|
+
const fileDir = path.dirname(file)
|
|
624
|
+
|
|
625
|
+
const sameDir = plans.find((plan) =>
|
|
626
|
+
plan.files.some((candidate) => path.dirname(candidate) === fileDir),
|
|
627
|
+
)
|
|
628
|
+
if (sameDir) return sameDir
|
|
629
|
+
|
|
630
|
+
let best = plans[0]
|
|
631
|
+
let bestScore = -1
|
|
632
|
+
for (const plan of plans) {
|
|
633
|
+
for (const candidate of plan.files) {
|
|
634
|
+
const score = commonPathPrefixLength(candidate, file)
|
|
635
|
+
if (score > bestScore) {
|
|
636
|
+
bestScore = score
|
|
637
|
+
best = plan
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return best
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function commonPathPrefixLength(left, right) {
|
|
645
|
+
const leftParts = left.split('/')
|
|
646
|
+
const rightParts = right.split('/')
|
|
647
|
+
let index = 0
|
|
648
|
+
while (
|
|
649
|
+
index < leftParts.length &&
|
|
650
|
+
index < rightParts.length &&
|
|
651
|
+
leftParts[index] === rightParts[index]
|
|
652
|
+
) {
|
|
653
|
+
index++
|
|
654
|
+
}
|
|
655
|
+
return index
|
|
656
|
+
}
|
|
657
|
+
|
|
564
658
|
function normalizeCommitMessage(message) {
|
|
565
659
|
const lines = String(message || '')
|
|
566
660
|
.split('\n')
|
|
@@ -595,17 +689,25 @@ async function validateAndNormalizePlans(cwd, plans) {
|
|
|
595
689
|
}
|
|
596
690
|
}
|
|
597
691
|
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
if (!fs.existsSync(filePath)) return false
|
|
601
|
-
|
|
602
|
-
const tracked =
|
|
692
|
+
function isTracked(cwd, file) {
|
|
693
|
+
return (
|
|
603
694
|
spawnSync('git', ['ls-files', '--error-unmatch', '--', file], {
|
|
604
695
|
cwd: cwd,
|
|
605
696
|
encoding: 'utf8',
|
|
606
697
|
}).status === 0
|
|
698
|
+
)
|
|
699
|
+
}
|
|
607
700
|
|
|
608
|
-
|
|
701
|
+
function isUntracked(cwd, file) {
|
|
702
|
+
return !isTracked(cwd, file) && fs.existsSync(path.join(cwd, file))
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function fileHasChanges(cwd, file) {
|
|
706
|
+
const filePath = path.join(cwd, file)
|
|
707
|
+
const tracked = isTracked(cwd, file)
|
|
708
|
+
|
|
709
|
+
if (!tracked) return fs.existsSync(filePath)
|
|
710
|
+
if (!fs.existsSync(filePath)) return true
|
|
609
711
|
|
|
610
712
|
return (
|
|
611
713
|
spawnSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
|
|
@@ -615,6 +717,11 @@ function fileHasChanges(cwd, file) {
|
|
|
615
717
|
)
|
|
616
718
|
}
|
|
617
719
|
|
|
720
|
+
function stageFiles(cwd, files) {
|
|
721
|
+
if (!files.length) return
|
|
722
|
+
git(cwd, ['add', '-A', '--', ...files])
|
|
723
|
+
}
|
|
724
|
+
|
|
618
725
|
async function confirm(count) {
|
|
619
726
|
if (!input.isTTY) throw new Error('No TTY — use --yes or --dry-run')
|
|
620
727
|
const rl = readline.createInterface({ input, output })
|