Skip to content
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ Only lint the changes you've staged for an upcoming commit.
}
```

#### `"plugin:diff/committed"`

Only lint the changes you've committed, for running in a pre-push hook. You should set `ESLINT_PLUGIN_DIFF_COMMIT` in your pre-push hook for this to be useful.

```json
{
"extends": ["plugin:diff/committed"]
}
```

## CI Setup

To lint all the changes of a pull-request, you only have to set
Expand Down
18 changes: 18 additions & 0 deletions src/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ exports[`plugin should match expected export 1`] = `
"diff",
],
},
"committed": {
"overrides": [
{
"files": [
"*",
],
"processor": "diff/committed",
},
],
"plugins": [
"diff",
],
},
"diff": {
"overrides": [
{
Expand Down Expand Up @@ -51,6 +64,11 @@ exports[`plugin should match expected export 2`] = `
"preprocess": [Function],
"supportsAutofix": true,
},
"committed": {
"postprocess": [Function],
"preprocess": [Function],
"supportsAutofix": false,
},
"diff": {
"postprocess": [Function],
"preprocess": [Function],
Expand Down
46 changes: 21 additions & 25 deletions src/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ describe("getRangesForDiff", () => {

describe("getDiffForFile", () => {
it("should get the staged diff of a file", () => {
mockedChildProcess.execFileSync.mockReturnValueOnce(Buffer.from(hunks));
mockedChildProcess.execFileSync.mockReturnValueOnce(hunks);
process.env.ESLINT_PLUGIN_DIFF_COMMIT = "1234567";

const diffFromFile = getDiffForFile("./mockfile.js", true);
const diffFromFile = getDiffForFile("./mockfile.js", "staged");

const expectedCommand = "git";
const expectedArgs =
"diff --diff-algorithm=histogram --diff-filter=ACM --find-renames=100% --no-ext-diff --relative --staged --unified=0 1234567";
"diff-index --diff-algorithm=histogram --diff-filter=ACM --find-renames=100% --no-ext-diff --relative --cached --unified=0 1234567";

const lastCall = mockedChildProcess.execFileSync.mock.calls.at(-1);
const [command, argsIncludingFile = []] = lastCall ?? [""];
Expand All @@ -68,14 +68,14 @@ describe("getDiffForFile", () => {
});

it("should work when using staged = false", () => {
mockedChildProcess.execFileSync.mockReturnValueOnce(Buffer.from(hunks));
mockedChildProcess.execFileSync.mockReturnValueOnce(hunks);
process.env.ESLINT_PLUGIN_DIFF_COMMIT = "1234567";

const diffFromFile = getDiffForFile("./mockfile.js", false);
const diffFromFile = getDiffForFile("./mockfile.js", "working");

const expectedCommand = "git";
const expectedArgs =
"diff --diff-algorithm=histogram --diff-filter=ACM --find-renames=100% --no-ext-diff --relative --unified=0 1234567";
"diff-index --diff-algorithm=histogram --diff-filter=ACM --find-renames=100% --no-ext-diff --relative --unified=0 1234567";

const lastCall = mockedChildProcess.execFileSync.mock.calls.at(-1);
const [command, argsIncludingFile = []] = lastCall ?? [""];
Expand All @@ -88,14 +88,14 @@ describe("getDiffForFile", () => {
});

it("should use HEAD when no commit was defined", () => {
mockedChildProcess.execFileSync.mockReturnValueOnce(Buffer.from(hunks));
mockedChildProcess.execFileSync.mockReturnValueOnce(hunks);
process.env.ESLINT_PLUGIN_DIFF_COMMIT = undefined;

const diffFromFile = getDiffForFile("./mockfile.js", false);
const diffFromFile = getDiffForFile("./mockfile.js", "working");

const expectedCommand = "git";
const expectedArgs =
"diff --diff-algorithm=histogram --diff-filter=ACM --find-renames=100% --no-ext-diff --relative --unified=0 HEAD";
"diff-index --diff-algorithm=histogram --diff-filter=ACM --find-renames=100% --no-ext-diff --relative --unified=0 HEAD";

const lastCall = mockedChildProcess.execFileSync.mock.calls.at(-1);
const [command, argsIncludingFile = []] = lastCall ?? [""];
Expand All @@ -120,7 +120,7 @@ describe("hasCleanIndex", () => {

it("returns true otherwise", () => {
jest.mock("child_process").resetAllMocks();
mockedChildProcess.execFileSync.mockReturnValue(Buffer.from(""));
mockedChildProcess.execFileSync.mockReturnValue("");
expect(hasCleanIndex("")).toEqual(true);
expect(mockedChildProcess.execFileSync).toHaveBeenCalled();
});
Expand All @@ -129,11 +129,9 @@ describe("hasCleanIndex", () => {
describe("getDiffFileList", () => {
it("should get the list of staged files", () => {
jest.mock("child_process").resetAllMocks();
mockedChildProcess.execFileSync.mockReturnValueOnce(
Buffer.from(diffFileList)
);
mockedChildProcess.execFileSync.mockReturnValueOnce(diffFileList);
expect(mockedChildProcess.execFileSync).toHaveBeenCalledTimes(0);
const fileListA = getDiffFileList(false);
const fileListA = getDiffFileList("working");

expect(mockedChildProcess.execFileSync).toHaveBeenCalledTimes(1);
expect(fileListA).toEqual(
Expand All @@ -145,18 +143,13 @@ describe("getDiffFileList", () => {
describe("getUntrackedFileList", () => {
it("should get the list of untracked files", () => {
jest.mock("child_process").resetAllMocks();
mockedChildProcess.execFileSync.mockReturnValueOnce(
Buffer.from(diffFileList)
);
mockedChildProcess.execFileSync.mockReturnValueOnce(diffFileList);
expect(mockedChildProcess.execFileSync).toHaveBeenCalledTimes(0);
const fileListA = getUntrackedFileList(false);
const fileListA = getUntrackedFileList("working");
expect(mockedChildProcess.execFileSync).toHaveBeenCalledTimes(1);

mockedChildProcess.execFileSync.mockReturnValueOnce(
Buffer.from(diffFileList)
);
const staged = false;
const fileListB = getUntrackedFileList(staged);
mockedChildProcess.execFileSync.mockReturnValueOnce(diffFileList);
const fileListB = getUntrackedFileList("working");
// `getUntrackedFileList` uses a cache, so the number of calls to
// `execFileSync` will not have increased.
expect(mockedChildProcess.execFileSync).toHaveBeenCalledTimes(1);
Expand All @@ -168,7 +161,10 @@ describe("getUntrackedFileList", () => {
});

it("should not get a list when looking when using staged", () => {
const staged = true;
expect(getUntrackedFileList(staged)).toEqual([]);
expect(getUntrackedFileList("staged")).toEqual([]);
});

it("should not get a list when looking when using committed", () => {
expect(getUntrackedFileList("committed")).toEqual([]);
});
});
69 changes: 48 additions & 21 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,56 @@ import * as child_process from "child_process";
import { resolve } from "path";
import { Range } from "./Range";

export type DiffType = "staged" | "committed" | "working";

const COMMAND = "git";
const OPTIONS = { maxBuffer: 1024 * 1024 * 100 };
const OPTIONS = { encoding: "utf8" as const, maxBuffer: 1024 * 1024 * 100 };

const getDiffForFile = (filePath: string, staged: boolean): string => {
const getDiffForFile = (filePath: string, diffType: DiffType): string => {
const args = [
"diff",
diffType === "committed" ? "diff-tree" : "diff-index",
"--diff-algorithm=histogram",
"--diff-filter=ACM",
"--find-renames=100%",
"--no-ext-diff",
"--relative",
staged && "--staged",
diffType === "staged" && "--cached",
"--unified=0",
process.env.ESLINT_PLUGIN_DIFF_COMMIT ?? "HEAD",
diffType === "committed" && "HEAD",
"--",
resolve(filePath),
].reduce<string[]>(
(acc, cur) => (typeof cur === "string" ? [...acc, cur] : acc),
[]
);
].filter((cur): cur is string => typeof cur === "string");

return child_process.execFileSync(COMMAND, args, OPTIONS).toString();
return child_process.execFileSync(COMMAND, args, OPTIONS);
};

const getDiffFileList = (staged: boolean): string[] => {
const getDiffFileList = (diffType: DiffType): string[] => {
const args = [
"diff",
diffType === "committed" ? "diff-tree" : "diff-index",
"--diff-algorithm=histogram",
"--diff-filter=ACM",
"--find-renames=100%",
"--name-only",
"--no-ext-diff",
"--relative",
staged && "--staged",
diffType === "staged" && "--cached",
diffType === "committed" && "-r",
process.env.ESLINT_PLUGIN_DIFF_COMMIT ?? "HEAD",
diffType === "committed" && "HEAD",
"--",
].reduce<string[]>(
(acc, cur) => (typeof cur === "string" ? [...acc, cur] : acc),
[]
);
].filter((cur): cur is string => typeof cur === "string");

return child_process
.execFileSync(COMMAND, args, OPTIONS)
.toString()
.trim()
.split("\n")
.map((filePath) => resolve(filePath));
};

const hasCleanIndex = (filePath: string): boolean => {
const args = [
"diff",
"diff-files",
"--no-ext-diff",
"--quiet",
"--relative",
Expand All @@ -71,6 +69,27 @@ const hasCleanIndex = (filePath: string): boolean => {
return true;
};

const hasCleanTree = (filePath: string): boolean => {
const args = [
"diff-index",
"--no-ext-diff",
"--quiet",
"--relative",
"--unified=0",
"HEAD",
"--",
resolve(filePath),
];

try {
child_process.execFileSync(COMMAND, args, OPTIONS);
} catch (err: unknown) {
return false;
}

return true;
};

const fetchFromOrigin = (branch: string) => {
const args = ["fetch", "--quiet", "origin", branch];

Expand All @@ -79,10 +98,10 @@ const fetchFromOrigin = (branch: string) => {

let untrackedFileListCache: string[] | undefined;
const getUntrackedFileList = (
staged: boolean,
diffType: DiffType,
shouldRefresh = false
): string[] => {
if (staged) {
if (diffType !== "working") {
return [];
}

Expand All @@ -91,7 +110,6 @@ const getUntrackedFileList = (

untrackedFileListCache = child_process
.execFileSync(COMMAND, args, OPTIONS)
.toString()
.trim()
.split("\n")
.map((filePath) => resolve(filePath));
Expand Down Expand Up @@ -155,11 +173,20 @@ const getRangesForDiff = (diff: string): Range[] =>
return [...ranges, range];
}, []);

const readFileFromGit = (filePath: string) => {
const getBlob = ["ls-tree", "--object-only", "HEAD", resolve(filePath)];
const blob = child_process.execFileSync(COMMAND, getBlob, OPTIONS).trim();
const catFile = ["cat-file", "blob", blob];
return child_process.execFileSync(COMMAND, catFile, OPTIONS);
};

export {
fetchFromOrigin,
getDiffFileList,
getDiffForFile,
getRangesForDiff,
getUntrackedFileList,
hasCleanIndex,
hasCleanTree,
readFileFromGit,
};
4 changes: 1 addition & 3 deletions src/index-ci.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import * as child_process from "child_process";

jest.mock("child_process");
const mockedChildProcess = jest.mocked(child_process, { shallow: true });
mockedChildProcess.execFileSync.mockReturnValue(
Buffer.from("line1\nline2\nline3")
);
mockedChildProcess.execFileSync.mockReturnValue("line1\nline2\nline3");

import "./index";

Expand Down
4 changes: 1 addition & 3 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import * as child_process from "child_process";

jest.mock("child_process");
const mockedChildProcess = jest.mocked(child_process, { shallow: true });
mockedChildProcess.execFileSync.mockReturnValue(
Buffer.from("line1\nline2\nline3")
);
mockedChildProcess.execFileSync.mockReturnValue("line1\nline2\nline3");

import { configs, processors } from "./index";

Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import {
diffConfig,
staged,
stagedConfig,
committed,
committedConfig,
} from "./processors";

const configs = {
ci: ciConfig,
diff: diffConfig,
staged: stagedConfig,
committed: committedConfig,
};
const processors = { ci, diff, staged };
const processors = { ci, diff, staged, committed };

module.exports = { configs, processors };

Expand Down
Loading