@itcase/config 1.7.24 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/smart-commit/cli.js +121 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itcase/config",
3
- "version": "1.7.24",
3
+ "version": "1.7.25",
4
4
  "author": "ITCase",
5
5
  "description": "ITCase Config",
6
6
  "engines": {
@@ -221,10 +221,17 @@ async function main(argv) {
221
221
 
222
222
  // Drop leftover index entries so only this plan's files are committed.
223
223
  git(cwd, ['reset', '-q', 'HEAD'])
224
- stageFiles(cwd, filesToStage)
224
+ const skippedOnStage = stageFiles(cwd, filesToStage)
225
225
  const staged = lines(cwd, ['diff', '--cached', '--name-only'])
226
226
  const stagedForPlan = filesToStage.filter((file) => staged.includes(file))
227
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
+ }
228
235
  throw new Error(
229
236
  `Nothing staged for commit: ${plan.message}\n` +
230
237
  `Files: ${filesToStage.join(', ')}`,
@@ -1548,13 +1555,28 @@ function isOnDisk(cwd, file) {
1548
1555
  function gitIgnoredFiles(cwd, files) {
1549
1556
  if (!files.length) return []
1550
1557
 
1551
- const result = spawnSync('git', ['check-ignore', '-z', '--stdin'], {
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,
1552
1573
  cwd: cwd,
1553
1574
  encoding: 'utf8',
1554
- input: `${files.join('\0')}\0`,
1575
+ input: `${paths.join('\0')}\0`,
1555
1576
  stdio: ['pipe', 'pipe', 'pipe'],
1556
1577
  })
1557
- // 0 — at least one path is ignored, 1 — none.
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.
1558
1580
  if (result.status !== 0) return []
1559
1581
 
1560
1582
  const ignored = new Set(
@@ -1562,7 +1584,67 @@ function gitIgnoredFiles(cwd, files) {
1562
1584
  .split('\0')
1563
1585
  .filter(Boolean),
1564
1586
  )
1565
- return files.filter((file) => ignored.has(file))
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
1566
1648
  }
1567
1649
 
1568
1650
  function isTracked(cwd, file) {
@@ -1631,14 +1713,17 @@ function includeHookLockfiles(cwd, planFiles, dirtyBefore) {
1631
1713
  }
1632
1714
 
1633
1715
  function stageFiles(cwd, files) {
1634
- if (!files.length) return
1716
+ if (!files.length) return []
1635
1717
 
1636
- // `git add -- ignored/path` exits 1 and abandons the rest of the batch.
1637
- // Tracked files that also match gitignore still stage; check-ignore
1638
- // omits them unless --no-index is passed.
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.
1639
1723
  const ignored = new Set(gitIgnoredFiles(cwd, files))
1640
1724
  const stageableFiles = files.filter((file) => !ignored.has(file))
1641
- if (!stageableFiles.length) return
1725
+ const skipped = [...ignored]
1726
+ if (!stageableFiles.length) return skipped
1642
1727
 
1643
1728
  const existing = []
1644
1729
  const removed = []
@@ -1650,11 +1735,33 @@ function stageFiles(cwd, files) {
1650
1735
  // `git add -A -- deleted/path` warns or misses the deletion when the
1651
1736
  // parent directory is already gone. `git rm` stages the index removal.
1652
1737
  forEachBatch(existing, (batch) => {
1653
- 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
+ )
1654
1760
  })
1655
1761
  forEachBatch(removed, (batch) => {
1656
1762
  git(cwd, ['rm', '--ignore-unmatch', '-q', '--', ...batch])
1657
1763
  })
1764
+ return skipped
1658
1765
  }
1659
1766
 
1660
1767
  async function confirm(count) {
@@ -1899,6 +2006,9 @@ function git(cwd, args, option = {}) {
1899
2006
 
1900
2007
  const stderr = result.stderr || ''
1901
2008
  if (!retryIndexLock(cwd, attempt, stderr)) {
2009
+ if (option.allowIgnored && ignoredPathsFromGitAdd(stderr).length) {
2010
+ return result
2011
+ }
1902
2012
  if (option.inherit && stderr) process.stderr.write(stderr)
1903
2013
  throw new Error(stderr.trim() || `git ${args.join(' ')}`)
1904
2014
  }