Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/large-showers-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
18 changes: 16 additions & 2 deletions .dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ module.exports = {
from: { path: "^packages/hunk-vcs/src/" },
to: { path: "^packages/", pathNot: "^packages/hunk-vcs/src/" },
},
{
name: "hunk-git-stays-on-vcs-contract",
comment:
"@hunk/git owns the Git provider and may reach only its local modules, the public extension contract, and explicit dependency-bottom @hunk/vcs leaves.",
severity: "error",
from: { path: "^packages/hunk-git/src/" },
to: {
path: "^packages/",
pathNot: "^packages/(hunk-git|hunk-vcs)/src/|^packages/hunk/src/extension-api/",
},
},
{
name: "lib-is-a-leaf",
comment:
Expand Down Expand Up @@ -177,9 +188,12 @@ module.exports = {
{
name: "packages-stay-standalone",
comment:
"Workspace packages are standalone publishable units; they never import the app source tree.",
"Workspace packages are standalone units; bundled providers are governed by their narrower public-contract rules above, while other packages never import the app source tree.",
severity: "error",
from: { path: "^packages/(?!hunk/)" },
from: {
path: "^packages/(?!hunk/)",
pathNot: "^packages/hunk-git/",
},
to: { path: "^packages/hunk/src/" },
},
],
Expand Down
11 changes: 11 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
"project": ["src/**/*.{ts,tsx}", "bin/**/*.cjs"],
"ignoreExportsUsedInFile": true
},
"packages/hunk-git": {
"entry": ["src/**/*.test.ts"],
"project": ["src/**/*.ts"]
},
"packages/hunk-vcs": {
"entry": ["src/**/*.test.ts"],
"project": ["src/**/*.ts"]
Expand Down
1 change: 1 addition & 0 deletions nix/bun.lock.nix
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
url = "https://registry.npmjs.org/@hono/node-ws/-/node-ws-1.3.1.tgz";
hash = "sha512-vo/MwCnpJAVHBkGzWjCJ28wF45fYHAfbPZcH2rodZODHtch2GHA94KtMfusmVycTUtsLAsaNsHhtY6P8X3RQsA==";
};
"@hunk/git" = copyPathToStore ../packages/hunk-git;
"@hunk/session-broker" = copyPathToStore ../packages/session-broker;
"@hunk/session-broker-bun" = copyPathToStore ../packages/session-broker-bun;
"@hunk/session-broker-core" = copyPathToStore ../packages/session-broker-core;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
},
"devDependencies": {
"@changesets/changelog-github": "^0.7.0",
"@hunk/git": "workspace:*",
"@hunk/session-broker": "workspace:*",
"@hunk/session-broker-bun": "workspace:*",
"@hunk/session-broker-core": "workspace:*",
Expand Down
26 changes: 26 additions & 0 deletions packages/hunk-git/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@hunk/git",
"version": "0.0.0",
"private": true,
"description": "Private statically bundled Git provider for Hunk.",
"license": "MIT",
"files": [
"src"
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
}
},
"dependencies": {
"@hunk/vcs": "workspace:*",
"hunkdiff": "workspace:*"
},
"engines": {
"bun": ">=1.3.14",
"node": ">=22"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ import { runAbortableCommand } from "@hunk/vcs/async-process";
/**
* Every Git command Hunk runs, and the failures they translate into.
*
* This is the implementation layer behind the bundled Git backend
* (`packages/hunk/src/extensions/default/vcs/git/`), so nothing here reaches into core, the
* diff engine, or the adapter registry — user-facing failures are raised as the
* This is the implementation layer behind the bundled Git backend. Nothing here
* reaches into core, the diff engine, or the adapter registry — user-facing failures are raised as the
* published `HunkExtensionUserError`, which is exactly what a third-party
* backend would throw.
*/
Expand Down Expand Up @@ -51,7 +50,7 @@ export interface GitColorMovedOptions {
}

/** Append Git pathspec arguments only when the caller requested them. */
export function appendGitPathspecs(args: string[], pathspecs?: string[]) {
function appendGitPathspecs(args: string[], pathspecs?: string[]) {
if (!pathspecs || pathspecs.length === 0) {
return;
}
Expand All @@ -65,7 +64,7 @@ export function appendGitPathspecs(args: string[], pathspecs?: string[]) {
* example `--output=<path>`) that Git would parse as a flag; it fails closed here instead
* of reaching the spawned command.
*/
export function requireGitRevisionArg(input: GitBackedInput, value: string) {
function requireGitRevisionArg(input: GitBackedInput, value: string) {
if (value.length === 0) {
throw new HunkExtensionUserError(
`\`${formatGitCommandLabel(input)}\` refused an empty revision.`,
Expand Down Expand Up @@ -306,7 +305,7 @@ export function buildGitStashShowArgs(
return withNormalizedDiffPrefixes(withGitMovedLineColorConfig(args, colorMoved));
}

export function formatGitCommandLabel(input: GitBackedInput) {
function formatGitCommandLabel(input: GitBackedInput) {
switch (input.kind) {
case "vcs": {
if (input.staged) {
Expand Down Expand Up @@ -869,7 +868,7 @@ export function resolveGitRepoRoot(
}

/** Resolve one commit-ish ref to the exact commit object used for later blob reads. */
export function resolveGitCommitRef(
function resolveGitCommitRef(
input: GitBackedInput,
ref: string,
options: Omit<RunGitTextOptions, "input" | "args"> = {},
Expand Down
2 changes: 1 addition & 1 deletion packages/hunk/src/core/changeset/loaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { join } from "node:path";
import { replaceExtensionFileLanguages } from "./fileLanguage";
import { SourceTextTooLargeError } from "./fileSource";
import { getBundledVcsCatalog } from "../../app/vcsCatalog";
import { createGitVcsAdapter } from "../../extensions/default/vcs/git";
import { createGitVcsAdapter } from "@hunk/git";
import { toInternalVcsAdapter } from "../../extensions/runExtension";
import { createVcsCatalog } from "../vcs";
import { loadAppBootstrap as loadCoreAppBootstrap, type LoadAppBootstrapOptions } from "./loaders";
Expand Down
2 changes: 1 addition & 1 deletion packages/hunk/src/extensions/default/vcs/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import gitExtension from "./git";
import gitExtension from "@hunk/git";
import jjExtension from "./jujutsu";
import slExtension from "./sapling";
import { runExtensionFactory } from "../../runExtension";
Expand Down
54 changes: 54 additions & 0 deletions scripts/hunk-git-package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, test } from "bun:test";
import gitExtension, { GitVcsAdapter } from "@hunk/git";
import { readFileSync } from "node:fs";
import { join, resolve } from "node:path";

const REPO_ROOT = resolve(import.meta.dir, "..");
const PACKAGE_ROOT = join(REPO_ROOT, "packages", "hunk-git");

describe("@hunk/git package boundary", () => {
test("exports only the explicit Git provider entry", () => {
const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")) as {
name: string;
private: boolean;
files: string[];
exports: Record<string, unknown>;
dependencies: Record<string, string>;
};

expect(manifest.name).toBe("@hunk/git");
expect(manifest.private).toBe(true);
expect(manifest.files).toEqual(["src"]);
expect(manifest.exports).toEqual({
".": {
types: "./src/index.ts",
import: "./src/index.ts",
},
});
expect(manifest.dependencies).toEqual({
"@hunk/vcs": "workspace:*",
hunkdiff: "workspace:*",
});
});

test("registers the private workspace without leaking it into hunkdiff", () => {
const rootManifest = JSON.parse(readFileSync(join(REPO_ROOT, "package.json"), "utf8")) as {
devDependencies: Record<string, string>;
};
const hunkManifest = readFileSync(join(REPO_ROOT, "packages", "hunk", "package.json"), "utf8");
const bunLock = readFileSync(join(REPO_ROOT, "bun.lock"), "utf8");
const nixLock = readFileSync(join(REPO_ROOT, "nix", "bun.lock.nix"), "utf8");

expect(rootManifest.devDependencies["@hunk/git"]).toBe("workspace:*");
expect(hunkManifest).not.toContain("@hunk/git");
expect(hunkManifest).not.toContain("workspace:");
expect(bunLock).toContain('"packages/hunk-git": {');
expect(bunLock).toContain('"@hunk/git@workspace:packages/hunk-git"');
expect(nixLock).toContain('"@hunk/git" = copyPathToStore ../packages/hunk-git;');
});

test("loads its provider entrypoint through the workspace", () => {
expect(typeof gitExtension).toBe("function");
expect(GitVcsAdapter.id).toBe("git");
});
});
33 changes: 21 additions & 12 deletions scripts/source-boundaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const SRC_ROOT = join(REPO_ROOT, "packages", "hunk", "src");
const CORE_ROOT = join(SRC_ROOT, "core");
const EXTENSIONS_ROOT = join(SRC_ROOT, "extensions");
const BUNDLED_PROVIDER_ROOT = join(EXTENSIONS_ROOT, "default", "vcs");
const GIT_PACKAGE_ROOT = join(REPO_ROOT, "packages", "hunk-git", "src");
const VCS_PACKAGE_ROOT = join(REPO_ROOT, "packages", "hunk-vcs", "src");
const REVIEW_MODEL_ROOT = join(CORE_ROOT, "review");
// The published extension contract, which the review model may name for the annotation shapes
Expand Down Expand Up @@ -166,18 +167,25 @@ function unexpectedProviderImports() {
"@hunk/vcs/path",
"@hunk/vcs/source",
]);
return sourceFiles(BUNDLED_PROVIDER_ROOT).flatMap((path) =>
importSpecifiers(path)
// The repository-wide lightweight scanner can also match prose ending in `from "…"`;
// real module specifiers never contain whitespace.
.filter((specifier) => !/\s/.test(specifier))
.filter(
(specifier) =>
!specifier.startsWith(".") &&
!specifier.startsWith("node:") &&
!allowedImports.has(specifier),
)
.map((specifier) => `${repoPath(path)} -> ${specifier}`),
return [BUNDLED_PROVIDER_ROOT, GIT_PACKAGE_ROOT].flatMap((providerRoot) =>
sourceFiles(providerRoot).flatMap((path) =>
importSpecifiers(path)
// The lightweight scanner can match prose ending in `from "…"`; module specifiers
// never contain whitespace.
.filter((specifier) => !/\s/.test(specifier))
.filter((specifier) => {
if (
specifier.startsWith(".") ||
specifier.startsWith("node:") ||
specifier.startsWith("bun:") ||
allowedImports.has(specifier)
) {
return false;
}
return !(path === join(BUNDLED_PROVIDER_ROOT, "index.ts") && specifier === "@hunk/git");
})
.map((specifier) => `${repoPath(path)} -> ${specifier}`),
),
);
}

Expand Down Expand Up @@ -303,6 +311,7 @@ describe("source architecture boundaries", () => {

test("keeps bundled providers on the public contract and explicit VCS helper leaves", () => {
expect(forbiddenImports(BUNDLED_PROVIDER_ROOT, CORE_ROOT)).toEqual([]);
expect(escapingImports(GIT_PACKAGE_ROOT, [GIT_PACKAGE_ROOT])).toEqual([]);
expect(unexpectedProviderImports()).toEqual([]);
});

Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"types": ["bun", "react"],
"baseUrl": ".",
"paths": {
"@hunk/git": ["packages/hunk-git/src/index.ts"],
"@hunk/session-broker": ["packages/session-broker/src/index.ts"],
"@hunk/session-broker-bun": ["packages/session-broker-bun/src/index.ts"],
"@hunk/session-broker-core": ["packages/session-broker-core/src/index.ts"],
Expand Down
Loading