Repository: okonet/lint-staged
Pull request #784 (2020-01-30 05:08:39)

fix: always resolve real git config dir location if .git is a file
This PR fixes https://github.com/okonet/lint-staged/issues/783 by always checking if the the `.git` directory is actually file, and then resolves the real directory location by reading the file.

This works for both submodules and added worktrees.

codecov[bot]: # [Codecov](https://codecov.io/gh/okonet/lint-staged/pull/784?src=pr&el=h1) Report
> Merging [#784](https://codecov.io/gh/okonet/lint-staged/pull/784?src=pr&el=desc) into [master](https://codecov.io/gh/okonet/lint-staged/commit/9c08e8ee0bdf00de7dc6e15cd660e6fc55129832?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/okonet/lint-staged/pull/784/graphs/tree.svg?width=650&token=LSoEgsjk4O&height=150&src=pr)](https://codecov.io/gh/okonet/lint-staged/pull/784?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #784   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          13     13           
  Lines         484    485    +1     
  Branches      108    106    -2     
=====================================
+ Hits          484    485    +1
```


| [Impacted Files](https://codecov.io/gh/okonet/lint-staged/pull/784?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [lib/runAll.js](https://codecov.io/gh/okonet/lint-staged/pull/784/diff?src=pr&el=tree#diff-bGliL3J1bkFsbC5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [lib/resolveGitRepo.js](https://codecov.io/gh/okonet/lint-staged/pull/784/diff?src=pr&el=tree#diff-bGliL3Jlc29sdmVHaXRSZXBvLmpz) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/okonet/lint-staged/pull/784?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/okonet/lint-staged/pull/784?src=pr&el=footer). Last update [9c08e8e...89ae090](https://codecov.io/gh/okonet/lint-staged/pull/784?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/lib/resolveGitRepo.js b/lib/resolveGitRepo.js
index 5ee5d41..fa06498 100644
--- a/lib/resolveGitRepo.js
+++ b/lib/resolveGitRepo.js
@@ -1,48 +1,48 @@
 'use strict'
 
 const normalize = require('normalize-path')
+const fs = require('fs').promises
 const path = require('path')
 
+const debugLog = require('debug')('lint-staged:resolveGitRepo')
+
 const execGit = require('./execGit')
 const { readBufferFromFile } = require('./file')
 
 /**
  * Resolve path to the .git directory, with special handling for
- * submodules
+ * submodules and worktrees
  */
-const resolveGitConfigDir = async ({ gitDir, isSubmodule }) => {
+const resolveGitConfigDir = async gitDir => {
   const defaultDir = path.resolve(gitDir, '.git')
-  if (!isSubmodule) return normalize(defaultDir)
-
-  const buffer = await readBufferFromFile(defaultDir)
-  const dotGit = buffer.toString()
-  const gitConfigDir = path.resolve(gitDir, dotGit.replace(/^gitdir: /, '').trim())
-  return normalize(gitConfigDir)
+  const stats = await fs.lstat(defaultDir)
+  // If .git is a directory, use it
+  if (stats.isDirectory()) return defaultDir
+  // Otherwise .git is a file containing path to real location
+  const file = (await readBufferFromFile(defaultDir)).toString()
+  return path.resolve(gitDir, file.replace(/^gitdir: /, '')).trim()
 }
 
 /**
  * Resolve git directory and possible submodule paths
  */
-module.exports = async function resolveGitRepo(options = {}) {
+module.exports = async function resolveGitRepo(cwd) {
   try {
+    debugLog('Resolving git repo from `%s`', cwd)
     // git cli uses GIT_DIR to fast track its response however it might be set to a different path
     // depending on where the caller initiated this from, hence clear GIT_DIR
+    debugLog('Deleting GIT_DIR from env with value `%s`', process.env.GIT_DIR)
     delete process.env.GIT_DIR
 
-    // The git repo root directory; this points to the root of a submodule instead of the parent
-    const gitDir = await execGit(['rev-parse', '--show-toplevel'], options)
-
-    // A super-project working tree exists only in submodules; poinst to the parent root
-    const superprojectWorkingTree = await execGit(
-      ['rev-parse', '--show-superproject-working-tree'],
-      options
-    )
+    const gitDir = normalize(await execGit(['rev-parse', '--show-toplevel'], { cwd }))
+    const gitConfigDir = normalize(await resolveGitConfigDir(gitDir))
 
-    const isSubmodule = !!superprojectWorkingTree
-    const gitConfigDir = await resolveGitConfigDir({ gitDir, isSubmodule })
+    debugLog('Resolved git directory to be `%s`', gitDir)
+    debugLog('Resolved git config directory to be `%s`', gitConfigDir)
 
-    return { gitDir: normalize(gitDir), gitConfigDir, isSubmodule }
+    return { gitDir, gitConfigDir }
   } catch (error) {
-    return { error, gitDir: null }
+    debugLog('Failed to resolve git repo with error:', error)
+    return { error, gitDir: null, gitConfigDir: null }
   }
 }
diff --git a/lib/runAll.js b/lib/runAll.js
index 3a4f0e4..8d3a9ae 100644
--- a/lib/runAll.js
+++ b/lib/runAll.js
@@ -55,11 +55,8 @@ module.exports = async function runAll(
 ) {
   debugLog('Running all linter scripts')
 
-  const { gitDir, gitConfigDir, isSubmodule } = await resolveGitRepo({ cwd })
+  const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
   if (!gitDir) throw new Error('Current directory is not a git directory!')
-  debugLog('Resolved git directory to be `%s`', gitDir)
-  debugLog('Resolved git config directory to be `%s`', gitConfigDir)
-  if (isSubmodule) debugLog('Current git directory is a submodule')
 
   const files = await getStagedFiles({ cwd: gitDir })
   if (!files) throw new Error('Unable to get staged files!')


Test patch:
diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js
index 0c6233f..fd96f65 100644
--- a/test/runAll.unmocked.spec.js
+++ b/test/runAll.unmocked.spec.js
@@ -777,6 +777,30 @@ describe('runAll', () => {
     expect(await execGit(['log', '-1', '--pretty=%B'], { cwd: submoduleDir })).toMatch('test')
     expect(await readFile('test.js', submoduleDir)).toEqual(testJsFilePretty)
   })
+
+  it('should handle git worktrees', async () => {
+    // create a new branch and add it as worktree
+    const workTreeDir = path.resolve(cwd, 'worktree')
+    await execGit(['branch', 'test'])
+    await execGit(['worktree', 'add', workTreeDir, 'test'])
+
+    // Stage pretty file
+    await appendFile('test.js', testJsFilePretty, workTreeDir)
+    await execGit(['add', 'test.js'], { cwd: workTreeDir })
+
+    // Run lint-staged with `prettier --list-different` and commit pretty file
+    await runAll({
+      config: { '*.js': 'prettier --list-different' },
+      cwd: workTreeDir,
+      quiet: true
+    })
+    await execGit(['commit', '-m test'], { cwd: workTreeDir })
+
+    // Nothing is wrong, so a new commit is created
+    expect(await execGit(['rev-list', '--count', 'HEAD'], { cwd: workTreeDir })).toEqual('2')
+    expect(await execGit(['log', '-1', '--pretty=%B'], { cwd: workTreeDir })).toMatch('test')
+    expect(await readFile('test.js', workTreeDir)).toEqual(testJsFilePretty)
+  })
 })
 
 describe('runAll', () => {