Skip to content
Closed
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
7 changes: 7 additions & 0 deletions apps/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ export async function main(): Promise<void> {
}

// Check prerequisites (git, jj, arr initialized)
const debug = !!parsed.flags.debug;
let t0 = Date.now();
const prereqs = await checkContext();
if (debug) console.log(` checkContext: ${Date.now() - t0}ms`);
if (!isContextValid(prereqs, requiredLevel)) {
printContextError(prereqs, requiredLevel);
process.exit(1);
Expand All @@ -193,12 +196,16 @@ export async function main(): Promise<void> {
// Initialize context with engine
let context: ArrContext | null = null;
try {
t0 = Date.now();
context = await initContext();
if (debug) console.log(` initContext: ${Date.now() - t0}ms`);

// Trigger background PR refresh (rate-limited)
triggerBackgroundRefresh(context.cwd);

t0 = Date.now();
await handler(parsed, context);
if (debug) console.log(` handler: ${Date.now() - t0}ms`);
} finally {
// Auto-persist engine changes
context?.engine.persist();
Expand Down
7 changes: 2 additions & 5 deletions apps/cli/src/commands/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,8 @@ export async function log(
}

// Extract data directly from jj output (no extra jj calls needed)
const { unsyncedBookmarks, behindTrunkChanges, wcParentBookmark } = extractTemplateData(
result.value.stdout,
trackedBookmarks,
);
const { unsyncedBookmarks, behindTrunkChanges, wcParentBookmark } =
extractTemplateData(result.value.stdout, trackedBookmarks);

// Build enhancement data
t0 = Date.now();
Expand Down Expand Up @@ -547,7 +545,6 @@ function buildPRInfoMap(
return prInfoMap;
}


function formatChangeId(changeId: string, prefix: string): string {
const short = changeId.slice(0, 8);
if (prefix && short.startsWith(prefix)) {
Expand Down
12 changes: 7 additions & 5 deletions apps/cli/src/commands/status.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { hasResolvedConflict } from "@array/core/commands/resolve";
import { status as statusCmd } from "@array/core/commands/status";
import { COMMANDS } from "../registry";
import {
Expand All @@ -18,11 +17,14 @@ import {
} from "../utils/output";
import { unwrap } from "../utils/run";

export async function status(): Promise<void> {
const { info, stats } = unwrap(await statusCmd());
export async function status(options: { debug?: boolean } = {}): Promise<void> {
const debug = options.debug ?? false;
const {
info,
stats,
hasResolvedConflict: hasResolved,
} = unwrap(await statusCmd({ debug }));
const statsStr = stats ? ` ${formatDiffStats(stats)}` : "";
const resolvedResult = await hasResolvedConflict();
const hasResolved = resolvedResult.ok && resolvedResult.value;

// Check if on main with no stack above (fresh start)
const isOnMainFresh =
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const HANDLERS: Record<string, CommandHandler> = {
init: (p) => init(p.flags),
auth: () => auth(),
config: () => config(),
status: () => status(),
status: (p) => status({ debug: !!p.flags.debug }),
create: (p, ctx) => create(p.args.join(" "), ctx!),
submit: (p, ctx) => submit(p.flags, ctx!),
get: (p, ctx) => get(ctx!, p.args[0]),
Expand Down
46 changes: 22 additions & 24 deletions packages/core/src/commands/squash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,31 @@ export async function squash(
): Promise<Result<SquashResult>> {
const { id, engine } = options;

const statusBefore = await status();
if (!statusBefore.ok) return statusBefore;

// Resolve the change
let change: Changeset;
if (id) {
const findResult = await findChange(id, { includeBookmarks: true });
if (!findResult.ok) return findResult;
if (findResult.value.status === "none") {
return err(createError("INVALID_REVISION", `Change not found: ${id}`));
}
if (findResult.value.status === "multiple") {
return err(
createError(
"AMBIGUOUS_REVISION",
`Multiple changes match "${id}". Use a more specific identifier.`,
),
);
}
change = findResult.value.change;
} else {
// Use current working copy
change = statusBefore.value.workingCopy;
// Resolve the change - always use findChange for full Changeset info
const targetRevset = id || "@-"; // @- is the parent (current change), @ is WC
const findResult = await findChange(targetRevset, { includeBookmarks: true });
if (!findResult.ok) return findResult;
if (findResult.value.status === "none") {
return err(
createError("INVALID_REVISION", `Change not found: ${id || "current"}`),
);
}
if (findResult.value.status === "multiple") {
return err(
createError(
"AMBIGUOUS_REVISION",
`Multiple changes match "${id}". Use a more specific identifier.`,
),
);
}
const change = findResult.value.change;

// Check if we're currently on this change (need status for WC info)
const statusBefore = await status();
if (!statusBefore.ok) return statusBefore;
const wasOnChange =
statusBefore.value.workingCopy.changeId === change.changeId;
statusBefore.value.workingCopy.changeId === change.changeId ||
statusBefore.value.parents[0]?.changeId === change.changeId;
const parentId = change.parents[0];

const childrenResult = await list({
Expand Down
124 changes: 54 additions & 70 deletions packages/core/src/commands/status.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,73 @@
import {
getBookmarkTracking,
getDiffStats,
getTrunk,
status as jjStatus,
list,
} from "../jj";
import { buildTree, flattenTree } from "../log";
import { getDiffStats, getTrunk, status as jjStatus } from "../jj";
import { ok, type Result } from "../result";
import type { DiffStats, NextAction, StatusInfo } from "../types";
import type { Command } from "./types";

interface StatusResult {
info: StatusInfo;
stats: DiffStats | null;
hasResolvedConflict: boolean;
}

/**
* Get current status information including working copy state,
* stack path, conflicts, modified files, and diff stats.
*/
export async function status(): Promise<Result<StatusResult>> {
const statusResult = await jjStatus();
if (!statusResult.ok) return statusResult;

// Fetch all mutable changes plus trunk for log
const logResult = await list({ revset: "mutable() | trunk()" });
if (!logResult.ok) return logResult;

const trunkBranch = await getTrunk();
const trunk =
logResult.value.find(
(c) => c.bookmarks.includes(trunkBranch) && c.isImmutable,
) ?? null;
const workingCopy = logResult.value.find((c) => c.isWorkingCopy) ?? null;
const allChanges = logResult.value.filter((c) => !c.isImmutable);
const trunkId = trunk?.changeId ?? "";

// Current change is the parent (@-)
const currentChangeId = workingCopy?.parents[0] ?? null;
const isOnTrunk = currentChangeId === trunkId;

// Filter changes - exclude the WC itself (it's always empty/scratch)
const wcChangeId = workingCopy?.changeId ?? null;
const changes = allChanges.filter((c) => {
if (c.description.trim() !== "" || c.hasConflicts) return true;
if (c.changeId === wcChangeId) return false;
return !c.isEmpty;
});

// Get bookmark tracking to find modified bookmarks
const trackingResult = await getBookmarkTracking();
const modifiedBookmarks = new Set<string>();
if (trackingResult.ok) {
for (const s of trackingResult.value) {
if (s.aheadCount > 0) modifiedBookmarks.add(s.name);
}
export async function status(
options: { debug?: boolean } = {},
): Promise<Result<StatusResult>> {
const debug = options.debug ?? false;
const t0 = Date.now();

// Run jjStatus (single template call) and getDiffStats in parallel
const [statusResult, statsResult, trunkBranch] = await Promise.all([
(async () => {
const t = Date.now();
const r = await jjStatus();
if (debug) console.log(` jjStatus: ${Date.now() - t}ms`);
return r;
})(),
(async () => {
const t = Date.now();
const r = await getDiffStats("@");
if (debug) console.log(` getDiffStats: ${Date.now() - t}ms`);
return r;
})(),
(async () => {
const t = Date.now();
const r = await getTrunk();
if (debug) console.log(` getTrunk: ${Date.now() - t}ms`);
return r;
})(),
]);

if (debug) {
console.log(` parallel calls: ${Date.now() - t0}ms`);
}

const roots = buildTree(changes, trunkId);
const entries = flattenTree(roots, currentChangeId, modifiedBookmarks);
if (!statusResult.ok) return statusResult;

const { modifiedFiles, conflicts, parents } = statusResult.value;
const {
workingCopy,
parents,
modifiedFiles,
conflicts,
hasResolvedConflict,
} = statusResult.value;

// Current change is the parent, not the WC
const currentChange = parents[0] ?? null;
const hasChanges = modifiedFiles.length > 0;
const hasConflicts = conflicts.length > 0;
const parentHasConflicts = currentChange?.hasConflicts ?? false;
const isUndescribed = currentChange?.description.trim() === "";
const isOnTrunk = currentChange?.bookmarks.includes(trunkBranch) ?? false;

// Build stack path
// Build stack path from parent bookmarks
const stackPath: string[] = [];
const parentIds = new Set(parents.map((p) => p.changeId));
for (const entry of entries) {
if (parentIds.has(entry.change.changeId)) {
const label = entry.change.bookmarks[0] || entry.change.description;
if (label) stackPath.push(label);
for (const pid of entry.change.parents) parentIds.add(pid);
}
if (stackPath.length >= 3) {
stackPath.push("...");
break;
}
if (currentChange) {
const label = currentChange.bookmarks[0] || currentChange.description || "";
if (label) stackPath.push(label);
}
stackPath.push(trunkBranch);

Expand All @@ -96,15 +82,11 @@ export async function status(): Promise<Result<StatusResult>> {
} else if (isOnTrunk) {
nextAction = { action: "create", reason: "on_trunk" };
} else {
const currentEntry = entries.find((e) => e.isCurrent);
const hasBookmark =
currentEntry && currentEntry.change.bookmarks.length > 0;
const currentModified = currentEntry?.isModified ?? false;

if (modifiedFiles.length > 0 || currentModified) {
const hasBookmark = currentChange && currentChange.bookmarks.length > 0;
if (modifiedFiles.length > 0) {
nextAction = {
action: "submit",
reason: hasBookmark && currentModified ? "update_pr" : "create_pr",
reason: hasBookmark ? "update_pr" : "create_pr",
};
} else {
nextAction = { action: "up", reason: "start_new" };
Expand All @@ -126,14 +108,16 @@ export async function status(): Promise<Result<StatusResult>> {
nextAction,
};

// Get diff stats for uncommitted work in WC
const statsResult = await getDiffStats("@");
const stats = statsResult.ok ? statsResult.value : null;

return ok({ info, stats });
if (debug) {
console.log(` TOTAL: ${Date.now() - t0}ms`);
}

return ok({ info, stats, hasResolvedConflict });
}

export const statusCommand: Command<StatusResult> = {
export const statusCommand: Command<StatusResult, [{ debug?: boolean }?]> = {
meta: {
name: "status",
description: "Show the current change and working copy modifications",
Expand Down
14 changes: 8 additions & 6 deletions packages/core/src/commands/track.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Engine } from "../engine";
import { ensureBookmark, findChange, getTrunk, list, status } from "../jj";
import { ensureBookmark, findChange, getTrunk, list } from "../jj";
import { createError, err, ok, type Result } from "../result";
import { datePrefixedLabel } from "../slugify";
import type { Command } from "./types";
Expand Down Expand Up @@ -42,13 +42,15 @@ export async function track(

if (!target) {
// No target - use current change (@-)
const statusResult = await status();
if (!statusResult.ok) return statusResult;

const current = statusResult.value.parents[0];
if (!current) {
const findResult = await findChange("@-", { includeBookmarks: true });
if (!findResult.ok) return findResult;
if (findResult.value.status === "none") {
return err(createError("INVALID_STATE", "No current change"));
}
if (findResult.value.status === "multiple") {
return err(createError("INVALID_STATE", "Unexpected multiple matches"));
}
const current = findResult.value.change;
changeId = current.changeId;
description = current.description;
existingBookmark = current.bookmarks[0];
Expand Down
12 changes: 5 additions & 7 deletions packages/core/src/engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
deleteMetadata,
listTrackedBranches,
type PRInfo,
readMetadata,
readMetadataBatch,
writeMetadata,
} from "../git/metadata";
import { getTrunk, list } from "../jj";
Expand Down Expand Up @@ -84,13 +84,11 @@ export function createEngine(cwd: string = process.cwd()): Engine {
load(): void {
if (loaded) return;

// Load metadata from git refs
// Load metadata from git refs - single git call for all branches
const tracked = listTrackedBranches(cwd);
for (const [bookmarkName] of tracked) {
const meta = readMetadata(bookmarkName, cwd);
if (meta) {
branches.set(bookmarkName, meta);
}
const metadataMap = readMetadataBatch(tracked, cwd);
for (const [bookmarkName, meta] of metadataMap) {
branches.set(bookmarkName, meta);
}

loaded = true;
Expand Down
Loading
Loading