Release Git Tools 0.1.0

This commit is contained in:
2026-09-01 13:05:57 +02:00
commit ed0acbf8c8
57 changed files with 9605 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import { expect, test, type Page } from "@playwright/test";
const ORIGIN = "http://127.0.0.1:4173";
async function localOnly(page: Page) {
const external: string[] = [];
await page.route("**/*", async (route) => {
const url = new URL(route.request().url());
if (url.origin !== ORIGIN) {
external.push(url.href);
await route.abort();
} else await route.continue();
});
return external;
}
test("runs from a nested path without external requests", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
const external = await localOnly(page);
await page.goto("/deep/nested/git/");
await expect(
page.getByRole("banner").getByRole("heading", { name: "Git Tools" }),
).toBeVisible();
expect(external).toEqual([]);
expect(errors).toEqual([]);
});
test("authors and re-inspects a patch without flashing state", async ({
page,
}) => {
const external = await localOnly(page);
await page.goto("/deep/nested/git/");
await page.getByRole("tab", { name: "Author" }).click();
await page.getByLabel("After text").fill("alpha\nbeta\n");
await page.getByRole("button", { name: "Generate patch" }).click();
await expect(page.getByLabel("Generated patch")).toContainText("+alpha");
await page.getByRole("button", { name: "Inspect generated patch" }).click();
await expect(page.getByText("src/example.ts", { exact: true })).toBeVisible();
expect(external).toEqual([]);
});
test("serves release identity and hardened headers", async ({ request }) => {
const index = await request.get("/deep/nested/git/");
expect(index.ok()).toBe(true);
expect(index.headers()["content-security-policy"]).toContain(
"connect-src 'self'",
);
expect(await index.text()).not.toMatch(/\b(?:src|href)=["']\//u);
const manifest = await request.get("/deep/nested/git/toolbox-app.json");
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.git-tools",
version: "0.1.0",
entry: "./",
privacy: { processing: "local", telemetry: false },
});
});
+24
View File
@@ -0,0 +1,24 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { App } from "../../src/App";
describe("Git Tools", () => {
it("renders the local workbench and ignore explanations", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("Not found", { status: 404 })),
);
render(<App />);
expect(
await screen.findByRole("heading", { name: "Git Tools" }),
).toBeVisible();
await userEvent.click(
await screen.findByRole("tab", { name: ".gitignore" }),
);
expect(screen.getAllByText(/Last matching rule/u).length).toBeGreaterThan(
0,
);
expect(screen.getByText("Browser-local")).toBeVisible();
});
});
+126
View File
@@ -0,0 +1,126 @@
import { describe, expect, it } from "vitest";
import {
buildConventionalCommit,
compareSemVer,
createUnifiedPatch,
explainGitignore,
lintConventionalCommit,
normalizeChangelog,
parseSemVer,
parseUnifiedPatch,
satisfiesSemVer,
} from "../../src/git/tools";
describe("Git interchange tools", () => {
it("parses patch files, renames, hunks, and statistics", () => {
const source = `diff --git a/old.txt b/new.txt\nsimilarity index 90%\nrename from old.txt\nrename to new.txt\n--- a/old.txt\n+++ b/new.txt\n@@ -1,2 +1,2 @@ section\n same\n-old\n+new\n`;
const parsed = parseUnifiedPatch(source);
expect(parsed).toMatchObject({ added: 1, deleted: 1 });
expect(parsed.files[0]).toMatchObject({
status: "renamed",
oldPath: "old.txt",
newPath: "new.txt",
similarity: 90,
});
expect(parsed.files[0]?.hunks[0]?.validCounts).toBe(true);
});
it("decodes Git C-quoted octal paths", () => {
const source = `diff --git "a/foo\\040bar.txt" "b/foo\\040bar.txt"\n--- "a/foo\\040bar.txt"\n+++ "b/foo\\040bar.txt"\n@@ -1 +1 @@\n-old\n+new\n`;
expect(parseUnifiedPatch(source).files[0]).toMatchObject({
oldPath: "foo bar.txt",
newPath: "foo bar.txt",
});
});
it("authors a patch that parses with valid counts", () => {
const output = createUnifiedPatch(
"a\nb\nc\n",
"a\nB\nc\nd\n",
"src/a file.txt",
1,
);
expect(output).toContain("-b");
expect(output).toContain("+B");
const parsed = parseUnifiedPatch(output);
expect(parsed.diagnostics).toEqual([]);
expect(parsed).toMatchObject({ added: 2, deleted: 1 });
});
it("explains ordered ignore rules and blocked parents", () => {
const source = `dist/\n*.log\n!keep.log\ncache/\n!cache/readme.md\n`;
const results = explainGitignore(source, [
"dist/app.js",
"debug.log",
"keep.log",
"cache/readme.md",
]);
expect(results.map((item) => item.ignored)).toEqual([
true,
true,
false,
true,
]);
expect(results[3]?.explanation).toMatch(/parent cache/u);
});
it("propagates ordinary directory matches but respects directory-only inputs", () => {
const results = explainGitignore(`dist/\nbuild\n`, [
"dist",
"dist/",
"dist/app.js",
"build",
"build/",
"build/app.js",
]);
expect(results.map((item) => item.ignored)).toEqual([
false,
true,
true,
true,
true,
true,
]);
});
it("implements SemVer precedence and basic ranges", () => {
expect(compareSemVer("1.0.0-alpha.1", "1.0.0-alpha.beta")).toBeLessThan(0);
expect(compareSemVer("1.0.0+one", "1.0.0+two")).toBe(0);
expect(satisfiesSemVer("1.4.2", "^1.2.0 <2.0.0")).toBe(true);
expect(satisfiesSemVer("2.0.0", "^1.2.0 || >=3.0.0")).toBe(false);
expect(satisfiesSemVer("1.3.0-beta.1", "^1.2.0")).toBe(false);
expect(satisfiesSemVer("1.9.9", "~1")).toBe(true);
expect(satisfiesSemVer("0.8.0", "^0")).toBe(true);
expect(satisfiesSemVer("0.0.9", "^0.0")).toBe(true);
expect(satisfiesSemVer("2.3.9", "1.2 - 2.3")).toBe(true);
expect(satisfiesSemVer("1.99.0", "<=1")).toBe(true);
expect(satisfiesSemVer("2.0.0", "<=1")).toBe(false);
expect(() => parseSemVer("9007199254740992.0.0")).toThrow(/safe integer/u);
});
it("builds and lints conventional commits", () => {
const message = buildConventionalCommit({
type: "feat",
scope: "api",
breaking: true,
subject: "change response shape",
breakingDescription: "Clients must read data.items.",
issues: "#42",
});
expect(message).toContain("feat(api)!");
expect(message).toContain("BREAKING CHANGE:");
expect(lintConventionalCommit(message).valid).toBe(true);
expect(lintConventionalCommit("bad header").valid).toBe(false);
});
it("normalizes changelog headings, order, and duplicate bullets", () => {
const result = normalizeChangelog(
`# Changelog\n\n## 1.0.0 - 2026-01-01\n\n### fixed\n\n- B\n- B\n\n### Added\n\n- A\n`,
);
expect(result.deduplicated).toBe(1);
expect(result.output.indexOf("### Added")).toBeLessThan(
result.output.indexOf("### Fixed"),
);
expect(result.output).toContain("## [1.0.0] - 2026-01-01");
});
});