diff --git a/src/cmd/lib/checkout.ts b/src/cmd/lib/checkout.ts index de9037f7..4e75553d 100644 --- a/src/cmd/lib/checkout.ts +++ b/src/cmd/lib/checkout.ts @@ -144,11 +144,6 @@ export const checkoutCmd = new Command() }, ); - // Note that if they try to check out to the same branch, we can't - // even figure that out, because we store the branch ID, not the - // branch name. So this warning, while useful, won't show up if they - // are checking out FROM a branch that has been deleted and currently - // does not exist. if (currentBranchData.name === branchName) { spinner.warn( `You are already on branch "${dryCheckoutResult.fromBranch.name}"`, diff --git a/src/cmd/lib/clone.ts b/src/cmd/lib/clone.ts index b7709531..7642ae8f 100644 --- a/src/cmd/lib/clone.ts +++ b/src/cmd/lib/clone.ts @@ -112,6 +112,7 @@ export const cloneCmd = new Command() rootPath: clonePath, valName, username: ownerName, + branchName, }); if (editorFiles) { diff --git a/src/cmd/tests/checkout_test.ts b/src/cmd/tests/checkout_test.ts index 9a5e09ef..cdda460f 100644 --- a/src/cmd/tests/checkout_test.ts +++ b/src/cmd/tests/checkout_test.ts @@ -2,8 +2,12 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; import { join } from "@std/path"; import sdk from "~/sdk.ts"; -import { runVtCommand, streamVtCommand } from "~/cmd/tests/utils.ts"; -import { assert, assertStringIncludes } from "@std/assert"; +import { + readPersistedBranchName, + runVtCommand, + streamVtCommand, +} from "~/cmd/tests/utils.ts"; +import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { exists } from "@std/fs"; import type ValTown from "@valtown/sdk"; @@ -50,6 +54,12 @@ Deno.test({ tmpDir, ); + assertEquals( + await readPersistedBranchName(fullPath), + "main", + "clone should persist the checked-out branch name", + ); + // Make a remote change to main branch after cloning await sdk.vals.files.update( val.id, @@ -85,6 +95,12 @@ Deno.test({ "feature.ts should exist after checkout; we're not on feature branch", ); + assertEquals( + await readPersistedBranchName(fullPath), + "feature-branch", + "checkout should persist the target branch name", + ); + const [statusOutput] = await runVtCommand(["status"], fullPath); assertStringIncludes( statusOutput, @@ -147,6 +163,12 @@ Deno.test({ checkoutOutput, 'Created and switched to new branch "feature-with-changes"', ); + + assertEquals( + await readPersistedBranchName(fullPath), + "feature-with-changes", + "checkout -b should persist the new branch name", + ); }); await t.step("verify local changes are preserved", async () => { diff --git a/src/cmd/tests/clone_test.ts b/src/cmd/tests/clone_test.ts index 3eebd05e..f7ab6061 100644 --- a/src/cmd/tests/clone_test.ts +++ b/src/cmd/tests/clone_test.ts @@ -8,6 +8,7 @@ import { import { exists } from "@std/fs"; import { join } from "@std/path"; import { + readPersistedBranchName, runVtCommand, streamVtCommand, waitForStable, @@ -15,6 +16,7 @@ import { import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; import sdk, { getCurrentUser, randomValName } from "~/sdk.ts"; import type { ValFileType } from "~/types.ts"; +import { DEFAULT_BRANCH_NAME } from "~/consts.ts"; Deno.test({ name: "clone preserves custom deno.json and .vtignore", @@ -129,6 +131,12 @@ Deno.test({ ], tmpDir); assertStringIncludes(output, "cloned to"); + assertEquals( + await readPersistedBranchName(cloneDir), + DEFAULT_BRANCH_NAME, + "clone should persist the checked-out branch name", + ); + // Verify the files exist const testJsExists = await exists(join(cloneDir, "test.js")); assertEquals(testJsExists, true, "test.js should exist"); @@ -148,6 +156,29 @@ Deno.test({ "content of test_inner.js should match", ); }); + + await t.step("clone a non-default branch", async () => { + const branchName = "feature-branch"; + await sdk.vals.branches.create( + val.id, + { name: branchName, branchId: branch.id }, + ); + + const cloneDir = join(tmpDir, "feature-clone"); + await runVtCommand([ + "clone", + val.name, + cloneDir, + branchName, + "--no-editor-files", + ], tmpDir); + + assertEquals( + await readPersistedBranchName(cloneDir), + branchName, + "clone should persist a non-default checked-out branch name", + ); + }); }); }); }, diff --git a/src/cmd/tests/create_test.ts b/src/cmd/tests/create_test.ts index 0ac47810..9c0f0e7b 100644 --- a/src/cmd/tests/create_test.ts +++ b/src/cmd/tests/create_test.ts @@ -15,7 +15,11 @@ import sdk, { listValItems, randomValName, } from "~/sdk.ts"; -import { runVtCommand, streamVtCommand } from "~/cmd/tests/utils.ts"; +import { + readPersistedBranchName, + runVtCommand, + streamVtCommand, +} from "~/cmd/tests/utils.ts"; import { DEFAULT_BRANCH_NAME } from "~/consts.ts"; import { delay } from "@std/async"; @@ -112,6 +116,12 @@ Deno.test({ await exists(join(tmpDir, newValName)), "val was not cloned to target", ); + + assertEquals( + await readPersistedBranchName(join(tmpDir, newValName)), + DEFAULT_BRANCH_NAME, + "create should persist the initial branch name", + ); }); }); } finally { diff --git a/src/cmd/tests/remix_test.ts b/src/cmd/tests/remix_test.ts index 53e51e00..6396cd9d 100644 --- a/src/cmd/tests/remix_test.ts +++ b/src/cmd/tests/remix_test.ts @@ -1,10 +1,10 @@ import { doWithNewVal } from "~/vt/lib/tests/utils.ts"; import { join } from "@std/path"; import sdk, { getCurrentUser } from "~/sdk.ts"; -import { runVtCommand } from "~/cmd/tests/utils.ts"; -import { assert, assertStringIncludes } from "@std/assert"; +import { readPersistedBranchName, runVtCommand } from "~/cmd/tests/utils.ts"; +import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { exists } from "@std/fs"; -import { META_FOLDER_NAME } from "~/consts.ts"; +import { DEFAULT_BRANCH_NAME, META_FOLDER_NAME } from "~/consts.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; Deno.test({ @@ -44,6 +44,12 @@ Deno.test({ await exists(join(remixedValPath, META_FOLDER_NAME)), "remixed Val should have .vt metadata folder", ); + + assertEquals( + await readPersistedBranchName(remixedValPath), + DEFAULT_BRANCH_NAME, + "remix should persist the initial branch name", + ); }); // Clean up the remixed val diff --git a/src/cmd/tests/utils.ts b/src/cmd/tests/utils.ts index 3fee3c99..c252a55d 100644 --- a/src/cmd/tests/utils.ts +++ b/src/cmd/tests/utils.ts @@ -12,6 +12,7 @@ import { ENTRYPOINT_NAME } from "~/consts.ts"; import { doWithTempDir } from "~/vt/lib/utils/misc.ts"; import { parseValUri } from "~/cmd/lib/utils/parsing.ts"; import { delay } from "@std/async"; +import VTMeta from "~/vt/vt/VTMeta.ts"; /** * Creates and spawns a Deno child process for the vt.ts script. @@ -42,6 +43,15 @@ export function runVtProc( return command.spawn(); } +/** + * Reads the branch name persisted in a Val's local VT state file. + */ +export async function readPersistedBranchName( + rootPath: string, +): Promise { + return (await new VTMeta(rootPath).loadVtState()).branch.name; +} + /** * Runs the vt.ts script with provided arguments. * diff --git a/src/vt/vt/VTClient.ts b/src/vt/vt/VTClient.ts index 000b9681..5f84ba22 100644 --- a/src/vt/vt/VTClient.ts +++ b/src/vt/vt/VTClient.ts @@ -140,7 +140,7 @@ export default class VTClient { await vt.getMeta().saveVtState({ val: { id: valId }, - branch: { id: branch.id, version: version }, + branch: { id: branch.id, name: branch.name, version: version }, }); return vt; @@ -427,7 +427,7 @@ export default class VTClient { // Save the VT state await vt.getMeta().saveVtState({ val: { id: valId }, - branch: { id: branch.id, version }, + branch: { id: branch.id, name: branch.name, version }, }); // Perform the clone @@ -608,6 +608,7 @@ export default class VTClient { if (!baseParams.dryRun) { if (result.toBranch) { vtState.branch.id = result.toBranch.id; + vtState.branch.name = result.toBranch.name; vtState.branch.version = FIRST_VERSION_NUMBER; // Set version to 1 for the new branch } } @@ -637,6 +638,7 @@ export default class VTClient { if (!baseParams.dryRun) { if (result.toBranch) { vtState.branch.id = result.toBranch.id; + vtState.branch.name = result.toBranch.name; vtState.branch.version = result.toBranch.version; // Use the target branch's version } } diff --git a/src/vt/vt/schemas.ts b/src/vt/vt/schemas.ts index d2142c02..599a1674 100644 --- a/src/vt/vt/schemas.ts +++ b/src/vt/vt/schemas.ts @@ -32,6 +32,14 @@ export const VTStateSchema = z .catch({ id: "" }), branch: z.object({ id: z.string().uuid(), + // Store the exact Val Town branch name in JSON so shell prompts and + // other local tooling can read it without making an API call. + // + // Keep this optional so existing checkouts created before this field was + // added continue to load; new state writes populate it whenever the + // branch is known. JSON.stringify will safely escape control characters + // if a branch name is displayed by bash/zsh prompt integrations. + name: z.string().optional(), version: z.number().gte(0), }), lastRun: z.object({