From dbc271eb3a6d3f845e2b83fee35158d369984a19 Mon Sep 17 00:00:00 2001 From: "voidzero-bot[bot]" <348775505+voidzero-bot[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:28:01 +0000 Subject: [PATCH 1/2] fix(cli/staged): replace stale unstaged recovery patch message --- packages/cli/src/staged/logger.spec.ts | 52 ++++++++++++++++++++++++++ packages/cli/src/staged/logger.ts | 46 ++++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/staged/logger.spec.ts b/packages/cli/src/staged/logger.spec.ts index 79d30ecb3c..348ddb7c94 100644 --- a/packages/cli/src/staged/logger.spec.ts +++ b/packages/cli/src/staged/logger.spec.ts @@ -1,3 +1,7 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + import type { Logger } from 'lint-staged'; import { describe, expect, it, vi, type Mocked } from 'vite-plus/test'; @@ -70,4 +74,52 @@ describe('staged logger', () => { logger.log(' %s', 'task output'); expect(output.log.mock.calls).toEqual([[taskOutput], [' %s', 'task output']]); }); + + it('keeps the lint-staged recovery message when the patch file exists', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-staged-patch-')); + const patchPath = path.join(tmpDir, 'unstaged.patch'); + fs.writeFileSync(patchPath, 'patch content'); + const { output, logger } = setup(); + + logger.warn('Unstaged changes have been kept back in a patch file:'); + logger.warn(patchPath); + + expect(output.warn.mock.calls).toEqual([ + ['Unstaged changes have been kept back in a patch file:'], + [patchPath], + ]); + + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('replaces the stale recovery message when the patch file is already gone', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { output, logger } = setup(); + const patchPath = path.join(os.tmpdir(), 'vp-staged-missing', 'unstaged.patch'); + // Ensure the file does not exist (parent directory may or may not exist). + fs.rmSync(patchPath, { force: true }); + + logger.warn('Unstaged changes have been kept back in a patch file:'); + logger.warn(patchPath); + + // The original label/path pair is suppressed; a truthful Vite+ warning is emitted instead. + expect(output.warn.mock.calls).toEqual([]); + expect(consoleErrorSpy.mock.calls.length).toBe(1); + expect(consoleErrorSpy.mock.calls[0][1]).toBe( + 'Unstaged changes were restored from the backup stash. No recovery patch remains.', + ); + + consoleErrorSpy.mockRestore(); + }); + + it('flushes a buffered recovery label if the next warning is not a patch path', () => { + const { output, logger } = setup(); + logger.warn('Unstaged changes have been kept back in a patch file:'); + logger.warn('Some unrelated warning'); + + expect(output.warn.mock.calls).toEqual([ + ['Unstaged changes have been kept back in a patch file:'], + ['Some unrelated warning'], + ]); + }); }); diff --git a/packages/cli/src/staged/logger.ts b/packages/cli/src/staged/logger.ts index 4b6df5c53f..81a194bb78 100644 --- a/packages/cli/src/staged/logger.ts +++ b/packages/cli/src/staged/logger.ts @@ -1,8 +1,9 @@ +import fs from 'node:fs'; import { stripVTControlCharacters } from 'node:util'; import type { Logger } from 'lint-staged'; -import { log } from '../utils/terminal.ts'; +import { log, warnMsg } from '../utils/terminal.ts'; // lint-staged exposes a text logger, but no task events or custom renderer. // Hide its task plan and routine Git progress, keeping command results and @@ -32,7 +33,16 @@ function isWorkflowProgress(message: string): boolean { ); } +const UNSTAGED_PATCH_LABEL = 'Unstaged changes have been kept back in a patch file:'; + +function isUnstagedPatchPath(message: string): boolean { + const text = stripVTControlCharacters(message).trim(); + return text.length > 0 && !text.includes('\n') && text.endsWith('/unstaged.patch'); +} + export function createStagedLogger(logger: Logger = { ...console, log }): Logger { + let pendingPatchLabel = false; + return { ...logger, log(message, ...args) { @@ -41,5 +51,39 @@ export function createStagedLogger(logger: Logger = { ...console, log }): Logger } logger.log(message, ...args); }, + warn(message, ...args) { + if (typeof message !== 'string' || args.length !== 0) { + logger.warn(message, ...args); + return; + } + + const text = stripVTControlCharacters(message); + + if (pendingPatchLabel) { + pendingPatchLabel = false; + if (isUnstagedPatchPath(text)) { + if (fs.existsSync(text.trim())) { + logger.warn(UNSTAGED_PATCH_LABEL); + logger.warn(text); + } else { + warnMsg( + 'Unstaged changes were restored from the backup stash. No recovery patch remains.', + ); + } + return; + } + // Not the expected path: flush the buffered label first. + logger.warn(UNSTAGED_PATCH_LABEL); + logger.warn(message); + return; + } + + if (text.trim() === UNSTAGED_PATCH_LABEL) { + pendingPatchLabel = true; + return; + } + + logger.warn(message, ...args); + }, }; } From fe0bb4191bce6cee5e68939090294be9c2bcd092 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 13 Nov 2026 01:00:00 +0800 Subject: [PATCH 2/2] fix(staged): report recovery from rollback state --- .../command_staged_restore_conflict/file.txt | 3 + .../package.json | 5 + .../reformat.cjs | 2 + .../snapshots.toml | 18 +++ .../command_staged_restore_conflict.global.md | 75 ++++++++++ .../command_staged_restore_conflict.local.md | 75 ++++++++++ .../vite.config.ts | 5 + packages/cli/src/staged/logger.spec.ts | 52 ------- packages/cli/src/staged/logger.ts | 46 +------ packages/cli/src/staged/recovery.spec.ts | 128 ++++++++++++++++++ patches/lint-staged@17.6.0.patch | 43 ++++++ pnpm-lock.yaml | 7 +- pnpm-workspace.yaml | 1 + 13 files changed, 360 insertions(+), 100 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/file.txt create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/reformat.cjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots/command_staged_restore_conflict.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots/command_staged_restore_conflict.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/vite.config.ts create mode 100644 packages/cli/src/staged/recovery.spec.ts create mode 100644 patches/lint-staged@17.6.0.patch diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/file.txt b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/file.txt new file mode 100644 index 0000000000..de980441c3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/file.txt @@ -0,0 +1,3 @@ +a +b +c diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/package.json new file mode 100644 index 0000000000..1661268e37 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-staged-restore-conflict", + "private": true, + "type": "module" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/reformat.cjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/reformat.cjs new file mode 100644 index 0000000000..208e045599 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/reformat.cjs @@ -0,0 +1,2 @@ +// Removing the changed line makes both attempts to apply the hidden diff fail. +require('node:fs').writeFileSync('file.txt', 'a\nc\n'); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots.toml new file mode 100644 index 0000000000..a9edbef719 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots.toml @@ -0,0 +1,18 @@ +[[case]] +name = "command_staged_restore_conflict" +vp = ["local", "global"] +comment = "A restore conflict in a linked worktree reports successful rollback without naming a deleted patch." +steps = [ + { argv = ["git", "init"], snapshot = false }, + { argv = ["git", "add", "-A"], snapshot = false }, + { argv = ["git", "-c", "user.name=Vite Plus", "-c", "user.email=vite-plus@example.com", "commit", "-m", "init"], snapshot = false }, + { argv = ["git", "worktree", "add", "--detach", "linked", "HEAD"], snapshot = false }, + { argv = ["vpt", "write-file", "linked/file.txt", "a\nstaged\nc\n"], snapshot = false }, + { argv = ["git", "-C", "linked", "add", "file.txt"], snapshot = false }, + { argv = ["vpt", "write-file", "linked/file.txt", "a\nunstaged\nc\n"], snapshot = false }, + { argv = ["vp", "staged", "--cwd", "linked"], continue-on-failure = true }, + { argv = ["vpt", "print-file", "linked/file.txt"], comment = "The working tree retains the unstaged change." }, + { argv = ["git", "-C", "linked", "show", ":file.txt"], comment = "The index retains the staged change." }, + { argv = ["vpt", "list-dir", ".git/worktrees/linked/lint-staged"], comment = "The temporary recovery directory is empty." }, + { argv = ["git", "stash", "list"], comment = "The backup stash was cleaned up after successful rollback." }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots/command_staged_restore_conflict.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots/command_staged_restore_conflict.global.md new file mode 100644 index 0000000000..182a855179 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots/command_staged_restore_conflict.global.md @@ -0,0 +1,75 @@ +# command_staged_restore_conflict + +A restore conflict in a linked worktree reports successful rollback without naming a deleted patch. + +## `git init` + + +## `git add -A` + + +## `git -c 'user.name=Vite Plus' -c user.email=vite-plus@example.com commit -m init` + + +## `git worktree add --detach linked HEAD` + + +## `vpt write-file linked/file.txt 'a +staged +c +'` + + +## `git -C linked add file.txt` + + +## `vpt write-file linked/file.txt 'a +unstaged +c +'` + + +## `vp staged --cwd linked` + +**Exit code:** 1 + +``` +✔ node reformat.cjs +✖ Failed to restore unstaged changes! +✔ Done reverting to original state! +Original state was restored from the backup stash. +``` + +## `vpt print-file linked/file.txt` + +The working tree retains the unstaged change. + +``` +a +unstaged +c +``` + +## `git -C linked show :file.txt` + +The index retains the staged change. + +``` +a +staged +c +``` + +## `vpt list-dir .git/worktrees/linked/lint-staged` + +The temporary recovery directory is empty. + +``` +``` + +## `git stash list` + +The backup stash was cleaned up after successful rollback. + +``` +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots/command_staged_restore_conflict.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots/command_staged_restore_conflict.local.md new file mode 100644 index 0000000000..182a855179 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/snapshots/command_staged_restore_conflict.local.md @@ -0,0 +1,75 @@ +# command_staged_restore_conflict + +A restore conflict in a linked worktree reports successful rollback without naming a deleted patch. + +## `git init` + + +## `git add -A` + + +## `git -c 'user.name=Vite Plus' -c user.email=vite-plus@example.com commit -m init` + + +## `git worktree add --detach linked HEAD` + + +## `vpt write-file linked/file.txt 'a +staged +c +'` + + +## `git -C linked add file.txt` + + +## `vpt write-file linked/file.txt 'a +unstaged +c +'` + + +## `vp staged --cwd linked` + +**Exit code:** 1 + +``` +✔ node reformat.cjs +✖ Failed to restore unstaged changes! +✔ Done reverting to original state! +Original state was restored from the backup stash. +``` + +## `vpt print-file linked/file.txt` + +The working tree retains the unstaged change. + +``` +a +unstaged +c +``` + +## `git -C linked show :file.txt` + +The index retains the staged change. + +``` +a +staged +c +``` + +## `vpt list-dir .git/worktrees/linked/lint-staged` + +The temporary recovery directory is empty. + +``` +``` + +## `git stash list` + +The backup stash was cleaned up after successful rollback. + +``` +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/vite.config.ts new file mode 100644 index 0000000000..2bfc9225b7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_staged_restore_conflict/vite.config.ts @@ -0,0 +1,5 @@ +export default { + staged: { + '*.txt': 'node reformat.cjs', + }, +}; diff --git a/packages/cli/src/staged/logger.spec.ts b/packages/cli/src/staged/logger.spec.ts index 348ddb7c94..79d30ecb3c 100644 --- a/packages/cli/src/staged/logger.spec.ts +++ b/packages/cli/src/staged/logger.spec.ts @@ -1,7 +1,3 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; - import type { Logger } from 'lint-staged'; import { describe, expect, it, vi, type Mocked } from 'vite-plus/test'; @@ -74,52 +70,4 @@ describe('staged logger', () => { logger.log(' %s', 'task output'); expect(output.log.mock.calls).toEqual([[taskOutput], [' %s', 'task output']]); }); - - it('keeps the lint-staged recovery message when the patch file exists', () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-staged-patch-')); - const patchPath = path.join(tmpDir, 'unstaged.patch'); - fs.writeFileSync(patchPath, 'patch content'); - const { output, logger } = setup(); - - logger.warn('Unstaged changes have been kept back in a patch file:'); - logger.warn(patchPath); - - expect(output.warn.mock.calls).toEqual([ - ['Unstaged changes have been kept back in a patch file:'], - [patchPath], - ]); - - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - it('replaces the stale recovery message when the patch file is already gone', () => { - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const { output, logger } = setup(); - const patchPath = path.join(os.tmpdir(), 'vp-staged-missing', 'unstaged.patch'); - // Ensure the file does not exist (parent directory may or may not exist). - fs.rmSync(patchPath, { force: true }); - - logger.warn('Unstaged changes have been kept back in a patch file:'); - logger.warn(patchPath); - - // The original label/path pair is suppressed; a truthful Vite+ warning is emitted instead. - expect(output.warn.mock.calls).toEqual([]); - expect(consoleErrorSpy.mock.calls.length).toBe(1); - expect(consoleErrorSpy.mock.calls[0][1]).toBe( - 'Unstaged changes were restored from the backup stash. No recovery patch remains.', - ); - - consoleErrorSpy.mockRestore(); - }); - - it('flushes a buffered recovery label if the next warning is not a patch path', () => { - const { output, logger } = setup(); - logger.warn('Unstaged changes have been kept back in a patch file:'); - logger.warn('Some unrelated warning'); - - expect(output.warn.mock.calls).toEqual([ - ['Unstaged changes have been kept back in a patch file:'], - ['Some unrelated warning'], - ]); - }); }); diff --git a/packages/cli/src/staged/logger.ts b/packages/cli/src/staged/logger.ts index 81a194bb78..4b6df5c53f 100644 --- a/packages/cli/src/staged/logger.ts +++ b/packages/cli/src/staged/logger.ts @@ -1,9 +1,8 @@ -import fs from 'node:fs'; import { stripVTControlCharacters } from 'node:util'; import type { Logger } from 'lint-staged'; -import { log, warnMsg } from '../utils/terminal.ts'; +import { log } from '../utils/terminal.ts'; // lint-staged exposes a text logger, but no task events or custom renderer. // Hide its task plan and routine Git progress, keeping command results and @@ -33,16 +32,7 @@ function isWorkflowProgress(message: string): boolean { ); } -const UNSTAGED_PATCH_LABEL = 'Unstaged changes have been kept back in a patch file:'; - -function isUnstagedPatchPath(message: string): boolean { - const text = stripVTControlCharacters(message).trim(); - return text.length > 0 && !text.includes('\n') && text.endsWith('/unstaged.patch'); -} - export function createStagedLogger(logger: Logger = { ...console, log }): Logger { - let pendingPatchLabel = false; - return { ...logger, log(message, ...args) { @@ -51,39 +41,5 @@ export function createStagedLogger(logger: Logger = { ...console, log }): Logger } logger.log(message, ...args); }, - warn(message, ...args) { - if (typeof message !== 'string' || args.length !== 0) { - logger.warn(message, ...args); - return; - } - - const text = stripVTControlCharacters(message); - - if (pendingPatchLabel) { - pendingPatchLabel = false; - if (isUnstagedPatchPath(text)) { - if (fs.existsSync(text.trim())) { - logger.warn(UNSTAGED_PATCH_LABEL); - logger.warn(text); - } else { - warnMsg( - 'Unstaged changes were restored from the backup stash. No recovery patch remains.', - ); - } - return; - } - // Not the expected path: flush the buffered label first. - logger.warn(UNSTAGED_PATCH_LABEL); - logger.warn(message); - return; - } - - if (text.trim() === UNSTAGED_PATCH_LABEL) { - pendingPatchLabel = true; - return; - } - - logger.warn(message, ...args); - }, }; } diff --git a/packages/cli/src/staged/recovery.spec.ts b/packages/cli/src/staged/recovery.spec.ts new file mode 100644 index 0000000000..2efd8259ad --- /dev/null +++ b/packages/cli/src/staged/recovery.spec.ts @@ -0,0 +1,128 @@ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import lintStaged from 'lint-staged'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'; + +import { createStagedLogger } from './logger.ts'; + +const restoredMessage = 'Original state was restored from the backup stash.'; +const patchMessage = 'Unstaged changes have been kept back in a patch file:'; + +function createLogger() { + return { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }; +} + +describe('staged recovery', () => { + let cwd: string; + const git = (...args: string[]) => execFileSync('git', args, { cwd, encoding: 'utf8' }).trim(); + + beforeEach(() => { + cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-staged-recovery-')); + git('init', '--quiet'); + git('config', 'user.name', 'Vite Plus'); + git('config', 'user.email', 'vite-plus@example.com'); + git('config', 'commit.gpgsign', 'false'); + git('config', 'core.autocrlf', 'false'); + fs.writeFileSync(path.join(cwd, 'file.txt'), 'a\nb\nc\n'); + fs.writeFileSync( + path.join(cwd, 'task.cjs'), + "require('node:fs').writeFileSync('file.txt', 'a\\nc\\n');\n", + ); + git('add', '.'); + git('-c', 'core.hooksPath=', 'commit', '--quiet', '-m', 'init'); + fs.writeFileSync(path.join(cwd, 'file.txt'), 'a\nstaged\nc\n'); + git('add', 'file.txt'); + fs.writeFileSync(path.join(cwd, 'file.txt'), 'a\nunstaged\nc\n'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(cwd, { recursive: true, force: true }); + }); + + function expectOriginalState() { + expect(git('show', ':file.txt')).toBe('a\nstaged\nc'); + expect(fs.readFileSync(path.join(cwd, 'file.txt'), 'utf8')).toBe('a\nunstaged\nc\n'); + } + + it.each([false, true])('reports successful rollback with debug=%s', async (debug) => { + const logger = createLogger(); + const success = await lintStaged( + { cwd, config: { '*.txt': 'node task.cjs' }, debug, color: false }, + debug ? logger : createStagedLogger(logger), + ); + + expect(success).toBe(false); + expect(logger.warn.mock.calls).toEqual([[restoredMessage]]); + expectOriginalState(); + expect(git('stash', 'list')).toBe(''); + }); + + it.each([{ revert: false }, { stash: false }])( + 'keeps the recovery patch with %o', + async (options) => { + const logger = createLogger(); + const success = await lintStaged( + { cwd, config: { '*.txt': 'node task.cjs' }, color: false, ...options }, + createStagedLogger(logger), + ); + + expect(success).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(patchMessage); + expect(logger.warn).not.toHaveBeenCalledWith(restoredMessage); + const patchPath = logger.warn.mock.calls.at(-1)![0] as string; + expect(fs.readFileSync(patchPath, 'utf8')).toContain('+unstaged'); + }, + ); + + it('keeps the recovery patch and backup stash when rollback fails', async () => { + // Fail only the rollback's stash apply; all other Git operations are real. + const { GitWorkflow } = await import( + new URL('./gitWorkflow.js', import.meta.resolve('lint-staged')).href + ); + const restoreOriginalState = GitWorkflow.prototype.restoreOriginalState; + vi.spyOn(GitWorkflow.prototype, 'restoreOriginalState').mockImplementation(async function ( + this: { execGit: (args: string[]) => Promise }, + ctx: unknown, + ) { + const execGit = this.execGit; + this.execGit = async (args) => { + if (args[0] === 'stash' && args[1] === 'apply') { + throw new Error('Simulated stash apply failure'); + } + return execGit(args); + }; + await restoreOriginalState.call(this, ctx); + }); + const logger = createLogger(); + const success = await lintStaged( + { cwd, config: { '*.txt': 'node task.cjs' }, color: false }, + createStagedLogger(logger), + ); + + expect(success).toBe(false); + expect(logger.error).toHaveBeenCalledWith('✖ Failed to revert to original state!'); + expect(logger.warn).toHaveBeenCalledWith(patchMessage); + expect(logger.warn).not.toHaveBeenCalledWith(restoredMessage); + const patchPath = logger.warn.mock.calls.at(-1)![0] as string; + expect(fs.readFileSync(patchPath, 'utf8')).toContain('+unstaged'); + expect(git('stash', 'list')).toContain('lint-staged automatic backup'); + git('stash', 'apply', '--index'); + expectOriginalState(); + }); + + it('preserves both diffs without recovery warnings when tasks succeed', async () => { + const logger = createLogger(); + const success = await lintStaged( + { cwd, config: { '*.txt': 'node --version' }, color: false }, + createStagedLogger(logger), + ); + + expect(success).toBe(true); + expect(logger.warn).not.toHaveBeenCalled(); + expectOriginalState(); + }); +}); diff --git a/patches/lint-staged@17.6.0.patch b/patches/lint-staged@17.6.0.patch new file mode 100644 index 0000000000..41f66e3a44 --- /dev/null +++ b/patches/lint-staged@17.6.0.patch @@ -0,0 +1,43 @@ +diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js +index 0d0fad3e894dd4dc0574168deff118928da0c957..de869658c27a64ff3bc543ba037970fb50fd6b9a 100644 +--- a/lib/gitWorkflow.js ++++ b/lib/gitWorkflow.js +@@ -535,6 +535,7 @@ export class GitWorkflow { + await this.execGit(['stash', 'apply', '--quiet', '--index', ctx.backupHash]) + await this.restoreMergeStatus(ctx) + await removeDir(this.hiddenPath) ++ ctx.originalStateRestored = true + this.logger.log(`${figures.done()} Done reverting to original state!`) + } catch (error) { + ctx.errors.add(GitError) +diff --git a/lib/index.js b/lib/index.js +index 9256d5cdf57ec59605215b998097a50e49c0ab9e..f9b3135cb6a8f99ea6ad7a15b2d2383cdff6ae68 100644 +--- a/lib/index.js ++++ b/lib/index.js +@@ -109,8 +109,12 @@ const lintStaged = async (options = {}, logger = console) => { + logger.warn(preventedTaskModifications() + '\n') + logger.warn(restoreStashExample(ctx.backupHash)) + } else if (ctx.errors.has(RestoreUnstagedChangesError)) { +- logger.warn(unstagedChangesBackupStashLocation()) +- logger.warn(ctx.unstagedPatch) ++ if (ctx.originalStateRestored) { ++ logger.warn('Original state was restored from the backup stash.') ++ } else { ++ logger.warn(unstagedChangesBackupStashLocation()) ++ logger.warn(ctx.unstagedPatch) ++ } + } else if (ctx.errors.has(GitError) || cleanupSkipped(ctx)) { + logger.error(gitError()) + if (ctx.shouldBackup) { +diff --git a/lib/state.js b/lib/state.js +index 3ae30ff4a882a52c87c698b59704f6ef8fdf5064..b76620e400b17a0ea552c3674eb52b4a47a2e393 100644 +--- a/lib/state.js ++++ b/lib/state.js +@@ -23,6 +23,7 @@ export const getInitialState = ({ + shouldFailOnChanges: failOnChanges, + hasFilesToHide: null, + output: [], ++ originalStateRestored: false, + quiet, + runId: crypto.randomBytes(4).toString('hex'), + shouldBackup: null, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c594da1bbc..b9fdc77a97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -379,6 +379,7 @@ packageExtensionsChecksum: sha256-If0sPrN3HV7D+EgeoSZUm8CYcOJOyo0gWobIh2WtTMM= patchedDependencies: chokidar@3.6.0: e17269188c54412478078f7ac7877c1d5a475a2f2438328e3a7e725513ccbf1f dotenv-expand@13.0.0: 49330a663821151418e003e822a82a6a61d2f0f8a6e3cab00c1c94815a112889 + lint-staged@17.6.0: 40e79bcabf3a0396d39ff56f25159a085a5737f34255d4dfc98201a6db81da7f sirv@3.0.2: c07c56eb72faea34341d465cde2314e89db472106ed378181e3447893af6bf95 importers: @@ -526,7 +527,7 @@ importers: version: 3.3.1 lint-staged: specifier: 'catalog:' - version: 17.6.0 + version: 17.6.0(patch_hash=40e79bcabf3a0396d39ff56f25159a085a5737f34255d4dfc98201a6db81da7f) minimatch: specifier: 'catalog:' version: 11.0.5 @@ -920,7 +921,7 @@ importers: version: 9.6.1 lint-staged: specifier: ^17.6.0 - version: 17.6.0 + version: 17.6.0(patch_hash=40e79bcabf3a0396d39ff56f25159a085a5737f34255d4dfc98201a6db81da7f) oxfmt: specifier: ^0.77.0 version: 0.77.0(vite-plus@packages+cli) @@ -10891,7 +10892,7 @@ snapshots: lilconfig@3.1.3: {} - lint-staged@17.6.0: + lint-staged@17.6.0(patch_hash=40e79bcabf3a0396d39ff56f25159a085a5737f34255d4dfc98201a6db81da7f): dependencies: picomatch: 4.0.7 string-argv: 0.3.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 541696f236..6b9d6c7490 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -177,6 +177,7 @@ packageExtensions: patchedDependencies: chokidar@3.6.0: vite/patches/chokidar@3.6.0.patch dotenv-expand@13.0.0: vite/patches/dotenv-expand@13.0.0.patch + lint-staged@17.6.0: patches/lint-staged@17.6.0.patch sirv@3.0.2: vite/patches/sirv@3.0.2.patch peerDependencyRules: allowAny: