Repository: okonet/lint-staged
Pull request #1178 (2022-06-24 08:45:52)

fix: correctly handle git stash when using MSYS2
This simple patch fixes https://github.com/okonet/lint-staged/issues/1121 by detecting MSYS2 shell via env variables set by MSYS2 (https://github.com/msys2/MSYS2-packages/blob/master/filesystem/msys2_shell.cmd) and adds escaped braces if MSYS2 is found.

iiroj: Thank you for the PR!

Please rename the commit as something like `fix: correctly handle git stash when using MSYS2`.

Also, this needs at least an unit test, let me figure out how to do it.

Patch:
diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js
index 9813aa4..9f0df24 100644
--- a/lib/gitWorkflow.js
+++ b/lib/gitWorkflow.js
@@ -43,7 +43,7 @@ const processRenames = (files, includeRenameFrom = true) =>
     return flattened
   }, [])
 
-const STASH = 'lint-staged automatic backup'
+export const STASH = 'lint-staged automatic backup'
 
 const PATCH_UNSTAGED = 'lint-staged_unstaged.patch'
 
@@ -103,6 +103,16 @@ export class GitWorkflow {
       ctx.errors.add(GetBackupStashError)
       throw new Error('lint-staged automatic backup is missing!')
     }
+
+    /**
+     * https://github.com/okonet/lint-staged/issues/1121
+     * Detect MSYS in login shell mode and escape braces
+     * to prevent interpolation
+     */
+    if (!!process.env.MSYSTEM && !!process.env.LOGINSHELL) {
+      return `refs/stash@\\{${index}\\}`
+    }
+
     return `refs/stash@{${index}}`
   }
 


Test patch:
diff --git a/test/unit/getBackupStash.spec.js b/test/unit/getBackupStash.spec.js
new file mode 100644
index 0000000..4c55fab
--- /dev/null
+++ b/test/unit/getBackupStash.spec.js
@@ -0,0 +1,93 @@
+import { execGit } from '../../lib/execGit.js'
+import { GitWorkflow, STASH } from '../../lib/gitWorkflow.js'
+import { getInitialState } from '../../lib/state.js'
+import { GetBackupStashError } from '../../lib/symbols'
+
+jest.mock('../../lib/execGit.js', () => ({
+  execGit: jest.fn(async () => ''),
+}))
+
+describe('gitWorkflow', () => {
+  const options = { gitConfigDir: '.' }
+
+  describe('getBackupStash', () => {
+    it('should throw when stash not found', async () => {
+      const gitWorkflow = new GitWorkflow(options)
+      const ctx = getInitialState()
+
+      await expect(gitWorkflow.getBackupStash(ctx)).rejects.toThrowError(
+        'lint-staged automatic backup is missing!'
+      )
+
+      expect(ctx.errors.has(GetBackupStashError)).toEqual(true)
+    })
+
+    it('should throw when stash not found even when other stashes are', async () => {
+      const gitWorkflow = new GitWorkflow(options)
+      const ctx = getInitialState()
+
+      execGit.mockResolvedValueOnce('stash@{0}: some random stuff')
+
+      await expect(gitWorkflow.getBackupStash(ctx)).rejects.toThrowError(
+        'lint-staged automatic backup is missing!'
+      )
+
+      expect(ctx.errors.has(GetBackupStashError)).toEqual(true)
+    })
+
+    it('should return ref to the backup stash', async () => {
+      const gitWorkflow = new GitWorkflow(options)
+      const ctx = getInitialState()
+
+      execGit.mockResolvedValueOnce(
+        [
+          'stash@{0}: some random stuff',
+          `stash@{1}: ${STASH}`,
+          'stash@{2}: other random stuff',
+        ].join('\n')
+      )
+
+      await expect(gitWorkflow.getBackupStash(ctx)).resolves.toEqual('refs/stash@{1}')
+    })
+
+    it('should return unescaped ref to the backup stash when using MSYS2 without login shell', async () => {
+      const gitWorkflow = new GitWorkflow(options)
+      const ctx = getInitialState()
+
+      process.env.MSYSTEM = 'MSYS'
+
+      execGit.mockResolvedValueOnce(
+        [
+          'stash@{0}: some random stuff',
+          `stash@{1}: ${STASH}`,
+          'stash@{2}: other random stuff',
+        ].join('\n')
+      )
+
+      await expect(gitWorkflow.getBackupStash(ctx)).resolves.toEqual('refs/stash@{1}')
+
+      delete process.env.MSYSTEM
+    })
+
+    it('should return escaped ref to the backup stash when using MSYS2 with login shell', async () => {
+      const gitWorkflow = new GitWorkflow(options)
+      const ctx = getInitialState()
+
+      process.env.MSYSTEM = 'MSYS'
+      process.env.LOGINSHELL = 'bash'
+
+      execGit.mockResolvedValueOnce(
+        [
+          'stash@{0}: some random stuff',
+          `stash@{1}: ${STASH}`,
+          'stash@{2}: other random stuff',
+        ].join('\n')
+      )
+
+      await expect(gitWorkflow.getBackupStash(ctx)).resolves.toEqual('refs/stash@\\{1\\}')
+
+      delete process.env.MSYSTEM
+      delete process.env.LOGINSHELL
+    })
+  })
+})