Skip to content

Add new feature to lint against committed files #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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