Skip to content

Commit

Permalink
Merge pull request #1493 from lint-staged/help-messages
Browse files Browse the repository at this point in the history
Add more help messages about "git stash"
  • Loading branch information
iiroj authored Dec 27, 2024
2 parents 1d39241 + 22fe89d commit 6c9ab40
Show file tree
Hide file tree
Showing 13 changed files with 102 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/polite-plums-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'lint-staged': minor
---

Display "Backed up original state in git stash" instead of just "Preparing lint-staged..." when backup stash is enabled (by default)
5 changes: 5 additions & 0 deletions .changeset/smart-adults-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'lint-staged': minor
---

Add more help messages about restoring from git stash
5 changes: 5 additions & 0 deletions .changeset/soft-ties-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'lint-staged': minor
---

Add unique hash to each backup stash message; this refers to the dangling commit created with "git stash create"
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ npm install --save-dev lint-staged # requires further setup
```
$ git commit
Preparing lint-staged...
Backed up original state in git stash (5bda95f)
❯ Running tasks for staged files...
❯ packages/frontend/.lintstagedrc.json — 1 file
↓ *.js — no files [SKIPPED]
Expand Down Expand Up @@ -70,6 +70,9 @@ Now change a few files, `git add` or `git add --patch` some of them to your comm

See [examples](#examples) and [configuration](#configuration) for more information.

> [!CAUTION]
> _Lint-staged_ runs `git` operations affecting the files in your repository. By default _lint-staged_ creates a `git stash` as a backup of the original state before running any configured tasks to help prevent data loss.
## Changelog

See [Releases](https://github.com/okonet/lint-staged/releases).
Expand Down Expand Up @@ -117,19 +120,26 @@ Options:
-c, --config [path] path to configuration file, or - to read from stdin
--cwd [path] run all tasks in specific directory, instead of the current
-d, --debug print additional debug information (default: false)
--diff [string] override the default "--staged" flag of "git diff" to get list of files. Implies
"--no-stash".
--diff-filter [string] override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
--diff [string] override the default "--staged" flag of "git diff" to get list of files.
Implies "--no-stash".
--diff-filter [string] override the default "--diff-filter=ACMR" flag of "git diff" to get list of
files
--max-arg-length [number] maximum length of the command-line argument string (default: 0)
--no-stash disable the backup stash, and do not revert in case of errors. Implies
"--no-hide-partially-staged".
--no-hide-partially-staged disable hiding unstaged changes from partially staged files
-q, --quiet disable lint-staged’s own console output (default: false)
-r, --relative pass relative filepaths to tasks (default: false)
-x, --shell [path] skip parsing of tasks for better shell support (default: false)
-v, --verbose show task output even when tasks succeed; by default only failed output is shown
(default: false)
-v, --verbose show task output even when tasks succeed; by default only failed output is
shown (default: false)
-h, --help display help for command
Any lost modifications can be restored from a git stash:
> git stash list
stash@{0}: automatic lint-staged backup
> git stash apply --index stash@{0}
```

- **`--allow-empty`**: By default, when linter tasks undo all staged changes, lint-staged will exit with an error and abort the commit. Use this flag to allow creating empty git commits.
Expand Down
4 changes: 3 additions & 1 deletion bin/lint-staged.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Option, program } from 'commander'
import debug from 'debug'

import lintStaged from '../lib/index.js'
import { CONFIG_STDIN_ERROR } from '../lib/messages.js'
import { CONFIG_STDIN_ERROR, RESTORE_STASH_EXAMPLE } from '../lib/messages.js'
import { readStdin } from '../lib/readStdin.js'

// Force colors for packages that depend on https://www.npmjs.com/package/supports-color
Expand Down Expand Up @@ -103,6 +103,8 @@ cli.option(
false
)

cli.addHelpText('afterAll', '\n' + RESTORE_STASH_EXAMPLE)

const cliOptions = cli.parse(process.argv).opts()

