mirror of
https://github.com/astral-sh/ruff-action.git
synced 2026-05-23 07:10:46 +00:00
Fix wildcard src input
This commit is contained in:
@@ -395,6 +395,34 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
CHECK_SHOULD_FAIL_OUTCOME: ${{ steps.check-should-fail.outcome }}
|
CHECK_SHOULD_FAIL_OUTCOME: ${{ steps.check-should-fail.outcome }}
|
||||||
|
test-glob-src:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-latest, windows-latest ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Use glob pattern in src
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
src: __tests__/fixtures/glob-project/**/*.py
|
||||||
|
|
||||||
|
test-glob-single-star-src:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-latest, windows-latest ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Use single-star glob pattern in src
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
src: __tests__/fixtures/glob-single-star-project/foo/bar/*.py
|
||||||
|
|
||||||
test-multiple-src:
|
test-multiple-src:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -479,6 +507,8 @@ jobs:
|
|||||||
- test-with-explicit-token
|
- test-with-explicit-token
|
||||||
- test-args
|
- test-args
|
||||||
- test-failure
|
- test-failure
|
||||||
|
- test-glob-src
|
||||||
|
- test-glob-single-star-src
|
||||||
- test-multiple-src
|
- test-multiple-src
|
||||||
- test-parent-directory-pyproject
|
- test-parent-directory-pyproject
|
||||||
- test-custom-manifest-file
|
- test-custom-manifest-file
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ anything `ruff` can (ex, fix).
|
|||||||
| `version-file` | The file to read the version from. See [Install a version from a specified version file](#install-a-version-from-a-specified-version-file) | None |
|
| `version-file` | The file to read the version from. See [Install a version from a specified version file](#install-a-version-from-a-specified-version-file) | None |
|
||||||
| `manifest-file` | URL to a custom Ruff manifest in the `astral-sh/versions` format. | None |
|
| `manifest-file` | URL to a custom Ruff manifest in the `astral-sh/versions` format. | None |
|
||||||
| `args` | The arguments to pass to the `ruff` command. See [Configuring Ruff] | `check` |
|
| `args` | The arguments to pass to the `ruff` command. See [Configuring Ruff] | `check` |
|
||||||
| `src` | The directory or single files to run `ruff` on. | [github.workspace] |
|
| `src` | Source path(s) to run `ruff` on. Supports glob patterns. | [github.workspace] |
|
||||||
| `checksum` | The sha256 checksum of the downloaded artifact. | None |
|
| `checksum` | The sha256 checksum of the downloaded artifact. | None |
|
||||||
| `github-token` | The GitHub token to use when downloading Ruff release artifacts from GitHub. | `GITHUB_TOKEN` |
|
| `github-token` | The GitHub token to use when downloading Ruff release artifacts from GitHub. | `GITHUB_TOKEN` |
|
||||||
|
|
||||||
@@ -56,6 +56,8 @@ By default, Ruff version metadata is resolved from the
|
|||||||
|
|
||||||
### Specify multiple files
|
### Specify multiple files
|
||||||
|
|
||||||
|
Separate multiple `src` values with whitespace.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: astral-sh/ruff-action@v3
|
- uses: astral-sh/ruff-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -64,6 +66,24 @@ By default, Ruff version metadata is resolved from the
|
|||||||
path/to/file2.py
|
path/to/file2.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Use glob patterns
|
||||||
|
|
||||||
|
Glob patterns (`*`, `?`, `[...]`, and `**`) are expanded by the action using
|
||||||
|
`@actions/glob`, so they work consistently across Linux, macOS, and Windows
|
||||||
|
runners. Hidden files and directories are skipped by default; target them
|
||||||
|
explicitly, for example with `.venv/**/*.py`, to include them.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: astral-sh/ruff-action@v3
|
||||||
|
with:
|
||||||
|
src: "src/**/*.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> When using multiple patterns, only the first is used to search for
|
||||||
|
> `pyproject.toml` to determine the Ruff version. Use the `version` input
|
||||||
|
> for explicit control.
|
||||||
|
|
||||||
### Use to install ruff
|
### Use to install ruff
|
||||||
|
|
||||||
This action adds ruff to the PATH, so you can use it in subsequent steps.
|
This action adds ruff to the PATH, so you can use it in subsequent steps.
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "pyython-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"ruff==0.6.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
print("hello")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
print("deeper")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
print("check me")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# This file should be ignored when using foo/bar/*.py glob
|
||||||
|
print("ignore me")
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "glob-single-star-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"ruff==0.6.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
debug: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
expandSourceInput,
|
||||||
|
getSourceBasePath,
|
||||||
|
getVersionSourceDirectory,
|
||||||
|
splitInput,
|
||||||
|
} = await import("../../src/utils/source-input");
|
||||||
|
const { findPyprojectToml } = await import("../../src/utils/pyproject-finder");
|
||||||
|
|
||||||
|
let tempDir: string | undefined;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (tempDir !== undefined) {
|
||||||
|
await fs.rm(tempDir, { force: true, recursive: true });
|
||||||
|
tempDir = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createTempProject(): Promise<string> {
|
||||||
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ruff-action-source-"));
|
||||||
|
await fs.mkdir(path.join(tempDir, "src", "package", "nested"), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
await fs.writeFile(path.join(tempDir, "src", "package", "__init__.py"), "");
|
||||||
|
await fs.writeFile(path.join(tempDir, "src", "package", "main.py"), "");
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tempDir, "src", "package", "nested", "mod.py"),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
await fs.writeFile(path.join(tempDir, "src", "package", "notes.txt"), "");
|
||||||
|
await fs.mkdir(path.join(tempDir, "src", "pkg[legacy]"), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
await fs.writeFile(path.join(tempDir, "src", "pkg[legacy]", "main.py"), "");
|
||||||
|
await fs.mkdir(path.join(tempDir, "src", "pkgl"), { recursive: true });
|
||||||
|
await fs.writeFile(path.join(tempDir, "src", "pkgl", "main.py"), "");
|
||||||
|
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("splitInput", () => {
|
||||||
|
it("splits whitespace-delimited inputs", () => {
|
||||||
|
expect(splitInput(" check --fix\n--diff ")).toEqual([
|
||||||
|
"check",
|
||||||
|
"--fix",
|
||||||
|
"--diff",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array for empty input", () => {
|
||||||
|
expect(splitInput(" \n\t ")).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("expandSourceInput", () => {
|
||||||
|
it("leaves non-glob sources unchanged", async () => {
|
||||||
|
await expect(expandSourceInput("src file.py")).resolves.toEqual([
|
||||||
|
"src",
|
||||||
|
"file.py",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("expands recursive globs without relying on shell behavior", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const pattern = path.join(projectDir, "src", "**", "*.py");
|
||||||
|
|
||||||
|
await expect(expandSourceInput(pattern)).resolves.toEqual([
|
||||||
|
path.join(projectDir, "src", "package", "__init__.py"),
|
||||||
|
path.join(projectDir, "src", "package", "main.py"),
|
||||||
|
path.join(projectDir, "src", "package", "nested", "mod.py"),
|
||||||
|
path.join(projectDir, "src", "pkg[legacy]", "main.py"),
|
||||||
|
path.join(projectDir, "src", "pkgl", "main.py"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves relative source patterns as relative paths", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.chdir(projectDir);
|
||||||
|
await expect(expandSourceInput("src/**/*.py")).resolves.toEqual([
|
||||||
|
path.join("src", "package", "__init__.py"),
|
||||||
|
path.join("src", "package", "main.py"),
|
||||||
|
path.join("src", "package", "nested", "mod.py"),
|
||||||
|
path.join("src", "pkg[legacy]", "main.py"),
|
||||||
|
path.join("src", "pkgl", "main.py"),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps relative root glob matches to . instead of an empty argument", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.chdir(projectDir);
|
||||||
|
for (const pattern of ["**", "**/", "./**"]) {
|
||||||
|
const sources = await expandSourceInput(pattern);
|
||||||
|
|
||||||
|
expect(sources).toContain(".");
|
||||||
|
expect(sources).not.toContain("");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("excludes hidden paths by default while allowing explicit hidden patterns", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
await fs.mkdir(path.join(projectDir, ".venv"));
|
||||||
|
await fs.writeFile(path.join(projectDir, ".venv", "ignored.py"), "");
|
||||||
|
await fs.mkdir(path.join(projectDir, ".tox"));
|
||||||
|
await fs.writeFile(path.join(projectDir, ".tox", "ignored.py"), "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.chdir(projectDir);
|
||||||
|
const defaultSources = await expandSourceInput("**/*.py");
|
||||||
|
|
||||||
|
expect(defaultSources).not.toContain(path.join(".venv", "ignored.py"));
|
||||||
|
expect(defaultSources).not.toContain(path.join(".tox", "ignored.py"));
|
||||||
|
await expect(expandSourceInput(".venv/**/*.py")).resolves.toEqual([
|
||||||
|
path.join(".venv", "ignored.py"),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves unmatched globs for Ruff to report", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const pattern = path.join(projectDir, "src", "**", "missing-*.py");
|
||||||
|
|
||||||
|
await expect(expandSourceInput(pattern)).resolves.toEqual([pattern]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("expands multiple sources in input order", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const first = path.join(projectDir, "src", "package", "main.py");
|
||||||
|
const second = path.join(projectDir, "src", "package", "nested", "*.py");
|
||||||
|
|
||||||
|
await expect(expandSourceInput(`${first} ${second}`)).resolves.toEqual([
|
||||||
|
first,
|
||||||
|
path.join(projectDir, "src", "package", "nested", "mod.py"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves existing literal paths that contain glob metacharacters", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const source = path.join(projectDir, "src", "pkg[legacy]");
|
||||||
|
|
||||||
|
await expect(expandSourceInput(source)).resolves.toEqual([source]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes existing literal path segments before later glob segments", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const pattern = path.join(projectDir, "src", "pkg[legacy]", "*.py");
|
||||||
|
|
||||||
|
await expect(expandSourceInput(pattern)).resolves.toEqual([
|
||||||
|
path.join(projectDir, "src", "pkg[legacy]", "main.py"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes existing literal path segments that contain * or ?", async () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
await fs.mkdir(path.join(projectDir, "src", "pkg*star"));
|
||||||
|
await fs.writeFile(path.join(projectDir, "src", "pkg*star", "main.py"), "");
|
||||||
|
await fs.mkdir(path.join(projectDir, "src", "pkg-other-star"));
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(projectDir, "src", "pkg-other-star", "main.py"),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
await fs.mkdir(path.join(projectDir, "src", "pkg?question"));
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(projectDir, "src", "pkg?question", "main.py"),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
await fs.mkdir(path.join(projectDir, "src", "pkgxquestion"));
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(projectDir, "src", "pkgxquestion", "main.py"),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
expandSourceInput(path.join(projectDir, "src", "pkg*star", "*.py")),
|
||||||
|
).resolves.toEqual([path.join(projectDir, "src", "pkg*star", "main.py")]);
|
||||||
|
await expect(
|
||||||
|
expandSourceInput(path.join(projectDir, "src", "pkg?question", "*.py")),
|
||||||
|
).resolves.toEqual([
|
||||||
|
path.join(projectDir, "src", "pkg?question", "main.py"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not traverse symlinked directories", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const externalDir = path.join(projectDir, "external");
|
||||||
|
const symlinkPath = path.join(projectDir, "src", "linked");
|
||||||
|
await fs.mkdir(externalDir);
|
||||||
|
await fs.writeFile(path.join(externalDir, "external.py"), "");
|
||||||
|
await fs.symlink(externalDir, symlinkPath, "dir");
|
||||||
|
|
||||||
|
const sources = await expandSourceInput(
|
||||||
|
path.join(projectDir, "src", "**", "*.py"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sources).not.toContain(path.join(symlinkPath, "external.py"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still expands missing paths that contain glob metacharacters", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const pattern = path.join(projectDir, "src", "pkg[l]");
|
||||||
|
|
||||||
|
await expect(expandSourceInput(pattern)).resolves.toEqual([
|
||||||
|
path.join(projectDir, "src", "pkgl"),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getVersionSourceDirectory", () => {
|
||||||
|
it("does not scan src globs when version input is explicit", async () => {
|
||||||
|
await expect(
|
||||||
|
getVersionSourceDirectory("../**/*.py", "0.1.0", undefined),
|
||||||
|
).resolves.toBe(".");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not scan src globs when version-file input is explicit", async () => {
|
||||||
|
await expect(
|
||||||
|
getVersionSourceDirectory("../**/*.py", undefined, "pyproject.toml"),
|
||||||
|
).resolves.toBe(".");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses source discovery when no version input is explicit", async () => {
|
||||||
|
await expect(
|
||||||
|
getVersionSourceDirectory("../**/*.py", undefined, undefined),
|
||||||
|
).rejects.toThrow("Invalid pattern '../**/*.py'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getSourceBasePath", () => {
|
||||||
|
it("returns the directory for a plain path", async () => {
|
||||||
|
await expect(getSourceBasePath("my-project")).resolves.toBe("my-project");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the directory for a dotted path", async () => {
|
||||||
|
await expect(getSourceBasePath("./src")).resolves.toBe("./src");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips glob pattern and returns parent directory", async () => {
|
||||||
|
await expect(getSourceBasePath("src/**/*.py")).resolves.toBe("src");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns . for a top-level glob", async () => {
|
||||||
|
await expect(getSourceBasePath("*.py")).resolves.toBe(".");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns . for empty string", async () => {
|
||||||
|
await expect(getSourceBasePath("")).resolves.toBe(".");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses only the first whitespace-delimited token", async () => {
|
||||||
|
await expect(getSourceBasePath("sub1/*.py sub2/")).resolves.toBe("sub1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles trailing slash in prefix", async () => {
|
||||||
|
await expect(getSourceBasePath("sub/*.py")).resolves.toBe("sub");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the parent directory when a glob appears in a filename", async () => {
|
||||||
|
await expect(getSourceBasePath("src/file*.py")).resolves.toBe("src");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles ? glob", async () => {
|
||||||
|
await expect(getSourceBasePath("file?.py")).resolves.toBe(".");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles bracket glob", async () => {
|
||||||
|
await expect(getSourceBasePath("file[0-9].py")).resolves.toBe(".");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves existing literal base paths that contain glob metacharacters", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.chdir(projectDir);
|
||||||
|
await expect(getSourceBasePath("src/pkg[legacy]")).resolves.toBe(
|
||||||
|
"src/pkg[legacy]",
|
||||||
|
);
|
||||||
|
await expect(getSourceBasePath("src/pkg[legacy]/*.py")).resolves.toBe(
|
||||||
|
path.join("src", "pkg[legacy]", "main.py"),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps version discovery rooted in existing literal paths with glob metacharacters", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
const pyprojectPath = path.join(
|
||||||
|
projectDir,
|
||||||
|
"src",
|
||||||
|
"pkg[legacy]",
|
||||||
|
"pyproject.toml",
|
||||||
|
);
|
||||||
|
await fs.writeFile(
|
||||||
|
pyprojectPath,
|
||||||
|
"[project]\ndependencies = ['ruff==0.6.2']\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.chdir(projectDir);
|
||||||
|
const sourceBasePath = await getSourceBasePath("src/pkg[legacy]/main.py");
|
||||||
|
|
||||||
|
expect(findPyprojectToml(sourceBasePath, projectDir)).toBe(
|
||||||
|
await fs.realpath(pyprojectPath),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the first expanded glob match for version discovery", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
const pyprojectPath = path.join(
|
||||||
|
projectDir,
|
||||||
|
"packages",
|
||||||
|
"foo",
|
||||||
|
"pyproject.toml",
|
||||||
|
);
|
||||||
|
await fs.mkdir(path.join(projectDir, "packages", "foo", "src"), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
await fs.writeFile(
|
||||||
|
pyprojectPath,
|
||||||
|
"[project]\ndependencies = ['ruff==0.10.0']\n",
|
||||||
|
);
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(projectDir, "packages", "foo", "src", "module.py"),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.chdir(projectDir);
|
||||||
|
const sourceBasePath = await getSourceBasePath("packages/*/src/**/*.py");
|
||||||
|
|
||||||
|
expect(sourceBasePath).toBe(
|
||||||
|
path.join("packages", "foo", "src", "module.py"),
|
||||||
|
);
|
||||||
|
const workspaceRoot = await fs.realpath(projectDir);
|
||||||
|
expect(findPyprojectToml(sourceBasePath, workspaceRoot)).toBe(
|
||||||
|
await fs.realpath(pyprojectPath),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the unmatched glob fallback when version-discovery glob has no matches", async () => {
|
||||||
|
const projectDir = await createTempProject();
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
await fs.mkdir(path.join(projectDir, "packages"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.chdir(projectDir);
|
||||||
|
await expect(getSourceBasePath("packages/*/src/**/*.py")).resolves.toBe(
|
||||||
|
"packages",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat brace expansion as glob syntax", async () => {
|
||||||
|
await expect(getSourceBasePath("src/{a,b}/*.py")).resolves.toBe(
|
||||||
|
"src/{a,b}",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -7,7 +7,7 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: "check"
|
default: "check"
|
||||||
src:
|
src:
|
||||||
description: "Source to run Ruff. Defaults to the current directory."
|
description: "Source path(s) or glob pattern(s) to run Ruff on. Defaults to the current workspace."
|
||||||
required: false
|
required: false
|
||||||
default: ${{ github.workspace }}
|
default: ${{ github.workspace }}
|
||||||
version:
|
version:
|
||||||
|
|||||||
+3164
-492
File diff suppressed because it is too large
Load Diff
Generated
+47
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^3.0.0",
|
"@actions/core": "^3.0.0",
|
||||||
"@actions/exec": "^3.0.0",
|
"@actions/exec": "^3.0.0",
|
||||||
|
"@actions/glob": "^0.7.0",
|
||||||
"@actions/tool-cache": "^4.0.0",
|
"@actions/tool-cache": "^4.0.0",
|
||||||
"@octokit/core": "^7.0.3",
|
"@octokit/core": "^7.0.3",
|
||||||
"@octokit/plugin-paginate-rest": "^13.1.1",
|
"@octokit/plugin-paginate-rest": "^13.1.1",
|
||||||
@@ -51,6 +52,52 @@
|
|||||||
"@actions/io": "^3.0.2"
|
"@actions/io": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@actions/glob": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-+7s3wM+cXapDLmLL1NVWHawqcJOZzXZy2df/VhNn8DnZtS/x83iTCKaUn9F0llur4h3CII0AilvKKH4CMPL8Gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/core": "^3.0.0",
|
||||||
|
"minimatch": "^10.2.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/glob/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/glob/node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/glob/node_modules/minimatch": {
|
||||||
|
"version": "10.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
|
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@actions/http-client": {
|
"node_modules/@actions/http-client": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^3.0.0",
|
"@actions/core": "^3.0.0",
|
||||||
"@actions/exec": "^3.0.0",
|
"@actions/exec": "^3.0.0",
|
||||||
|
"@actions/glob": "^0.7.0",
|
||||||
"@actions/tool-cache": "^4.0.0",
|
"@actions/tool-cache": "^4.0.0",
|
||||||
"@octokit/core": "^7.0.3",
|
"@octokit/core": "^7.0.3",
|
||||||
"@octokit/plugin-paginate-rest": "^13.1.1",
|
"@octokit/plugin-paginate-rest": "^13.1.1",
|
||||||
|
|||||||
+14
-9
@@ -21,6 +21,11 @@ import {
|
|||||||
getPlatform,
|
getPlatform,
|
||||||
type Platform,
|
type Platform,
|
||||||
} from "./utils/platforms";
|
} from "./utils/platforms";
|
||||||
|
import {
|
||||||
|
expandSourceInput,
|
||||||
|
getVersionSourceDirectory,
|
||||||
|
splitInput,
|
||||||
|
} from "./utils/source-input";
|
||||||
import { resolveRuffVersion } from "./version/resolve";
|
import { resolveRuffVersion } from "./version/resolve";
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
@@ -42,11 +47,7 @@ async function run(): Promise<void> {
|
|||||||
core.setOutput("ruff-version", setupResult.version);
|
core.setOutput("ruff-version", setupResult.version);
|
||||||
core.info(`Successfully installed ruff version ${setupResult.version}`);
|
core.info(`Successfully installed ruff version ${setupResult.version}`);
|
||||||
|
|
||||||
await runRuff(
|
await runRuff(path.join(setupResult.ruffDir, "ruff"), args, src);
|
||||||
path.join(setupResult.ruffDir, "ruff"),
|
|
||||||
args.split(" "),
|
|
||||||
src.split(" "),
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -94,7 +95,11 @@ async function setupRuff(
|
|||||||
async function determineVersion(): Promise<string> {
|
async function determineVersion(): Promise<string> {
|
||||||
return await resolveRuffVersion({
|
return await resolveRuffVersion({
|
||||||
manifestFile: manifestFile || undefined,
|
manifestFile: manifestFile || undefined,
|
||||||
sourceDirectory: src,
|
sourceDirectory: await getVersionSourceDirectory(
|
||||||
|
src,
|
||||||
|
version,
|
||||||
|
versionFileInput,
|
||||||
|
),
|
||||||
version,
|
version,
|
||||||
versionFile: versionFileInput,
|
versionFile: versionFileInput,
|
||||||
workspaceRoot: process.env.GITHUB_WORKSPACE || ".",
|
workspaceRoot: process.env.GITHUB_WORKSPACE || ".",
|
||||||
@@ -125,10 +130,10 @@ function getActionRoot(): string {
|
|||||||
|
|
||||||
async function runRuff(
|
async function runRuff(
|
||||||
ruffExecutablePath: string,
|
ruffExecutablePath: string,
|
||||||
args: string[],
|
args: string,
|
||||||
src: string[],
|
src: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const execArgs = [...args, ...src];
|
const execArgs = [...splitInput(args), ...(await expandSourceInput(src))];
|
||||||
await exec.exec(ruffExecutablePath, execArgs);
|
await exec.exec(ruffExecutablePath, execArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,22 @@ import * as path from "node:path";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for a pyproject.toml file starting from the given directory
|
* Search for a pyproject.toml file starting from the given source path
|
||||||
* and traversing upwards through parent directories until reaching
|
* and traversing upwards through parent directories until reaching
|
||||||
* the GitHub workspace root.
|
* the GitHub workspace root.
|
||||||
*
|
*
|
||||||
* @param startDir The directory to start the search from (e.g., the src input)
|
* If the source path points to a file, the search begins in that file's
|
||||||
|
* parent directory.
|
||||||
|
*
|
||||||
|
* @param startPath The source path to start the search from (file or directory)
|
||||||
* @param workspaceRoot The GitHub workspace directory (GITHUB_WORKSPACE)
|
* @param workspaceRoot The GitHub workspace directory (GITHUB_WORKSPACE)
|
||||||
* @returns The path to the found pyproject.toml, or undefined if not found
|
* @returns The path to the found pyproject.toml, or undefined if not found
|
||||||
*/
|
*/
|
||||||
export function findPyprojectToml(
|
export function findPyprojectToml(
|
||||||
startDir: string,
|
startPath: string,
|
||||||
workspaceRoot: string,
|
workspaceRoot: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
let currentDir = path.resolve(startDir);
|
let currentDir = resolveStartDirectory(startPath);
|
||||||
const resolvedWorkspaceRoot = path.resolve(workspaceRoot);
|
const resolvedWorkspaceRoot = path.resolve(workspaceRoot);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -42,6 +45,19 @@ export function findPyprojectToml(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveStartDirectory(startPath: string): string {
|
||||||
|
const resolvedStartPath = path.resolve(startPath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(resolvedStartPath)) {
|
||||||
|
return resolvedStartPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(resolvedStartPath);
|
||||||
|
return stats.isDirectory()
|
||||||
|
? resolvedStartPath
|
||||||
|
: path.dirname(resolvedStartPath);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a given path is within or equal to the workspace root.
|
* Check if a given path is within or equal to the workspace root.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as glob from "@actions/glob";
|
||||||
|
|
||||||
|
const GLOB_PATTERN = /[*?[]/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split an action input into whitespace-delimited tokens.
|
||||||
|
*
|
||||||
|
* The action has historically documented both `args` and `src` as plain
|
||||||
|
* whitespace-delimited inputs. Keep that behavior instead of invoking a shell.
|
||||||
|
*/
|
||||||
|
export function splitInput(input: string): string[] {
|
||||||
|
return input.trim().split(/\s+/).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand glob patterns from the `src` input in a cross-platform way.
|
||||||
|
*
|
||||||
|
* Shell expansion is not portable across GitHub runners (notably Windows), so
|
||||||
|
* we expand globs with @actions/glob and pass the result to Ruff as argv entries.
|
||||||
|
* Unmatched patterns are preserved, matching the default behavior of POSIX
|
||||||
|
* shells and allowing Ruff to report the missing path.
|
||||||
|
*/
|
||||||
|
export async function expandSourceInput(srcInput: string): Promise<string[]> {
|
||||||
|
const sources = splitInput(srcInput);
|
||||||
|
|
||||||
|
if (sources.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandedSources: string[] = [];
|
||||||
|
for (const source of sources) {
|
||||||
|
if (!hasGlobPattern(source) || (await pathExists(source))) {
|
||||||
|
expandedSources.push(source);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = await globMatches(source);
|
||||||
|
expandedSources.push(...(matches.length > 0 ? matches : [source]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return expandedSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the starting path for version discovery from the `src` input.
|
||||||
|
*
|
||||||
|
* Takes the first whitespace-delimited token. Literal paths are preserved. Glob
|
||||||
|
* patterns use their first match when available, so pyproject.toml discovery
|
||||||
|
* starts from the project that Ruff will actually check. Unmatched globs fall
|
||||||
|
* back to their literal prefix so Ruff can report the unmatched source later.
|
||||||
|
*/
|
||||||
|
export async function getVersionSourceDirectory(
|
||||||
|
srcInput: string,
|
||||||
|
versionInput: string | undefined,
|
||||||
|
versionFileInput: string | undefined,
|
||||||
|
): Promise<string> {
|
||||||
|
if (hasInput(versionInput) || hasInput(versionFileInput)) {
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getSourceBasePath(srcInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSourceBasePath(srcInput: string): Promise<string> {
|
||||||
|
const firstSource = splitInput(srcInput)[0] ?? ".";
|
||||||
|
|
||||||
|
if (!hasGlobPattern(firstSource) || (await pathExists(firstSource))) {
|
||||||
|
return stripTrailingSeparators(firstSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = await globMatches(firstSource);
|
||||||
|
if (matches.length > 0) {
|
||||||
|
return matches[0] ?? ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getUnmatchedSourceBasePath(firstSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUnmatchedSourceBasePath(source: string): Promise<string> {
|
||||||
|
const sourcePath = splitSourcePath(source);
|
||||||
|
const existingSegments = await getExistingLiteralSegments(sourcePath);
|
||||||
|
const nextSegment = sourcePath.segments[existingSegments.length];
|
||||||
|
if (existingSegments.length > 0 && hasGlobPattern(nextSegment ?? "")) {
|
||||||
|
return formatSourcePath(sourcePath.root, existingSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
const globIndex = source.search(GLOB_PATTERN);
|
||||||
|
const literalPrefix = source.substring(0, globIndex);
|
||||||
|
if (literalPrefix === "") {
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTrailingSeparator(literalPrefix)) {
|
||||||
|
return stripTrailingSeparators(literalPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.dirname(literalPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInput(input: string | undefined): boolean {
|
||||||
|
return input !== undefined && input.trim() !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasGlobPattern(source: string): boolean {
|
||||||
|
return GLOB_PATTERN.test(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTrailingSeparator(source: string): boolean {
|
||||||
|
return /[/\\]$/.test(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripTrailingSeparators(source: string): string {
|
||||||
|
return source.replace(/[/\\]+$/, "") || ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pathExists(source: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.stat(source);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
if (isPathNotFoundError(err)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPathNotFoundError(err: unknown): boolean {
|
||||||
|
const code = (err as { code?: unknown }).code;
|
||||||
|
return code === "ENOENT" || code === "ENOTDIR";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGlobPattern(pattern: string): Promise<string> {
|
||||||
|
const sourcePath = splitSourcePath(pattern);
|
||||||
|
const existingSegments = await getExistingLiteralSegments(sourcePath);
|
||||||
|
|
||||||
|
if (existingSegments.length === 0) {
|
||||||
|
return normalizeGlobPattern(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapedSegments = sourcePath.segments.map((segment, index) =>
|
||||||
|
index < existingSegments.length ? globEscape(segment) : segment,
|
||||||
|
);
|
||||||
|
return normalizeGlobPattern(
|
||||||
|
formatSourcePath(sourcePath.root, escapedSegments),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SourcePath {
|
||||||
|
root: string;
|
||||||
|
segments: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitSourcePath(source: string): SourcePath {
|
||||||
|
const root = path.parse(source).root;
|
||||||
|
const relativePath = source.slice(root.length);
|
||||||
|
const separatorPattern = process.platform === "win32" ? /[/\\]+/ : /\/+/;
|
||||||
|
|
||||||
|
return {
|
||||||
|
root,
|
||||||
|
segments: relativePath.split(separatorPattern).filter(Boolean),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getExistingLiteralSegments(
|
||||||
|
sourcePath: SourcePath,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const existingSegments: string[] = [];
|
||||||
|
|
||||||
|
for (const segment of sourcePath.segments) {
|
||||||
|
const candidateSegments = [...existingSegments, segment];
|
||||||
|
if (
|
||||||
|
!(await pathExists(toFileSystemPath(sourcePath.root, candidateSegments)))
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingSegments.push(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingSegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFileSystemPath(root: string, segments: string[]): string {
|
||||||
|
if (segments.length === 0) {
|
||||||
|
return root || ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return root ? path.join(root, ...segments) : path.join(...segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSourcePath(root: string, segments: string[]): string {
|
||||||
|
if (segments.length === 0) {
|
||||||
|
return root || ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
return segments.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||||
|
return `${normalizedRoot || "/"}/${segments.join("/")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function globEscape(source: string): string {
|
||||||
|
return source
|
||||||
|
.replace(/(\[)(?=[^/]+\])/g, "[[]")
|
||||||
|
.replace(/\?/g, "[?]")
|
||||||
|
.replace(/\*/g, "[*]");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function globMatches(pattern: string): Promise<string[]> {
|
||||||
|
const globber = await glob.create(await getGlobPattern(pattern), {
|
||||||
|
excludeHiddenFiles: !explicitlyTargetsHiddenPath(pattern),
|
||||||
|
followSymbolicLinks: false,
|
||||||
|
implicitDescendants: false,
|
||||||
|
});
|
||||||
|
const matches = await globber.glob();
|
||||||
|
|
||||||
|
return await preserveSourcePathStyle(pattern, matches.sort());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preserveSourcePathStyle(
|
||||||
|
pattern: string,
|
||||||
|
matches: string[],
|
||||||
|
): Promise<string[]> {
|
||||||
|
if (path.isAbsolute(pattern)) {
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cwd = await fs.realpath(process.cwd());
|
||||||
|
return matches.map((match) => path.relative(cwd, match) || ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
function explicitlyTargetsHiddenPath(pattern: string): boolean {
|
||||||
|
return splitSourcePath(pattern).segments.some(isHiddenPathSegment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHiddenPathSegment(segment: string): boolean {
|
||||||
|
return segment.startsWith(".") && segment !== "." && segment !== "..";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGlobPattern(pattern: string): string {
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pattern.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user