refactor(jj): restructured exports into namespaced repo and diff objects
- Grouped `isRepository`, `workspaceRoot`, and `clearWorkspaceRootCache` under a `repo` namespace (`repo.is`, `repo.root`, `repo.clearRootCache`). - Promoted `diff` to a named export with a `changedFiles` sub-method via `Object.assign`. - Added `ensureAvailable` check and `nameOnly` support to `diff`. - Updated callers and tests to use the new API surface.
This commit is contained in:
@@ -449,7 +449,7 @@ async function getGitStatus(api: CustomCommandAPI): Promise<string> {
|
||||
}
|
||||
|
||||
async function getUncommittedReviewDiff(api: CustomCommandAPI): Promise<CurrentReviewDiff> {
|
||||
if (await jj.isRepository(api.cwd)) {
|
||||
if (await jj.repo.is(api.cwd)) {
|
||||
return {
|
||||
diffText: await jj.diff(api.cwd),
|
||||
diffInstruction: JJ_UNCOMMITTED_DIFF_INSTRUCTION,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { $which } from "@oh-my-pi/pi-utils";
|
||||
import { LRUCache } from "lru-cache/raw";
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Types
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Result from a completed `jj` subprocess invocation. */
|
||||
export interface JjCommandResult {
|
||||
/** Process exit code reported by `jj`. */
|
||||
@@ -12,10 +17,20 @@ export interface JjCommandResult {
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
/** Options for `jj diff --git` invocations. */
|
||||
/** Resolved Jujutsu workspace metadata. */
|
||||
export interface JjRepository {
|
||||
/** Root directory containing the `.jj` workspace metadata. */
|
||||
repoRoot: string;
|
||||
/** Path to the workspace store directory used to verify a real JJ checkout. */
|
||||
storeDir: string;
|
||||
}
|
||||
|
||||
/** Options for `jj diff` invocations. */
|
||||
export interface DiffOptions {
|
||||
/** Optional file paths to restrict the diff with `-- <files>`. */
|
||||
readonly files?: readonly string[];
|
||||
/** Return only changed file names instead of Git-format diff text. */
|
||||
readonly nameOnly?: boolean;
|
||||
/** Optional abort signal passed to the spawned `jj` process. */
|
||||
readonly signal?: AbortSignal;
|
||||
}
|
||||
@@ -24,6 +39,10 @@ interface CommandOptions {
|
||||
readonly signal?: AbortSignal;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Error
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Error thrown when a checked `jj` command exits non-zero. */
|
||||
export class JjCommandError extends Error {
|
||||
/** Arguments passed after the common `jj --no-pager --color=never` prefix. */
|
||||
@@ -40,6 +59,17 @@ export class JjCommandError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Internal: Core execution
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function ensureAvailable(): void {
|
||||
if (!$which("jj")) {
|
||||
throw new Error("jj is not installed.");
|
||||
}
|
||||
}
|
||||
|
||||
function formatCommandFailure(
|
||||
args: readonly string[],
|
||||
result: Pick<JjCommandResult, "exitCode" | "stdout" | "stderr">,
|
||||
@@ -79,6 +109,7 @@ async function runChecked(
|
||||
args: readonly string[],
|
||||
options: CommandOptions = {},
|
||||
): Promise<JjCommandResult> {
|
||||
ensureAvailable();
|
||||
const result = await jj(cwd, args, options);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new JjCommandError(args, result);
|
||||
@@ -86,14 +117,34 @@ async function runChecked(
|
||||
return result;
|
||||
}
|
||||
|
||||
async function runText(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<string> {
|
||||
return (await runChecked(cwd, args, options)).stdout;
|
||||
}
|
||||
|
||||
function splitLines(text: string): string[] {
|
||||
return text
|
||||
.split("\n")
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildDiffArgs(options: DiffOptions): string[] {
|
||||
const args = ["diff", "--git"];
|
||||
const args = ["diff"];
|
||||
args.push(options.nameOnly ? "--name-only" : "--git");
|
||||
if (options.files?.length) args.push("--", ...options.files);
|
||||
return args;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Internal: Repository resolution
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
interface WorkspaceRootCacheEntry {
|
||||
readonly root?: string;
|
||||
}
|
||||
|
||||
const WORKSPACE_ROOT_CACHE_MAX_ENTRIES = 256;
|
||||
const workspaceRootCache = new LRUCache<string, string | null>({ max: WORKSPACE_ROOT_CACHE_MAX_ENTRIES });
|
||||
const workspaceRootCache = new LRUCache<string, WorkspaceRootCacheEntry>({ max: WORKSPACE_ROOT_CACHE_MAX_ENTRIES });
|
||||
|
||||
async function hasJjWorkspaceMetadata(dir: string): Promise<boolean> {
|
||||
try {
|
||||
@@ -110,35 +161,70 @@ function parentOf(dir: string): string | undefined {
|
||||
|
||||
async function findWorkspaceRoot(cwd: string): Promise<string | undefined> {
|
||||
const key = path.resolve(cwd);
|
||||
if (workspaceRootCache.has(key)) return workspaceRootCache.get(key) ?? undefined;
|
||||
if (workspaceRootCache.has(key)) return workspaceRootCache.get(key)?.root;
|
||||
|
||||
for (let dir: string | undefined = key; dir; dir = parentOf(dir)) {
|
||||
if (await hasJjWorkspaceMetadata(dir)) {
|
||||
workspaceRootCache.set(key, dir);
|
||||
workspaceRootCache.set(key, { root: dir });
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
workspaceRootCache.set(key, null);
|
||||
workspaceRootCache.set(key, {});
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Clear cached workspace roots. Intended for tests that mutate JJ metadata under an existing path. */
|
||||
export function clearWorkspaceRootCache(): void {
|
||||
workspaceRootCache.clear();
|
||||
function repositoryFromRoot(root: string): JjRepository {
|
||||
return {
|
||||
repoRoot: root,
|
||||
storeDir: path.join(root, ".jj", "repo", "store"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve the current Jujutsu workspace root, or `undefined` when `cwd` is not in a JJ repository. */
|
||||
export async function workspaceRoot(cwd: string): Promise<string | undefined> {
|
||||
return findWorkspaceRoot(cwd);
|
||||
}
|
||||
|
||||
/** Return whether `cwd` is inside a Jujutsu repository. */
|
||||
export async function isRepository(cwd: string): Promise<boolean> {
|
||||
return (await workspaceRoot(cwd)) !== undefined;
|
||||
}
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// API: diff
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Run `jj diff --git` for the current workspace commit and return the raw Git-format diff text. */
|
||||
export async function diff(cwd: string, options: DiffOptions = {}): Promise<string> {
|
||||
return (await runChecked(cwd, buildDiffArgs(options), { signal: options.signal })).stdout;
|
||||
}
|
||||
export const diff = Object.assign(
|
||||
async function diff(cwd: string, options: DiffOptions = {}): Promise<string> {
|
||||
return runText(cwd, buildDiffArgs(options), { signal: options.signal });
|
||||
},
|
||||
{
|
||||
/** List changed file paths. */
|
||||
async changedFiles(
|
||||
cwd: string,
|
||||
options: Pick<DiffOptions, "files" | "signal"> = {},
|
||||
): Promise<string[]> {
|
||||
return splitLines(await diff(cwd, { ...options, nameOnly: true }));
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// API: repo
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const repo = {
|
||||
/** Clear cached workspace roots. Intended for tests that mutate JJ metadata under an existing path. */
|
||||
clearRootCache(): void {
|
||||
workspaceRootCache.clear();
|
||||
},
|
||||
|
||||
/** Resolve the current Jujutsu workspace root, or `null` when `cwd` is not in a JJ repository. */
|
||||
async root(cwd: string): Promise<string | null> {
|
||||
return (await findWorkspaceRoot(cwd)) ?? null;
|
||||
},
|
||||
|
||||
/** Full Jujutsu workspace metadata. */
|
||||
async resolve(cwd: string): Promise<JjRepository | null> {
|
||||
const root = await repo.root(cwd);
|
||||
return root ? repositoryFromRoot(root) : null;
|
||||
},
|
||||
|
||||
/** Check whether `cwd` is inside a Jujutsu repository. */
|
||||
async is(cwd: string): Promise<boolean> {
|
||||
return (await repo.root(cwd)) !== null;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user