diff --git a/src/commands/checkout.ts b/src/commands/checkout.ts index 86afe69..6276c5b 100644 --- a/src/commands/checkout.ts +++ b/src/commands/checkout.ts @@ -1,33 +1,19 @@ -import { join } from "path" -import { unlink } from "fs/promises" import * as git from "../git.ts" -import * as vm from "../vm.ts" -import * as state from "../state.ts" import { die } from "../fmt.ts" +import { requireSession, teardownSession } from "./helpers.ts" export async function action(branch: string, opts: { force?: boolean } = {}) { - const root = await git.repoRoot() - const session = await state.getSession(root, branch) + const { root, session } = await requireSession(branch) - if (!session) { - die(`No session found for branch "${branch}"`) - } - - const worktreeAbs = session.worktree - - if (!opts.force && await git.isDirty(worktreeAbs)) { + if (!opts.force && await git.isDirty(session.worktree)) { die(`Branch "${branch}" has unsaved changes. Run "sandlot save ${branch}" first, or use -f to force.`) } - await vm.clearActivity(worktreeAbs, branch) + if (!opts.force && await git.isDirty(root)) { + die(`Working tree has uncommitted changes that may conflict with checkout. Commit or stash them first, or use -f to force.`) + } - await git.removeWorktree(worktreeAbs, root) - .catch((e) => console.warn(`⚠ Failed to remove worktree: ${e.message}`)) - - await unlink(join(root, '.sandlot', branch)) - .catch(() => {}) // symlink may not exist - - await state.removeSession(root, branch) + await teardownSession(root, branch, session.worktree) await git.checkout(branch, root) diff --git a/src/commands/close.ts b/src/commands/close.ts index c1847b5..40f53c4 100644 --- a/src/commands/close.ts +++ b/src/commands/close.ts @@ -1,36 +1,18 @@ -import { join } from "path" -import { unlink } from "fs/promises" import * as git from "../git.ts" -import * as vm from "../vm.ts" -import * as state from "../state.ts" import { die } from "../fmt.ts" +import { requireSession, teardownSession } from "./helpers.ts" export async function action(branch: string, opts: { force?: boolean } = {}) { - const root = await git.repoRoot() - const session = await state.getSession(root, branch) + const { root, session } = await requireSession(branch) - if (!session) { - die(`No session found for branch "${branch}"`) - } - - const worktreeAbs = session.worktree - - if (!opts.force && await git.isDirty(worktreeAbs)) { + if (!opts.force && await git.isDirty(session.worktree)) { die(`Branch "${branch}" has unsaved changes. Run "sandlot save ${branch}" first, or use -f to force.`) } - await vm.clearActivity(worktreeAbs, branch) - - await git.removeWorktree(worktreeAbs, root) - .catch((e) => console.warn(`⚠ Failed to remove worktree: ${e.message}`)) - - await unlink(join(root, '.sandlot', branch)) - .catch(() => {}) // symlink may not exist + await teardownSession(root, branch, session.worktree) await git.deleteLocalBranch(branch, root) .catch((e) => console.warn(`⚠ Failed to delete branch ${branch}: ${e.message}`)) - await state.removeSession(root, branch) - console.log(`✔ Closed session ${branch}`) } diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 334da75..72c0529 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -1,11 +1,11 @@ import { join } from "path" +import { unlink } from "fs/promises" import { $ } from "bun" import * as git from "../git.ts" import * as vm from "../vm.ts" import * as state from "../state.ts" import { spinner } from "../spinner.ts" import { die } from "../fmt.ts" -import { action as closeAction } from "./close.ts" import type { Session } from "../state.ts" /** Look up a session by branch, dying if it doesn't exist. */ @@ -18,6 +18,19 @@ export async function requireSession(branch: string): Promise<{ root: string; se return { root, session } } +/** Tear down a session: clear activity, remove worktree, unlink symlink, remove state. */ +export async function teardownSession(root: string, branch: string, worktree: string): Promise { + await vm.clearActivity(worktree, branch) + + await git.removeWorktree(worktree, root) + .catch((e) => console.warn(`⚠ Failed to remove worktree: ${e.message}`)) + + await unlink(join(root, '.sandlot', branch)) + .catch(() => {}) // symlink may not exist + + await state.removeSession(root, branch) +} + /** Resolve conflict markers in files using Claude, then stage them. */ export async function resolveConflicts( files: string[], @@ -63,7 +76,8 @@ export async function mergeAndClose(branch: string, opts?: { squash?: boolean }) await squashCommit(branch, root) } spin.succeed(`${label} ${branch} into current branch`) - await closeAction(branch) + if (session) await teardownSession(root, branch, session.worktree) + await git.deleteLocalBranch(branch, root).catch(() => {}) return } @@ -87,7 +101,8 @@ export async function mergeAndClose(branch: string, opts?: { squash?: boolean }) process.exit(1) } - await closeAction(branch) + if (session) await teardownSession(root, branch, session.worktree) + await git.deleteLocalBranch(branch, root).catch(() => {}) } /** Generate a commit message for a squash-merge via Claude and commit. */