@itcase/config 1.6.77 → 1.6.79
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.md +1 -1
- package/lint-staged/index.js +5 -2
- package/lint-staged-ts/index.js +5 -2
- package/package.json +1 -1
- package/smart-commit/cli.js +308 -53
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ npm install --save-dev commitizen @commitlint/cz-commitlint
|
|
|
87
87
|
|
|
88
88
|
## Smart commit
|
|
89
89
|
|
|
90
|
-
`smart-commit` — Cursor Agent (`ask`) планирует коммиты по `SCOPES`, печатает сообщения, спрашивает `[y/N]`. Перед коммитом ждёт и снимает устаревший `.git/index.lock`, если его никто не держит.
|
|
90
|
+
`smart-commit` — Cursor Agent (`ask`) планирует коммиты по `SCOPES`, печатает сообщения, спрашивает `[y/N]`. Перед коммитом ждёт и снимает устаревший `.git/index.lock`, если его никто не держит. Lockfile (`package-lock.json` и аналоги) всегда кладётся в тот же коммит, что и соседний `package.json`; при одном только bump deps агент не вызывается.
|
|
91
91
|
|
|
92
92
|
```text
|
|
93
93
|
[yellow pixel banana animation]
|
package/lint-staged/index.js
CHANGED
|
@@ -9,8 +9,11 @@ export default {
|
|
|
9
9
|
'package.json': (files) => {
|
|
10
10
|
// Проверяем, есть ли package.json в корне репозитория
|
|
11
11
|
const rootPackageJson = resolve(process.cwd(), 'package.json')
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const isRoot = files.some((file) => resolve(file) === rootPackageJson)
|
|
13
|
+
if (existsSync(rootPackageJson) && isRoot) {
|
|
14
|
+
// lock пишется командой, но glob только package.json — без git add
|
|
15
|
+
// он останется вне коммита (типичный leftover после bump deps)
|
|
16
|
+
return ['npm install --package-lock-only', 'git add -- package-lock.json']
|
|
14
17
|
}
|
|
15
18
|
return 'true' // Пропускаем, если не найден
|
|
16
19
|
},
|
package/lint-staged-ts/index.js
CHANGED
|
@@ -17,8 +17,11 @@ export default {
|
|
|
17
17
|
'package.json': (files) => {
|
|
18
18
|
// Проверяем, есть ли package.json в корне репозитория
|
|
19
19
|
const rootPackageJson = resolve(process.cwd(), 'package.json')
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const isRoot = files.some((file) => resolve(file) === rootPackageJson)
|
|
21
|
+
if (existsSync(rootPackageJson) && isRoot) {
|
|
22
|
+
// lock пишется командой, но glob только package.json — без git add
|
|
23
|
+
// он останется вне коммита (типичный leftover после bump deps)
|
|
24
|
+
return ['npm install --package-lock-only', 'git add -- package-lock.json']
|
|
22
25
|
}
|
|
23
26
|
return 'true' // Пропускаем, если не найден
|
|
24
27
|
},
|
package/package.json
CHANGED
package/smart-commit/cli.js
CHANGED
|
@@ -22,6 +22,14 @@ const PACKAGE_NAME = '@itcase/config'
|
|
|
22
22
|
const CLI_RELATIVE = 'smart-commit/cli.js'
|
|
23
23
|
|
|
24
24
|
const MAX_DIFF = 80_000
|
|
25
|
+
const LOCKFILE_NAMES = new Set([
|
|
26
|
+
'package-lock.json',
|
|
27
|
+
'npm-shrinkwrap.json',
|
|
28
|
+
'yarn.lock',
|
|
29
|
+
'pnpm-lock.yaml',
|
|
30
|
+
'bun.lock',
|
|
31
|
+
'bun.lockb',
|
|
32
|
+
])
|
|
25
33
|
const AGENT_TIMEOUT_MS = 120_000
|
|
26
34
|
const AGENT_STEP_MS = 2_500
|
|
27
35
|
const NYAN_FRAME_MS = 90
|
|
@@ -71,29 +79,34 @@ async function main(argv) {
|
|
|
71
79
|
const cwd = process.cwd()
|
|
72
80
|
assertItcaseConfigPackage(cwd)
|
|
73
81
|
const { scopes, types } = await loadCommitlintConfig(cwd)
|
|
74
|
-
const
|
|
75
|
-
if (!
|
|
82
|
+
const changedFiles = listFiles(cwd, option)
|
|
83
|
+
if (!changedFiles.length) {
|
|
76
84
|
process.stdout.write('No changes to commit.\n')
|
|
77
85
|
return
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
let plans = tryFastPlan(changedFiles, cwd, scopes, types)
|
|
89
|
+
if (!plans) {
|
|
90
|
+
const prompt = buildPrompt(scopes, types, changedFiles, cwd, option.type)
|
|
91
|
+
const raw = await withAgentProgress(
|
|
92
|
+
{
|
|
93
|
+
fileCount: changedFiles.length,
|
|
94
|
+
sprite: pickSprite(option.sprite),
|
|
95
|
+
verbose: option.verbose,
|
|
96
|
+
},
|
|
97
|
+
(progress) =>
|
|
98
|
+
askAgent(
|
|
99
|
+
cwd,
|
|
100
|
+
option.model || process.env.SMART_COMMIT_MODEL || '',
|
|
101
|
+
prompt,
|
|
102
|
+
option.verbose,
|
|
103
|
+
progress.onAgentActivity,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
plans = parsePlan(raw, changedFiles)
|
|
107
|
+
plans = assignOmittedFiles(plans, changedFiles)
|
|
108
|
+
plans = attachLockfiles(plans, changedFiles)
|
|
109
|
+
}
|
|
97
110
|
await validateAndNormalizePlans(cwd, plans)
|
|
98
111
|
|
|
99
112
|
process.stdout.write('\nCommits:\n')
|
|
@@ -131,21 +144,21 @@ async function main(argv) {
|
|
|
131
144
|
|
|
132
145
|
let created = 0
|
|
133
146
|
for (const plan of activePlans) {
|
|
134
|
-
const
|
|
135
|
-
if (!
|
|
147
|
+
const planFiles = plan.files.filter((file) => fileHasChanges(cwd, file))
|
|
148
|
+
if (!planFiles.length) {
|
|
136
149
|
process.stdout.write(`Skipping empty commit: ${plan.message}\n`)
|
|
137
150
|
continue
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
// Drop leftover index entries so only this plan's files are committed.
|
|
141
154
|
git(cwd, ['reset', '-q', 'HEAD'])
|
|
142
|
-
|
|
155
|
+
stageFiles(cwd, planFiles)
|
|
143
156
|
const staged = lines(cwd, ['diff', '--cached', '--name-only'])
|
|
144
|
-
const stagedForPlan =
|
|
157
|
+
const stagedForPlan = planFiles.filter((file) => staged.includes(file))
|
|
145
158
|
if (!stagedForPlan.length) {
|
|
146
159
|
throw new Error(
|
|
147
160
|
`Nothing staged for commit: ${plan.message}\n` +
|
|
148
|
-
`Files: ${
|
|
161
|
+
`Files: ${planFiles.join(', ')}`,
|
|
149
162
|
)
|
|
150
163
|
}
|
|
151
164
|
|
|
@@ -159,6 +172,14 @@ async function main(argv) {
|
|
|
159
172
|
created++
|
|
160
173
|
}
|
|
161
174
|
process.stdout.write(`Created ${created} commit(s).\n`)
|
|
175
|
+
|
|
176
|
+
const remaining = changedFiles.filter((file) => fileHasChanges(cwd, file))
|
|
177
|
+
if (remaining.length) {
|
|
178
|
+
process.stdout.write(
|
|
179
|
+
`\nUncommitted changes (${remaining.length}):\n` +
|
|
180
|
+
`${remaining.map((file) => ` ${file}`).join('\n')}\n`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
162
183
|
}
|
|
163
184
|
|
|
164
185
|
function parseArgs(argv) {
|
|
@@ -306,28 +327,42 @@ function readReleaseBranches(cwd) {
|
|
|
306
327
|
}
|
|
307
328
|
|
|
308
329
|
function listFiles(cwd, option) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
330
|
+
if (option.staged) {
|
|
331
|
+
return lines(cwd, [
|
|
332
|
+
'diff',
|
|
333
|
+
'--cached',
|
|
334
|
+
'--name-only',
|
|
335
|
+
'--diff-filter=ACDMR',
|
|
336
|
+
])
|
|
337
|
+
}
|
|
316
338
|
return [
|
|
317
339
|
...new Set([
|
|
340
|
+
...lines(cwd, ['diff', '--cached', '--name-only', '--diff-filter=ACDMR']),
|
|
318
341
|
...lines(cwd, ['diff', '--name-only', '--diff-filter=ACDMR']),
|
|
319
342
|
...lines(cwd, ['ls-files', '--others', '--exclude-standard']),
|
|
320
343
|
]),
|
|
321
344
|
]
|
|
322
345
|
}
|
|
323
346
|
|
|
347
|
+
function isLockfile(file) {
|
|
348
|
+
return LOCKFILE_NAMES.has(path.basename(file))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function siblingManifest(file) {
|
|
352
|
+
const directory = path.posix.dirname(file)
|
|
353
|
+
return directory === '.' ? 'package.json' : `${directory}/package.json`
|
|
354
|
+
}
|
|
355
|
+
|
|
324
356
|
function buildPrompt(scopes, types, files, cwd, type) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
357
|
+
const statusByFile = new Map(
|
|
358
|
+
lines(cwd, ['status', '--short', '--porcelain', '--', ...files]).map(
|
|
359
|
+
(line) => [line.slice(3), line.slice(0, 2).trim()],
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
const sourceFiles = files.filter((file) => !isLockfile(file))
|
|
364
|
+
const lockFiles = files.filter((file) => isLockfile(file))
|
|
365
|
+
let diff = buildSourceDiff(cwd, sourceFiles)
|
|
331
366
|
if (diff.length > MAX_DIFF) diff = `${diff.slice(0, MAX_DIFF)}\n…truncated`
|
|
332
367
|
|
|
333
368
|
return `Plan git commits. Do not edit files or run git commit.
|
|
@@ -337,18 +372,60 @@ Scopes (exact): ${buildScopeEnum(scopes).join(', ')}
|
|
|
337
372
|
Subject rules:
|
|
338
373
|
${promptSubjectRules.map((rule) => `- ${rule}`).join('\n')}
|
|
339
374
|
Prefer one commit when changes belong to the same feature.
|
|
340
|
-
Split by scope. Each listed file in exactly one commit.
|
|
375
|
+
Split by scope. Each listed file in exactly one commit — including new/untracked files (??).
|
|
376
|
+
Put each lockfile in the same commit as its sibling package.json. Do not make a lockfile-only commit.
|
|
341
377
|
Do not add Co-authored-by or Made-with trailers.
|
|
342
|
-
${type ? `Prefer type "${type}".\n` : ''}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
status
|
|
346
|
-
|
|
347
|
-
|
|
378
|
+
${type ? `Prefer type "${type}".\n` : ''}Files:
|
|
379
|
+
${files
|
|
380
|
+
.map((file) => {
|
|
381
|
+
const status =
|
|
382
|
+
statusByFile.get(file) || (isUntracked(cwd, file) ? '??' : ' M')
|
|
383
|
+
const lockNote = isLockfile(file) ? ', lockfile' : ''
|
|
384
|
+
return `- ${file} (${status}${lockNote})`
|
|
385
|
+
})
|
|
386
|
+
.join('\n')}
|
|
387
|
+
${
|
|
388
|
+
lockFiles.length
|
|
389
|
+
? `Lockfiles (no diff, keep with package.json):\n${lockFiles
|
|
390
|
+
.map((file) => `- ${file}`)
|
|
391
|
+
.join('\n')}\n`
|
|
392
|
+
: ''
|
|
393
|
+
}diff:
|
|
348
394
|
${diff || '(none)'}
|
|
349
395
|
`
|
|
350
396
|
}
|
|
351
397
|
|
|
398
|
+
function buildSourceDiff(cwd, files) {
|
|
399
|
+
const tracked = []
|
|
400
|
+
const untracked = []
|
|
401
|
+
for (const file of files) {
|
|
402
|
+
if (isUntracked(cwd, file)) untracked.push(file)
|
|
403
|
+
else tracked.push(file)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const parts = []
|
|
407
|
+
if (tracked.length) {
|
|
408
|
+
const text = out(cwd, ['diff', 'HEAD', '--', ...tracked])
|
|
409
|
+
if (text) parts.push(text)
|
|
410
|
+
}
|
|
411
|
+
for (const file of untracked) {
|
|
412
|
+
const piece = buildUntrackedDiff(cwd, file)
|
|
413
|
+
if (piece) parts.push(piece)
|
|
414
|
+
}
|
|
415
|
+
return parts.join('\n\n')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function buildUntrackedDiff(cwd, file) {
|
|
419
|
+
const filePath = path.join(cwd, file)
|
|
420
|
+
if (!fs.existsSync(filePath)) return ''
|
|
421
|
+
|
|
422
|
+
const nullPath = process.platform === 'win32' ? 'NUL' : '/dev/null'
|
|
423
|
+
const untrackedDiff = out(cwd, ['diff', '--no-index', nullPath, file])
|
|
424
|
+
return untrackedDiff
|
|
425
|
+
? `new file ${file}:\n${untrackedDiff}`
|
|
426
|
+
: `new file ${file}: (empty)`
|
|
427
|
+
}
|
|
428
|
+
|
|
352
429
|
function buildAgentProgressSteps(fileCount) {
|
|
353
430
|
const fileLabel =
|
|
354
431
|
fileCount === 1 ? '1 changed file' : `${fileCount} changed files`
|
|
@@ -555,12 +632,175 @@ function parsePlan(raw, allowedFiles) {
|
|
|
555
632
|
}
|
|
556
633
|
|
|
557
634
|
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
635
|
return plans
|
|
562
636
|
}
|
|
563
637
|
|
|
638
|
+
function tryFastPlan(files, cwd, scopes, types) {
|
|
639
|
+
const sourceFiles = files.filter((file) => !isLockfile(file))
|
|
640
|
+
if (sourceFiles.length !== 1) return null
|
|
641
|
+
if (path.basename(sourceFiles[0]) !== 'package.json') return null
|
|
642
|
+
|
|
643
|
+
const message = inferDepsMessage(cwd, sourceFiles[0], scopes, types)
|
|
644
|
+
if (!message) return null
|
|
645
|
+
|
|
646
|
+
return [
|
|
647
|
+
{
|
|
648
|
+
message: message,
|
|
649
|
+
files: [...sourceFiles, ...files.filter(isLockfile)],
|
|
650
|
+
},
|
|
651
|
+
]
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function inferDepsMessage(cwd, file, scopes, types) {
|
|
655
|
+
const type = types.includes('deps')
|
|
656
|
+
? 'deps'
|
|
657
|
+
: types.includes('chore')
|
|
658
|
+
? 'chore'
|
|
659
|
+
: ''
|
|
660
|
+
if (!type) return ''
|
|
661
|
+
|
|
662
|
+
const names = changedDependencyNames(cwd, file)
|
|
663
|
+
if (!names?.length) return ''
|
|
664
|
+
|
|
665
|
+
const scope = pickScope(scopes, ['packages', 'deps', 'other'])
|
|
666
|
+
const subject =
|
|
667
|
+
names.length === 1
|
|
668
|
+
? `bump ${names[0]}`
|
|
669
|
+
: names.length <= 3
|
|
670
|
+
? `bump ${names.join(', ')}`
|
|
671
|
+
: 'bump dependencies'
|
|
672
|
+
return `${type}(${scope}): ${subject}`
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function changedDependencyNames(cwd, file) {
|
|
676
|
+
const current = readJsonFile(path.join(cwd, file))
|
|
677
|
+
if (!current) return null
|
|
678
|
+
|
|
679
|
+
let previous = {}
|
|
680
|
+
const shown = spawnSync('git', ['show', `HEAD:${file}`], {
|
|
681
|
+
cwd: cwd,
|
|
682
|
+
encoding: 'utf8',
|
|
683
|
+
})
|
|
684
|
+
if (!shown.status && shown.stdout) {
|
|
685
|
+
try {
|
|
686
|
+
previous = JSON.parse(shown.stdout)
|
|
687
|
+
} catch {
|
|
688
|
+
return null
|
|
689
|
+
}
|
|
690
|
+
} else if (isUntracked(cwd, file)) {
|
|
691
|
+
return null
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const sections = [
|
|
695
|
+
'dependencies',
|
|
696
|
+
'devDependencies',
|
|
697
|
+
'optionalDependencies',
|
|
698
|
+
'peerDependencies',
|
|
699
|
+
]
|
|
700
|
+
const names = []
|
|
701
|
+
for (const key of sections) {
|
|
702
|
+
const before = previous[key] || {}
|
|
703
|
+
const after = current[key] || {}
|
|
704
|
+
for (const name of new Set([
|
|
705
|
+
...Object.keys(before),
|
|
706
|
+
...Object.keys(after),
|
|
707
|
+
])) {
|
|
708
|
+
if (before[name] !== after[name]) names.push(name)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const keys = new Set([...Object.keys(previous), ...Object.keys(current)])
|
|
713
|
+
for (const key of keys) {
|
|
714
|
+
if (sections.includes(key)) continue
|
|
715
|
+
if (JSON.stringify(previous[key]) !== JSON.stringify(current[key])) {
|
|
716
|
+
return null
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return names.length ? names : null
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function readJsonFile(filePath) {
|
|
724
|
+
try {
|
|
725
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
726
|
+
} catch {
|
|
727
|
+
return null
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function pickScope(scopes, preferred) {
|
|
732
|
+
const list = buildScopeEnum(scopes)
|
|
733
|
+
return preferred.find((scope) => list.includes(scope)) || list[0] || 'other'
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function assignOmittedFiles(plans, allowedFiles) {
|
|
737
|
+
const assigned = new Set(plans.flatMap((plan) => plan.files))
|
|
738
|
+
const missing = allowedFiles.filter((file) => !assigned.has(file))
|
|
739
|
+
if (!missing.length) return plans
|
|
740
|
+
|
|
741
|
+
for (const file of missing) {
|
|
742
|
+
const target = findBestCommitForFile(plans, file)
|
|
743
|
+
target.files.push(file)
|
|
744
|
+
process.stderr.write(
|
|
745
|
+
`Auto-assigned omitted file to commit: ${file} → ${target.message}\n`,
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
return plans
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function attachLockfiles(plans, allowedFiles) {
|
|
752
|
+
const locks = allowedFiles.filter((file) => isLockfile(file))
|
|
753
|
+
if (!locks.length) return plans
|
|
754
|
+
|
|
755
|
+
for (const lock of locks) {
|
|
756
|
+
for (const plan of plans) {
|
|
757
|
+
plan.files = plan.files.filter((file) => file !== lock)
|
|
758
|
+
}
|
|
759
|
+
const manifest = siblingManifest(lock)
|
|
760
|
+
const withManifest = plans.find((plan) => plan.files.includes(manifest))
|
|
761
|
+
const target = withManifest || findBestCommitForFile(plans, lock)
|
|
762
|
+
target.files.push(lock)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return plans.filter((plan) => plan.files.length)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function findBestCommitForFile(plans, file) {
|
|
769
|
+
const fileDir = path.dirname(file)
|
|
770
|
+
|
|
771
|
+
const sameDir = plans.find((plan) =>
|
|
772
|
+
plan.files.some((candidate) => path.dirname(candidate) === fileDir),
|
|
773
|
+
)
|
|
774
|
+
if (sameDir) return sameDir
|
|
775
|
+
|
|
776
|
+
let best = plans[0]
|
|
777
|
+
let bestScore = -1
|
|
778
|
+
for (const plan of plans) {
|
|
779
|
+
for (const candidate of plan.files) {
|
|
780
|
+
const score = commonPathPrefixLength(candidate, file)
|
|
781
|
+
if (score > bestScore) {
|
|
782
|
+
bestScore = score
|
|
783
|
+
best = plan
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return best
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function commonPathPrefixLength(left, right) {
|
|
791
|
+
const leftParts = left.split('/')
|
|
792
|
+
const rightParts = right.split('/')
|
|
793
|
+
let index = 0
|
|
794
|
+
while (
|
|
795
|
+
index < leftParts.length &&
|
|
796
|
+
index < rightParts.length &&
|
|
797
|
+
leftParts[index] === rightParts[index]
|
|
798
|
+
) {
|
|
799
|
+
index++
|
|
800
|
+
}
|
|
801
|
+
return index
|
|
802
|
+
}
|
|
803
|
+
|
|
564
804
|
function normalizeCommitMessage(message) {
|
|
565
805
|
const lines = String(message || '')
|
|
566
806
|
.split('\n')
|
|
@@ -595,17 +835,25 @@ async function validateAndNormalizePlans(cwd, plans) {
|
|
|
595
835
|
}
|
|
596
836
|
}
|
|
597
837
|
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
if (!fs.existsSync(filePath)) return false
|
|
601
|
-
|
|
602
|
-
const tracked =
|
|
838
|
+
function isTracked(cwd, file) {
|
|
839
|
+
return (
|
|
603
840
|
spawnSync('git', ['ls-files', '--error-unmatch', '--', file], {
|
|
604
841
|
cwd: cwd,
|
|
605
842
|
encoding: 'utf8',
|
|
606
843
|
}).status === 0
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function isUntracked(cwd, file) {
|
|
848
|
+
return !isTracked(cwd, file) && fs.existsSync(path.join(cwd, file))
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function fileHasChanges(cwd, file) {
|
|
852
|
+
const filePath = path.join(cwd, file)
|
|
853
|
+
const tracked = isTracked(cwd, file)
|
|
607
854
|
|
|
608
|
-
if (!tracked) return
|
|
855
|
+
if (!tracked) return fs.existsSync(filePath)
|
|
856
|
+
if (!fs.existsSync(filePath)) return true
|
|
609
857
|
|
|
610
858
|
return (
|
|
611
859
|
spawnSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
|
|
@@ -615,6 +863,11 @@ function fileHasChanges(cwd, file) {
|
|
|
615
863
|
)
|
|
616
864
|
}
|
|
617
865
|
|
|
866
|
+
function stageFiles(cwd, files) {
|
|
867
|
+
if (!files.length) return
|
|
868
|
+
git(cwd, ['add', '-A', '--', ...files])
|
|
869
|
+
}
|
|
870
|
+
|
|
618
871
|
async function confirm(count) {
|
|
619
872
|
if (!input.isTTY) throw new Error('No TTY — use --yes or --dry-run')
|
|
620
873
|
const rl = readline.createInterface({ input, output })
|
|
@@ -696,6 +949,8 @@ function git(cwd, args, option = {}) {
|
|
|
696
949
|
stdio: option.inherit ? ['ignore', 'inherit', 'pipe'] : undefined,
|
|
697
950
|
})
|
|
698
951
|
if (!result.status) return result
|
|
952
|
+
// git diff exits 1 when there are differences
|
|
953
|
+
if (result.status === 1 && args[0] === 'diff') return result
|
|
699
954
|
|
|
700
955
|
const stderr = result.stderr || ''
|
|
701
956
|
if (!isIndexLockError(stderr) || attempt === INDEX_LOCK_RETRIES) {
|