diff --git a/README.md b/README.md index 78811a782..d4f12cb4f 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ 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 + --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 -q, --quiet disable lint-staged’s own console output (default: false) -r, --relative pass relative filepaths to tasks (default: false) @@ -111,6 +113,8 @@ Options: - **`--debug`**: Run in debug mode. When set, it does the following: - uses [debug](https://github.com/visionmedia/debug) internally to log additional information about staged files, commands being executed, location of binaries, etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`. - uses [`verbose` renderer](https://github.com/SamVerschueren/listr-verbose-renderer) for `listr`; this causes serial, uncoloured output to the terminal, instead of the default (beautified, dynamic) output. +- **`--diff`**: By default linters are filtered against all files staged in git, generated from `git diff --staged`. This option allows you to override the `--staged` flag with arbitrary revisions. For example to get a list of changed files between two branches, use `--diff="branch1...branch2"`. You can also read more from about [`git diff`](https://git-scm.com/docs/git-diff) and [`gitrevisions`](https://git-scm.com/docs/gitrevisions). +- **`--max-arg-length`**: long commands (a lot of files) are automatically split into multiple chunks when it detects the current shell cannot handle them. Use this flag to override the maximum length of the generated command string. - **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit. - **`--quiet`**: Supress all CLI output, except from tasks. - **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`. @@ -596,7 +600,7 @@ const success = await lintStaged({ relative: false, shell: false, stash: true, - verbose: false + verbose: false, }) ``` @@ -713,6 +717,30 @@ Example repo: [sudo-suhas/lint-staged-django-react-demo](https://github.com/sudo +### Can I run `lint-staged` in CI, or when there are no staged files? + +
+ Click to expand + +Lint-staged will by default run against files staged in git, and should be run during the git pre-commit hook, for example. It's also possible to override this default behaviour and run against files in a specific diff, for example +all changed files between two different branches. If you want to run _lint-staged_ in the CI, maybe you can set it up to compare the branch in a _Pull Request_/_Merge Request_ to the target branch. + +Try out the `git diff` command until you are satisfied with the result, for example: + +``` +git diff --diff-filter=ACMR --name-only master...my-branch +``` + +This will print a list of _added_, _changed_, _modified_, and _renamed_ files between `master` and `my-branch`. + +You can then run lint-staged against the same files with: + +``` +npx lint-staged --diff="master...my-branch" +``` + +
+ ### Can I use `lint-staged` with `ng lint`
diff --git a/bin/lint-staged.js b/bin/lint-staged.js index a2c3dd508..66fa7fbe6 100755 --- a/bin/lint-staged.js +++ b/bin/lint-staged.js @@ -42,6 +42,11 @@ cli.option('--cwd [path]', 'run all tasks in specific directory, instead of the cli.option('-d, --debug', 'print additional debug information', false) +cli.option( + '--diff [string]', + 'override the default "--staged" flag of "git diff" to get list of files' +) + cli.option('--max-arg-length [number]', 'maximum length of the command-line argument string', 0) /** @@ -86,6 +91,7 @@ const options = { configPath: cliOptions.config, cwd: cliOptions.cwd, debug: !!cliOptions.debug, + diff: cliOptions.diff, maxArgLength: cliOptions.maxArgLength || undefined, quiet: !!cliOptions.quiet, relative: !!cliOptions.relative, diff --git a/lib/getStagedFiles.js b/lib/getStagedFiles.js index 363e07a1d..7ab9c0265 100644 --- a/lib/getStagedFiles.js +++ b/lib/getStagedFiles.js @@ -5,16 +5,18 @@ import normalize from 'normalize-path' import { execGit } from './execGit.js' import { parseGitZOutput } from './parseGitZOutput.js' -export const getStagedFiles = async ({ cwd = process.cwd() } = {}) => { - try { - // 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'], { - cwd, - }) +/** + * Docs for --diff-filter option: @see https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203 + * Docs for -z option: @see https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--z + */ +const ARGS = ['diff', '--diff-filter=ACMR', '--name-only', '-z'] +export const getStagedFiles = async ({ cwd = process.cwd(), diff } = {}) => { + try { + /** Use `--diff branch1...branch2` or `--diff="branch1 branch2", or fall back to default staged files */ + const diffArgs = diff !== undefined ? diff.trim().split(' ') : ['--staged'] + const lines = await execGit([...ARGS, ...diffArgs], { cwd }) if (!lines) return [] - return parseGitZOutput(lines).map((file) => normalize(path.resolve(cwd, file))) } catch { return null diff --git a/lib/index.js b/lib/index.js index e15616d49..f815743db 100644 --- a/lib/index.js +++ b/lib/index.js @@ -49,6 +49,7 @@ const getMaxArgLength = () => { * @param {string} [options.configPath] - Path to configuration file * @param {Object} [options.cwd] - Current working directory * @param {boolean} [options.debug] - Enable debug mode + * @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files * @param {number} [options.maxArgLength] - Maximum argument string length * @param {boolean} [options.quiet] - Disable lint-staged’s own console output * @param {boolean} [options.relative] - Pass relative filepaths to tasks @@ -67,6 +68,7 @@ const lintStaged = async ( configPath, cwd, debug = false, + diff, maxArgLength = getMaxArgLength() / 2, quiet = false, relative = false, @@ -91,6 +93,7 @@ const lintStaged = async ( configPath, cwd, debug, + diff, maxArgLength, quiet, relative, diff --git a/lib/runAll.js b/lib/runAll.js index b9c54f7da..e2e682bf6 100644 --- a/lib/runAll.js +++ b/lib/runAll.js @@ -52,6 +52,7 @@ const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ct * @param {string} [options.configPath] - Explicit path to a config file * @param {string} [options.cwd] - Current working directory * @param {boolean} [options.debug] - Enable debug mode + * @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files * @param {number} [options.maxArgLength] - Maximum argument string length * @param {boolean} [options.quiet] - Disable lint-staged’s own console output * @param {boolean} [options.relative] - Pass relative filepaths to tasks @@ -69,6 +70,7 @@ export const runAll = async ( configPath, cwd, debug = false, + diff, maxArgLength, quiet = false, relative = false, @@ -106,7 +108,7 @@ export const runAll = async ( logger.warn(skippingBackup(hasInitialCommit)) } - const files = await getStagedFiles({ cwd: gitDir }) + const files = await getStagedFiles({ cwd: gitDir, diff }) if (!files) { if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES) ctx.errors.add(GetStagedFilesError) diff --git a/test/getStagedFiles.spec.js b/test/getStagedFiles.spec.js index 15866460d..285a6d187 100644 --- a/test/getStagedFiles.spec.js +++ b/test/getStagedFiles.spec.js @@ -11,11 +11,20 @@ jest.mock('../lib/execGit') const normalizePath = (input) => normalize(path.resolve('/', input)) describe('getStagedFiles', () => { + afterEach(() => { + jest.clearAllMocks() + }) + it('should return array of file names', async () => { execGit.mockImplementationOnce(async () => 'foo.js\u0000bar.js\u0000') const staged = await getStagedFiles({ cwd: '/' }) // Windows filepaths expect(staged).toEqual([normalizePath('/foo.js'), normalizePath('/bar.js')]) + + expect(execGit).toHaveBeenLastCalledWith( + ['diff', '--diff-filter=ACMR', '--name-only', '-z', '--staged'], + { cwd: '/' } + ) }) it('should return empty array when no staged files', async () => { @@ -31,4 +40,28 @@ describe('getStagedFiles', () => { const staged = await getStagedFiles({}) expect(staged).toEqual(null) }) + + it('should support overriding diff trees with ...', async () => { + execGit.mockImplementationOnce(async () => 'foo.js\u0000bar.js\u0000') + const staged = await getStagedFiles({ cwd: '/', diff: 'master...my-branch' }) + // Windows filepaths + expect(staged).toEqual([normalizePath('/foo.js'), normalizePath('/bar.js')]) + + expect(execGit).toHaveBeenLastCalledWith( + ['diff', '--diff-filter=ACMR', '--name-only', '-z', 'master...my-branch'], + { cwd: '/' } + ) + }) + + it('should support overriding diff trees with multiple args', async () => { + execGit.mockImplementationOnce(async () => 'foo.js\u0000bar.js\u0000') + const staged = await getStagedFiles({ cwd: '/', diff: 'master my-branch' }) + // Windows filepaths + expect(staged).toEqual([normalizePath('/foo.js'), normalizePath('/bar.js')]) + + expect(execGit).toHaveBeenLastCalledWith( + ['diff', '--diff-filter=ACMR', '--name-only', '-z', 'master', 'my-branch'], + { cwd: '/' } + ) + }) }) diff --git a/test/integration.test.js b/test/integration.test.js index b90a35c8e..4fc81abe6 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1309,6 +1309,32 @@ describe('lint-staged', () => { // File outside deeper/ was not fixed expect(await readFile('file.js')).toEqual(testJsFileUgly) }) + + it('should support overriding file list using --diff', async () => { + // Commit ugly file + await appendFile('test.js', testJsFileUgly) + await execGit(['add', 'test.js']) + await execGit(['commit', '-m', 'ugly'], { cwd }) + + const hashes = (await execGit(['log', '--format=format:%H'])).trim().split('\n') + expect(hashes).toHaveLength(2) + + // Run lint-staged with `--diff` between the two commits. + // Nothing is staged at this point, so don't rung `gitCommit` + const passed = await lintStaged({ + config: { '*.js': 'prettier --list-different' }, + cwd, + debug: true, + diff: `${hashes[1]}...${hashes[0]}`, + stash: false, + }) + + // Lint-staged failed because commit diff contains ugly file + expect(passed).toEqual(false) + + expect(console.printHistory()).toMatch('prettier --list-different:') + expect(console.printHistory()).toMatch('test.js') + }) }) describe('lintStaged', () => {