Repository: okonet/lint-staged (js)

Issue:
Compatibility with core.quotepath on
I’m sharing an issue I’ve encountered recently and I hope this may help others. IMHO there’s scope for a small improvement in `lint-staged` if I’ve understood things correctly.

### Description

Every time I staged a file with a non-ascii name, I was getting error messages saying that the file that I was trying commit did not exist.

When using `eslint --fix` as the first `lint-staged` command:

```
✖ eslint found some errors. Please fix them and try committing again.
No files matching the pattern "/path/to/project/"/320/277/321/200/320/270/320/262/320/265/321/202.md"" were found.
```

When using `prettier --write` as the first `lint-staged` command:

```
✖ prettier found some errors. Please fix them and try committing again.
[error] No files matching the pattern were found: "/path/to/project/"/320/277/321/200/320/270/320/262/320/265/321/202.md"".
```

Here is my setup (nothing extraordinary):

```json
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
    }
  },
  "lint-staged": {
    "**/*": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}
```

As it turned out, the issue came from a git config option [`core.quotepath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath), which turned out to be true on my machine.

### Steps to reproduce

I assume you are in a root of a git repo, in which `lint-staged` is configured similarly to above.

```sh
echo "hello world" > "привет.md"
git stage "привет.md"
```

```sh
git config core.quotepath on
git --no-pager diff --staged --name-only
# "\320\277\321\200\320\270\320\262\320\265\321\202.md"

git commit -m "Testing unicode in file names"
# ❌ because "/path/to/project/"/320/277/321/200/320/270/320/262/320/265/321/202.md"" not found
```

```sh
git config core.quotepath off
git --no-pager diff --staged --name-only
# привет.md

git commit -m "Testing unicode in file names"
# ✅ because "/path/to/project/привет.md" found
```

I managed to solve the problem locally by running `git config --global core.quotepath off` and thus avoiding `"..."` in `git ls-files`. However, other devs may fall into the same trap.

WDYT of using `--config core.quotepath=false` in [`getStagedFiles()`](https://github.com/okonet/lint-staged/blob/1ad263a5c03d0f3fd90f2dffd0d9083f0ef8112e/lib/getStagedFiles.js#L7)? I guess this should fix the issue permanently.

### Environment

<!-- Tell us about your development environment -->
- **OS:** macOS 10.15.4 (19E266)
- **Node.js:** c12.15.0
- **`lint-staged`:** v10.0.9
- **`git`:** 2.26.0


Pull request:
Support files with non-ASCII file names when git is configured with core.quotepath on
Fixes #818

Patch:
diff --git a/lib/getStagedFiles.js b/lib/getStagedFiles.js
index 61f2dd6..e36b807 100644
--- a/lib/getStagedFiles.js
+++ b/lib/getStagedFiles.js
@@ -4,8 +4,15 @@ const execGit = require('./execGit')
 
 module.exports = async function getStagedFiles(options) {
   try {
-    const lines = await execGit(['diff', '--staged', '--diff-filter=ACMR', '--name-only'], options)
-    return lines ? lines.split('\n') : []
+    // Docs for --diff-filter option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203
+    // Docs for -z option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--z
+    const lines = await execGit(
+      ['diff', '--staged', '--diff-filter=ACMR', '--name-only', '-z'],
+      options
+    )
+    // With `-z`, git prints `fileA\u0000fileB\u0000fileC\u0000` so we need to remove the last occurrence of `\u0000` before splitting
+    // eslint-disable-next-line no-control-regex
+    return lines ? lines.replace(/\u0000$/, '').split('\u0000') : []
   } catch {
     return null
   }


Test patch:
diff --git a/test/getStagedFiles.spec.js b/test/getStagedFiles.spec.js
index 07bc7aa..879ccf0 100644
--- a/test/getStagedFiles.spec.js
+++ b/test/getStagedFiles.spec.js
@@ -5,7 +5,7 @@ jest.mock('../lib/execGit')
 
 describe('getStagedFiles', () => {
   it('should return array of file names', async () => {
-    execGit.mockImplementationOnce(async () => 'foo.js\nbar.js')
+    execGit.mockImplementationOnce(async () => 'foo.js\u0000bar.js\u0000')
     const staged = await getStagedFiles()
     expect(staged).toEqual(['foo.js', 'bar.js'])
   })
diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js
index 52af3b5..88cfd68 100644
--- a/test/runAll.unmocked.spec.js
+++ b/test/runAll.unmocked.spec.js
@@ -843,6 +843,33 @@ describe('runAll', () => {
     expect(await execGit(['log', '-1', '--pretty=%B'], { cwd: workTreeDir })).toMatch('test')
     expect(await readFile('test.js', workTreeDir)).toEqual(testJsFilePretty)
   })
+
+  test.each([['on'], ['off']])(
+    'should handle files with non-ascii characters when core.quotepath is %s',
+    async quotePath => {
+      await execGit(['config', 'core.quotepath', quotePath])
+
+      // Stage multiple ugly files
+      await appendFile('привет.js', testJsFileUgly)
+      await execGit(['add', 'привет.js'])
+
+      await appendFile('你好.js', testJsFileUgly)
+      await execGit(['add', '你好.js'])
+
+      await appendFile('👋.js', testJsFileUgly)
+      await execGit(['add', '👋.js'])
+
+      // Run lint-staged with `prettier --write` and commit pretty files
+      await gitCommit(fixJsConfig)
+
+      // Nothing is wrong, so a new commit is created and files are pretty
+      expect(await execGit(['rev-list', '--count', 'HEAD'])).toEqual('2')
+      expect(await execGit(['log', '-1', '--pretty=%B'])).toMatch('test')
+      expect(await readFile('привет.js')).toEqual(testJsFilePretty)
+      expect(await readFile('你好.js')).toEqual(testJsFilePretty)
+      expect(await readFile('👋.js')).toEqual(testJsFilePretty)
+    }
+  )
 })
 
 describe('runAll', () => {