if (cliOptions.debug) {
Expand Down
25 changes: 19 additions & 6 deletions lib/gitWorkflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ export class GitWorkflow {
*/
async getBackupStash(ctx) {
const stashes = await this.execGit(['stash', 'list'])
const index = stashes.split('\n').findIndex((line) => line.includes(STASH))

const index = stashes
.split('\n')
.findIndex((line) => line.includes(STASH) && line.includes(ctx.backupHash))

if (index === -1) {
ctx.errors.add(GetBackupStashError)
throw new Error('lint-staged automatic backup is missing!')
Expand Down Expand Up @@ -190,9 +194,9 @@ export class GitWorkflow {
/**
* Create a diff of partially staged files and backup stash if enabled.
*/
async prepare(ctx) {
async prepare(ctx, task) {
try {
debugLog('Backing up original state...')
debugLog(task.title)

// Get a list of files with bot staged and unstaged changes.
// Unstaged changes to these files should be hidden before the tasks run.
Expand Down Expand Up @@ -223,10 +227,19 @@ export class GitWorkflow {
// Save stash of all staged files.
// The `stash create` command creates a dangling commit without removing any files,
// and `stash store` saves it as an actual stash.
const hash = await this.execGit(['stash', 'create'])
await this.execGit(['stash', 'store', '--quiet', '--message', STASH, hash])
const stashHash = await this.execGit(['stash', 'create'])
ctx.backupHash = await this.execGit(['rev-parse', '--short', stashHash])
await this.execGit([
'stash',
'store',
'--quiet',
'--message',
`${STASH} (${ctx.backupHash})`,
ctx.backupHash,
])

debugLog('Done backing up original state!')
task.title = `Backed up original state in git stash (${ctx.backupHash})`
debugLog(task.title)
} catch (error) {
handleError(error, ctx)
}
Expand Down
8 changes: 6 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from './messages.js'
import { printTaskOutput } from './printTaskOutput.js'
import { runAll } from './runAll.js'
import { cleanupSkipped } from './state.js'
import {
ApplyEmptyCommitError,
ConfigNotFoundError,
Expand Down Expand Up @@ -123,11 +124,14 @@ const lintStaged = async (
logger.error(NO_CONFIGURATION)
} else if (ctx.errors.has(ApplyEmptyCommitError)) {
logger.warn(PREVENTED_EMPTY_COMMIT)
} else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
} else if (
(ctx.errors.has(GitError) || cleanupSkipped(ctx)) &&
!ctx.errors.has(GetBackupStashError)
) {
logger.error(GIT_ERROR)
if (ctx.shouldBackup) {
// No sense to show this if the backup stash itself is missing.
logger.error(RESTORE_STASH_EXAMPLE)
logger.error(RESTORE_STASH_EXAMPLE + '\n')
}
}

Expand Down
9 changes: 4 additions & 5 deletions lib/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,11 @@ export const PREVENTED_EMPTY_COMMIT = `
Use the --allow-empty option to continue, or check your task configuration`)}
`

export const RESTORE_STASH_EXAMPLE = ` Any lost modifications can be restored from a git stash:
export const RESTORE_STASH_EXAMPLE = `Any lost modifications can be restored from a git stash:
> git stash list
stash@{0}: automatic lint-staged backup
> git stash apply --index stash@{0}
`
> git stash list
stash@{0}: automatic lint-staged backup
> git stash apply --index stash@{0}`

export const CONFIG_STDIN_ERROR = chalk.redBright(`${error} Failed to read config from stdin.`)

Expand Down
4 changes: 2 additions & 2 deletions lib/runAll.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@ export const runAll = async (
const runner = new Listr(
[
{
title: 'Preparing lint-staged...',
task: (ctx) => git.prepare(ctx),
title: ctx.shouldBackup ? 'Backing up original state...' : 'Preparing lint-staged...',
task: (ctx, task) => git.prepare(ctx, task),
},
{
title: 'Hiding unstaged changes to partially staged files...',
Expand Down
9 changes: 4 additions & 5 deletions lib/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
export const getInitialState = ({ quiet = false } = {}) => ({
hasPartiallyStagedFiles: null,
shouldBackup: null,
backupHash: null,
shouldHidePartiallyStaged: true,
errors: new Set([]),
events: new EventEmitter(),
Expand Down Expand Up @@ -40,6 +41,7 @@ export const restoreUnstagedChangesSkipped = (ctx) => {
if (ctx.errors.has(GitError)) {
return GIT_ERROR
}

// Should be skipped when tasks fail
if (ctx.errors.has(TaskError)) {
return TASK_ERROR
Expand Down Expand Up @@ -67,13 +69,10 @@ export const cleanupEnabled = (ctx) => ctx.shouldBackup

export const cleanupSkipped = (ctx) => {
// Should be skipped in case of unknown git errors
if (
ctx.errors.has(GitError) &&
!ctx.errors.has(ApplyEmptyCommitError) &&
!ctx.errors.has(RestoreUnstagedChangesError)
) {
if (restoreOriginalStateSkipped(ctx)) {
return GIT_ERROR
}

// Should be skipped when reverting to original state fails
if (ctx.errors.has(RestoreOriginalStateError)) {
return GIT_ERROR
Expand Down
4 changes: 1 addition & 3 deletions test/integration/git-lock-file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ describe('lint-staged', () => {
await removeFile(`.git/index.lock`)

// Luckily there is a stash
expect(await execGit(['stash', 'list'])).toMatchInlineSnapshot(
`"stash@{0}: lint-staged automatic backup"`
)
expect(await execGit(['stash', 'list'])).toMatch('stash@{0}: lint-staged automatic backup')
await execGit(['reset', '--hard'])
await execGit(['stash', 'pop', '--index'])

Expand Down
6 changes: 4 additions & 2 deletions test/unit/getBackupStash.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ describe('gitWorkflow', () => {
it('should throw when stash not found even when other stashes are', async () => {
const gitWorkflow = new GitWorkflow(options)
const ctx = getInitialState()
ctx.backupHash = 'not-found'

execGit.mockResolvedValueOnce('stash@{0}: some random stuff')
execGit.mockResolvedValueOnce(`stash@{1}: ${STASH} (abc123)`)

await expect(gitWorkflow.getBackupStash(ctx)).rejects.toThrow(
'lint-staged automatic backup is missing!'
Expand All @@ -44,11 +45,12 @@ describe('gitWorkflow', () => {
it('should return ref to the backup stash', async () => {
const gitWorkflow = new GitWorkflow(options)
const ctx = getInitialState()
ctx.backupHash = 'abc123'

execGit.mockResolvedValueOnce(
[
'stash@{0}: some random stuff',
`stash@{1}: ${STASH}`,
`stash@{1}: ${STASH} (${ctx.backupHash})`,
'stash@{2}: other random stuff',
].join('\n')
)
Expand Down
33 changes: 28 additions & 5 deletions test/unit/index3.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('lintStaged', () => {
`)
})

it('should log error when a git operation failed', async () => {
it('should log error and git stash message when a git operation failed', async () => {
const ctx = getInitialState()
ctx.shouldBackup = true
ctx.errors.add(GitError)
Expand All @@ -70,15 +70,38 @@ describe('lintStaged', () => {
"
ERROR
✖ lint-staged failed due to a git error.
ERROR Any lost modifications can be restored from a git stash:
ERROR Any lost modifications can be restored from a git stash:
> git stash list
stash@{0}: automatic lint-staged backup
> git stash apply --index stash@{0}
> git stash list
stash@{0}: automatic lint-staged backup
> git stash apply --index stash@{0}
"
`)
})

it('should log error without git stash message when a git operation failed and backup disabled', async () => {
const ctx = getInitialState()
ctx.shouldBackup = false
ctx.errors.add(GitError)
runAll.mockImplementationOnce(async () => {
throw { ctx }
})

const logger = makeConsoleMock()

await expect(lintStaged({}, logger)).resolves.toEqual(false)

expect(logger.printHistory()).toMatchInlineSnapshot(`
"
ERROR
✖ lint-staged failed due to a git error."
`)

expect(logger.printHistory()).not.toMatch(
'Any lost modifications can be restored from a git stash'
)
})

it('should throw when context is malformed', async () => {
expect.assertions(2)

Expand Down

0 comments on commit 6c9ab40

Please sign in to comment.