@itcase/config 1.7.23 → 1.7.25
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 +158 -10
package/package.json
CHANGED
package/smart-commit/cli.js
CHANGED
|
@@ -202,28 +202,46 @@ async function main(argv) {
|
|
|
202
202
|
continue
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
const ignoredFiles = gitIgnoredFiles(cwd, planFiles)
|
|
206
|
+
const filesToStage = planFiles.filter(
|
|
207
|
+
(file) => !ignoredFiles.includes(file),
|
|
208
|
+
)
|
|
209
|
+
process.stdout.write(`\n${paint('1;36', step)} ${plan.message}\n`)
|
|
210
|
+
if (ignoredFiles.length) {
|
|
211
|
+
process.stdout.write(
|
|
212
|
+
`${dim(` skipped (gitignore): ${ignoredFiles.join(', ')}`)}\n`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
if (!filesToStage.length) continue
|
|
216
|
+
|
|
205
217
|
const startedAt = Date.now()
|
|
206
218
|
const fileLabel =
|
|
207
|
-
|
|
208
|
-
process.stdout.write(`\n${paint('1;36', step)} ${plan.message}\n`)
|
|
219
|
+
filesToStage.length === 1 ? '1 file' : `${filesToStage.length} files`
|
|
209
220
|
process.stdout.write(`${dim(` staging ${fileLabel}…`)}\n`)
|
|
210
221
|
|
|
211
222
|
// Drop leftover index entries so only this plan's files are committed.
|
|
212
223
|
git(cwd, ['reset', '-q', 'HEAD'])
|
|
213
|
-
stageFiles(cwd,
|
|
224
|
+
const skippedOnStage = stageFiles(cwd, filesToStage)
|
|
214
225
|
const staged = lines(cwd, ['diff', '--cached', '--name-only'])
|
|
215
|
-
const stagedForPlan =
|
|
226
|
+
const stagedForPlan = filesToStage.filter((file) => staged.includes(file))
|
|
216
227
|
if (!stagedForPlan.length) {
|
|
228
|
+
const skipped = new Set(skippedOnStage)
|
|
229
|
+
// Ignored paths were not staged on purpose. Stop this commit and keep
|
|
230
|
+
// going — one ignored directory must not drop the rest of the run.
|
|
231
|
+
if (filesToStage.every((file) => skipped.has(file))) {
|
|
232
|
+
process.stdout.write(`${dim(` skip ignored: ${plan.message}`)}\n`)
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
217
235
|
throw new Error(
|
|
218
236
|
`Nothing staged for commit: ${plan.message}\n` +
|
|
219
|
-
`Files: ${
|
|
237
|
+
`Files: ${filesToStage.join(', ')}`,
|
|
220
238
|
)
|
|
221
239
|
}
|
|
222
240
|
|
|
223
241
|
const commitLabel = option.noVerify
|
|
224
242
|
? 'git commit --no-verify'
|
|
225
243
|
: 'git commit'
|
|
226
|
-
const hookLocks = siblingLocksForFiles(
|
|
244
|
+
const hookLocks = siblingLocksForFiles(filesToStage)
|
|
227
245
|
const dirtyLocksBefore = new Set(
|
|
228
246
|
hookLocks.filter((file) => fileHasChanges(cwd, file)),
|
|
229
247
|
)
|
|
@@ -233,7 +251,7 @@ async function main(argv) {
|
|
|
233
251
|
// equivalent flag — the hook simply does not run.
|
|
234
252
|
if (option.noVerify) commitArgs.push('--no-verify')
|
|
235
253
|
await gitCommitWithProgress(cwd, commitArgs, commitLabel)
|
|
236
|
-
const included = includeHookLockfiles(cwd,
|
|
254
|
+
const included = includeHookLockfiles(cwd, filesToStage, dirtyLocksBefore)
|
|
237
255
|
if (included.length) {
|
|
238
256
|
process.stdout.write(
|
|
239
257
|
`${dim(` hook updated ${included.join(', ')}, added to commit`)}\n`,
|
|
@@ -1534,6 +1552,101 @@ function isOnDisk(cwd, file) {
|
|
|
1534
1552
|
return fs.existsSync(path.join(cwd, file))
|
|
1535
1553
|
}
|
|
1536
1554
|
|
|
1555
|
+
function gitIgnoredFiles(cwd, files) {
|
|
1556
|
+
if (!files.length) return []
|
|
1557
|
+
|
|
1558
|
+
// Without --no-index, check-ignore hides tracked files that match
|
|
1559
|
+
// gitignore. Those still stage, unless a parent directory is ignored:
|
|
1560
|
+
// `git add` then exits 1 and names the directory, not the file.
|
|
1561
|
+
const direct = pathsIgnoredByGit(cwd, files)
|
|
1562
|
+
const underIgnoredDirectory = filesUnderIgnoredDirectory(cwd, files)
|
|
1563
|
+
return [...new Set([...direct, ...underIgnoredDirectory])]
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function pathsIgnoredByGit(cwd, paths, option = {}) {
|
|
1567
|
+
if (!paths.length) return []
|
|
1568
|
+
|
|
1569
|
+
const args = ['check-ignore', '-z', '--stdin']
|
|
1570
|
+
if (option.noIndex) args.push('--no-index')
|
|
1571
|
+
const result = spawnSync('git', args, {
|
|
1572
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
1573
|
+
cwd: cwd,
|
|
1574
|
+
encoding: 'utf8',
|
|
1575
|
+
input: `${paths.join('\0')}\0`,
|
|
1576
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1577
|
+
})
|
|
1578
|
+
// 0 — at least one path is ignored, 1 — none. Any other status is a
|
|
1579
|
+
// fatal; treat it as "nothing ignored" and let git add surface it.
|
|
1580
|
+
if (result.status !== 0) return []
|
|
1581
|
+
|
|
1582
|
+
const ignored = new Set(
|
|
1583
|
+
String(result.stdout || '')
|
|
1584
|
+
.split('\0')
|
|
1585
|
+
.filter(Boolean),
|
|
1586
|
+
)
|
|
1587
|
+
return paths.filter((file) => ignored.has(file))
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function ancestorDirectories(file) {
|
|
1591
|
+
const ancestors = []
|
|
1592
|
+
let directory = path.posix.dirname(file)
|
|
1593
|
+
while (directory && directory !== '.') {
|
|
1594
|
+
ancestors.push(directory)
|
|
1595
|
+
directory = path.posix.dirname(directory)
|
|
1596
|
+
}
|
|
1597
|
+
return ancestors
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function filesUnderIgnoredDirectory(cwd, files) {
|
|
1601
|
+
const ancestorsByFile = new Map()
|
|
1602
|
+
const uniqueAncestors = []
|
|
1603
|
+
const seen = new Set()
|
|
1604
|
+
for (const file of files) {
|
|
1605
|
+
const ancestors = ancestorDirectories(file)
|
|
1606
|
+
ancestorsByFile.set(file, ancestors)
|
|
1607
|
+
for (const ancestor of ancestors) {
|
|
1608
|
+
if (seen.has(ancestor)) continue
|
|
1609
|
+
seen.add(ancestor)
|
|
1610
|
+
uniqueAncestors.push(ancestor)
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const ignoredAncestors = new Set(
|
|
1615
|
+
pathsIgnoredByGit(cwd, uniqueAncestors, { noIndex: true }),
|
|
1616
|
+
)
|
|
1617
|
+
if (!ignoredAncestors.size) return []
|
|
1618
|
+
|
|
1619
|
+
return files.filter((file) =>
|
|
1620
|
+
ancestorsByFile
|
|
1621
|
+
.get(file)
|
|
1622
|
+
.some((ancestor) => ignoredAncestors.has(ancestor)),
|
|
1623
|
+
)
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
function pathMatchesIgnored(file, ignoredPaths) {
|
|
1627
|
+
return ignoredPaths.some(
|
|
1628
|
+
(ignored) => file === ignored || file.startsWith(`${ignored}/`),
|
|
1629
|
+
)
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function ignoredPathsFromGitAdd(stderr) {
|
|
1633
|
+
const header =
|
|
1634
|
+
'The following paths are ignored by one of your .gitignore files:'
|
|
1635
|
+
const start = String(stderr || '').indexOf(header)
|
|
1636
|
+
if (start < 0) return []
|
|
1637
|
+
|
|
1638
|
+
const paths = []
|
|
1639
|
+
for (const line of String(stderr)
|
|
1640
|
+
.slice(start + header.length)
|
|
1641
|
+
.split('\n')) {
|
|
1642
|
+
const trimmed = line.trim().replace(/\/+$/, '')
|
|
1643
|
+
if (!trimmed) continue
|
|
1644
|
+
if (trimmed.startsWith('hint:')) break
|
|
1645
|
+
paths.push(trimmed)
|
|
1646
|
+
}
|
|
1647
|
+
return paths
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1537
1650
|
function isTracked(cwd, file) {
|
|
1538
1651
|
return gitOk(cwd, ['ls-files', '--error-unmatch', '--', file])
|
|
1539
1652
|
}
|
|
@@ -1600,11 +1713,21 @@ function includeHookLockfiles(cwd, planFiles, dirtyBefore) {
|
|
|
1600
1713
|
}
|
|
1601
1714
|
|
|
1602
1715
|
function stageFiles(cwd, files) {
|
|
1603
|
-
if (!files.length) return
|
|
1716
|
+
if (!files.length) return []
|
|
1717
|
+
|
|
1718
|
+
// `git add -- path/inside/ignored-dir` exits 1 and names the directory,
|
|
1719
|
+
// after it has already updated the index. Drop those paths first. If git
|
|
1720
|
+
// add still reports an ignored directory, take those paths back out and
|
|
1721
|
+
// keep the rest of the batch. Tracked files that match gitignore but do
|
|
1722
|
+
// not sit inside an ignored directory still stage.
|
|
1723
|
+
const ignored = new Set(gitIgnoredFiles(cwd, files))
|
|
1724
|
+
const stageableFiles = files.filter((file) => !ignored.has(file))
|
|
1725
|
+
const skipped = [...ignored]
|
|
1726
|
+
if (!stageableFiles.length) return skipped
|
|
1604
1727
|
|
|
1605
1728
|
const existing = []
|
|
1606
1729
|
const removed = []
|
|
1607
|
-
for (const file of
|
|
1730
|
+
for (const file of stageableFiles) {
|
|
1608
1731
|
if (isOnDisk(cwd, file)) existing.push(file)
|
|
1609
1732
|
else removed.push(file)
|
|
1610
1733
|
}
|
|
@@ -1612,11 +1735,33 @@ function stageFiles(cwd, files) {
|
|
|
1612
1735
|
// `git add -A -- deleted/path` warns or misses the deletion when the
|
|
1613
1736
|
// parent directory is already gone. `git rm` stages the index removal.
|
|
1614
1737
|
forEachBatch(existing, (batch) => {
|
|
1615
|
-
git(cwd, ['add', '-A', '--', ...batch]
|
|
1738
|
+
const result = git(cwd, ['add', '-A', '--', ...batch], {
|
|
1739
|
+
allowIgnored: true,
|
|
1740
|
+
})
|
|
1741
|
+
if (!result.status) return
|
|
1742
|
+
|
|
1743
|
+
const ignoredPaths = ignoredPathsFromGitAdd(result.stderr || '')
|
|
1744
|
+
const blocked = batch.filter((file) =>
|
|
1745
|
+
pathMatchesIgnored(file, ignoredPaths),
|
|
1746
|
+
)
|
|
1747
|
+
if (!blocked.length) {
|
|
1748
|
+
throw new Error(
|
|
1749
|
+
(result.stderr || '').trim() || `git add -A -- ${batch.join(' ')}`,
|
|
1750
|
+
)
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// git add writes the index before returning 1, including tracked files
|
|
1754
|
+
// under the ignored directory. Take those back out of this commit.
|
|
1755
|
+
git(cwd, ['reset', '-q', 'HEAD', '--', ...blocked])
|
|
1756
|
+
skipped.push(...blocked)
|
|
1757
|
+
process.stdout.write(
|
|
1758
|
+
`${dim(` skipped (gitignore): ${blocked.join(', ')}`)}\n`,
|
|
1759
|
+
)
|
|
1616
1760
|
})
|
|
1617
1761
|
forEachBatch(removed, (batch) => {
|
|
1618
1762
|
git(cwd, ['rm', '--ignore-unmatch', '-q', '--', ...batch])
|
|
1619
1763
|
})
|
|
1764
|
+
return skipped
|
|
1620
1765
|
}
|
|
1621
1766
|
|
|
1622
1767
|
async function confirm(count) {
|
|
@@ -1861,6 +2006,9 @@ function git(cwd, args, option = {}) {
|
|
|
1861
2006
|
|
|
1862
2007
|
const stderr = result.stderr || ''
|
|
1863
2008
|
if (!retryIndexLock(cwd, attempt, stderr)) {
|
|
2009
|
+
if (option.allowIgnored && ignoredPathsFromGitAdd(stderr).length) {
|
|
2010
|
+
return result
|
|
2011
|
+
}
|
|
1864
2012
|
if (option.inherit && stderr) process.stderr.write(stderr)
|
|
1865
2013
|
throw new Error(stderr.trim() || `git ${args.join(' ')}`)
|
|
1866
2014
|
}
|