Refactor worktree create request parsing (#885)
This commit is contained in:
+14
-68
@@ -1,21 +1,17 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import { buildCreateWorktreeRequest, type WorktreeCreateOptions } from "./create-input.js";
|
||||||
buildCreateWorktreeInput,
|
|
||||||
toDaemonCreateInput,
|
|
||||||
type WorktreeCreateOptions,
|
|
||||||
} from "./create.js";
|
|
||||||
|
|
||||||
const REPO = "/tmp/repo";
|
const REPO = "/tmp/repo";
|
||||||
|
|
||||||
function build(options: WorktreeCreateOptions): unknown {
|
function build(options: WorktreeCreateOptions): unknown {
|
||||||
try {
|
try {
|
||||||
return buildCreateWorktreeInput(options, REPO);
|
return buildCreateWorktreeRequest(options, REPO);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("buildCreateWorktreeInput", () => {
|
describe("buildCreateWorktreeRequest", () => {
|
||||||
it("requires --mode", () => {
|
it("requires --mode", () => {
|
||||||
expect(build({})).toMatchObject({ code: "MISSING_MODE" });
|
expect(build({})).toMatchObject({ code: "MISSING_MODE" });
|
||||||
});
|
});
|
||||||
@@ -28,17 +24,20 @@ describe("buildCreateWorktreeInput", () => {
|
|||||||
expect(build({ mode: "branch-off" })).toMatchObject({ code: "MISSING_NEW_BRANCH" });
|
expect(build({ mode: "branch-off" })).toMatchObject({ code: "MISSING_NEW_BRANCH" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("branch-off parses with new branch only", () => {
|
it("branch-off builds a daemon request with a new branch", () => {
|
||||||
expect(build({ mode: "branch-off", newBranch: "feature-x" })).toEqual({
|
expect(build({ mode: "branch-off", newBranch: "feature-x" })).toEqual({
|
||||||
cwd: REPO,
|
cwd: REPO,
|
||||||
target: { mode: "branch-off", newBranch: "feature-x" },
|
worktreeSlug: "feature-x",
|
||||||
|
action: "branch-off",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("branch-off parses with base ref", () => {
|
it("branch-off includes the base ref when provided", () => {
|
||||||
expect(build({ mode: "branch-off", newBranch: "feature-x", base: "main" })).toEqual({
|
expect(build({ mode: "branch-off", newBranch: "feature-x", base: "main" })).toEqual({
|
||||||
cwd: REPO,
|
cwd: REPO,
|
||||||
target: { mode: "branch-off", newBranch: "feature-x", base: "main" },
|
worktreeSlug: "feature-x",
|
||||||
|
action: "branch-off",
|
||||||
|
refName: "main",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,10 +45,11 @@ describe("buildCreateWorktreeInput", () => {
|
|||||||
expect(build({ mode: "checkout-branch" })).toMatchObject({ code: "MISSING_BRANCH" });
|
expect(build({ mode: "checkout-branch" })).toMatchObject({ code: "MISSING_BRANCH" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("checkout-branch parses with branch", () => {
|
it("checkout-branch builds a checkout request for the branch", () => {
|
||||||
expect(build({ mode: "checkout-branch", branch: "feat/x" })).toEqual({
|
expect(build({ mode: "checkout-branch", branch: "feat/x" })).toEqual({
|
||||||
cwd: REPO,
|
cwd: REPO,
|
||||||
target: { mode: "checkout-branch", branch: "feat/x" },
|
action: "checkout",
|
||||||
|
refName: "feat/x",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,62 +69,8 @@ describe("buildCreateWorktreeInput", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("checkout-pr parses positive integers", () => {
|
it("checkout-pr builds a checkout request for the pull request", () => {
|
||||||
expect(build({ mode: "checkout-pr", prNumber: "42" })).toEqual({
|
expect(build({ mode: "checkout-pr", prNumber: "42" })).toEqual({
|
||||||
cwd: REPO,
|
|
||||||
target: { mode: "checkout-pr", prNumber: 42 },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("toDaemonCreateInput", () => {
|
|
||||||
it("maps branch-off without base", () => {
|
|
||||||
expect(
|
|
||||||
toDaemonCreateInput({
|
|
||||||
cwd: REPO,
|
|
||||||
target: { mode: "branch-off", newBranch: "feature-x" },
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
cwd: REPO,
|
|
||||||
worktreeSlug: "feature-x",
|
|
||||||
action: "branch-off",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps branch-off with base ref", () => {
|
|
||||||
expect(
|
|
||||||
toDaemonCreateInput({
|
|
||||||
cwd: REPO,
|
|
||||||
target: { mode: "branch-off", newBranch: "feature-x", base: "main" },
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
cwd: REPO,
|
|
||||||
worktreeSlug: "feature-x",
|
|
||||||
action: "branch-off",
|
|
||||||
refName: "main",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps checkout-branch to action=checkout + refName", () => {
|
|
||||||
expect(
|
|
||||||
toDaemonCreateInput({
|
|
||||||
cwd: REPO,
|
|
||||||
target: { mode: "checkout-branch", branch: "feat/x" },
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
cwd: REPO,
|
|
||||||
action: "checkout",
|
|
||||||
refName: "feat/x",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps checkout-pr to action=checkout + githubPrNumber", () => {
|
|
||||||
expect(
|
|
||||||
toDaemonCreateInput({
|
|
||||||
cwd: REPO,
|
|
||||||
target: { mode: "checkout-pr", prNumber: 42 },
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
cwd: REPO,
|
cwd: REPO,
|
||||||
action: "checkout",
|
action: "checkout",
|
||||||
githubPrNumber: 42,
|
githubPrNumber: 42,
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import type { DaemonClient } from "@getpaseo/server";
|
||||||
|
import type { CommandError, CommandOptions } from "../../output/index.js";
|
||||||
|
|
||||||
|
export interface WorktreeCreateOptions extends CommandOptions {
|
||||||
|
host?: string;
|
||||||
|
cwd?: string;
|
||||||
|
mode?: string;
|
||||||
|
newBranch?: string;
|
||||||
|
base?: string;
|
||||||
|
branch?: string;
|
||||||
|
prNumber?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_MODES = ["branch-off", "checkout-branch", "checkout-pr"] as const;
|
||||||
|
|
||||||
|
type CreatePaseoWorktreeRequest = Parameters<DaemonClient["createPaseoWorktree"]>[0];
|
||||||
|
|
||||||
|
export function buildCreateWorktreeRequest(
|
||||||
|
options: WorktreeCreateOptions,
|
||||||
|
cwd: string,
|
||||||
|
): CreatePaseoWorktreeRequest {
|
||||||
|
const mode = options.mode;
|
||||||
|
if (!mode) {
|
||||||
|
throw cmdError(
|
||||||
|
"MISSING_MODE",
|
||||||
|
"--mode is required",
|
||||||
|
`Expected one of: ${VALID_MODES.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case "branch-off":
|
||||||
|
return buildBranchOffRequest(options, cwd);
|
||||||
|
case "checkout-branch":
|
||||||
|
return buildCheckoutBranchRequest(options, cwd);
|
||||||
|
case "checkout-pr":
|
||||||
|
return buildCheckoutPrRequest(options, cwd);
|
||||||
|
default:
|
||||||
|
throw cmdError(
|
||||||
|
"INVALID_MODE",
|
||||||
|
`Invalid --mode: ${mode}`,
|
||||||
|
`Expected one of: ${VALID_MODES.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBranchOffRequest(
|
||||||
|
options: WorktreeCreateOptions,
|
||||||
|
cwd: string,
|
||||||
|
): CreatePaseoWorktreeRequest {
|
||||||
|
if (!options.newBranch) {
|
||||||
|
throw cmdError("MISSING_NEW_BRANCH", "--new-branch is required for --mode branch-off");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
worktreeSlug: options.newBranch,
|
||||||
|
action: "branch-off",
|
||||||
|
...(options.base ? { refName: options.base } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCheckoutBranchRequest(
|
||||||
|
options: WorktreeCreateOptions,
|
||||||
|
cwd: string,
|
||||||
|
): CreatePaseoWorktreeRequest {
|
||||||
|
if (!options.branch) {
|
||||||
|
throw cmdError("MISSING_BRANCH", "--branch is required for --mode checkout-branch");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
action: "checkout",
|
||||||
|
refName: options.branch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCheckoutPrRequest(
|
||||||
|
options: WorktreeCreateOptions,
|
||||||
|
cwd: string,
|
||||||
|
): CreatePaseoWorktreeRequest {
|
||||||
|
if (options.prNumber === undefined || options.prNumber === "") {
|
||||||
|
throw cmdError("MISSING_PR_NUMBER", "--pr-number is required for --mode checkout-pr");
|
||||||
|
}
|
||||||
|
|
||||||
|
const prNumber = Number(options.prNumber);
|
||||||
|
if (!Number.isInteger(prNumber) || prNumber <= 0) {
|
||||||
|
throw cmdError(
|
||||||
|
"INVALID_PR_NUMBER",
|
||||||
|
`Invalid --pr-number: ${options.prNumber}`,
|
||||||
|
"Expected a positive integer",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cwd,
|
||||||
|
action: "checkout",
|
||||||
|
githubPrNumber: prNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmdError(code: string, message: string, details?: string): CommandError {
|
||||||
|
return details ? { code, message, details } : { code, message };
|
||||||
|
}
|
||||||
@@ -2,12 +2,8 @@ import path from "node:path";
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import type { DaemonClient } from "@getpaseo/server";
|
import type { DaemonClient } from "@getpaseo/server";
|
||||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||||
import type {
|
import type { CommandError, OutputSchema, SingleResult } from "../../output/index.js";
|
||||||
CommandError,
|
import { buildCreateWorktreeRequest, type WorktreeCreateOptions } from "./create-input.js";
|
||||||
CommandOptions,
|
|
||||||
OutputSchema,
|
|
||||||
SingleResult,
|
|
||||||
} from "../../output/index.js";
|
|
||||||
|
|
||||||
export interface WorktreeCreateResult {
|
export interface WorktreeCreateResult {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -24,110 +20,6 @@ export const createSchema: OutputSchema<WorktreeCreateResult> = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface WorktreeCreateOptions extends CommandOptions {
|
|
||||||
host?: string;
|
|
||||||
cwd?: string;
|
|
||||||
mode?: string;
|
|
||||||
newBranch?: string;
|
|
||||||
base?: string;
|
|
||||||
branch?: string;
|
|
||||||
prNumber?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WorktreeCreateTarget =
|
|
||||||
| { mode: "branch-off"; newBranch: string; base?: string }
|
|
||||||
| { mode: "checkout-branch"; branch: string }
|
|
||||||
| { mode: "checkout-pr"; prNumber: number };
|
|
||||||
|
|
||||||
export interface ParsedWorktreeCreateInput {
|
|
||||||
cwd: string;
|
|
||||||
target: WorktreeCreateTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VALID_MODES = ["branch-off", "checkout-branch", "checkout-pr"] as const;
|
|
||||||
|
|
||||||
export function buildCreateWorktreeInput(
|
|
||||||
options: WorktreeCreateOptions,
|
|
||||||
cwd: string,
|
|
||||||
): ParsedWorktreeCreateInput {
|
|
||||||
const mode = options.mode;
|
|
||||||
if (!mode) {
|
|
||||||
throw cmdError(
|
|
||||||
"MISSING_MODE",
|
|
||||||
"--mode is required",
|
|
||||||
`Expected one of: ${VALID_MODES.join(", ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (mode) {
|
|
||||||
case "branch-off": {
|
|
||||||
if (!options.newBranch) {
|
|
||||||
throw cmdError("MISSING_NEW_BRANCH", "--new-branch is required for --mode branch-off");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
cwd,
|
|
||||||
target: {
|
|
||||||
mode: "branch-off",
|
|
||||||
newBranch: options.newBranch,
|
|
||||||
...(options.base ? { base: options.base } : {}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "checkout-branch": {
|
|
||||||
if (!options.branch) {
|
|
||||||
throw cmdError("MISSING_BRANCH", "--branch is required for --mode checkout-branch");
|
|
||||||
}
|
|
||||||
return { cwd, target: { mode: "checkout-branch", branch: options.branch } };
|
|
||||||
}
|
|
||||||
case "checkout-pr": {
|
|
||||||
if (options.prNumber === undefined || options.prNumber === "") {
|
|
||||||
throw cmdError("MISSING_PR_NUMBER", "--pr-number is required for --mode checkout-pr");
|
|
||||||
}
|
|
||||||
const parsed = Number(options.prNumber);
|
|
||||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
||||||
throw cmdError(
|
|
||||||
"INVALID_PR_NUMBER",
|
|
||||||
`Invalid --pr-number: ${options.prNumber}`,
|
|
||||||
"Expected a positive integer",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { cwd, target: { mode: "checkout-pr", prNumber: parsed } };
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw cmdError(
|
|
||||||
"INVALID_MODE",
|
|
||||||
`Invalid --mode: ${mode}`,
|
|
||||||
`Expected one of: ${VALID_MODES.join(", ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toDaemonCreateInput(parsed: ParsedWorktreeCreateInput) {
|
|
||||||
switch (parsed.target.mode) {
|
|
||||||
case "branch-off":
|
|
||||||
return {
|
|
||||||
cwd: parsed.cwd,
|
|
||||||
worktreeSlug: parsed.target.newBranch,
|
|
||||||
action: "branch-off" as const,
|
|
||||||
...(parsed.target.base ? { refName: parsed.target.base } : {}),
|
|
||||||
};
|
|
||||||
case "checkout-branch":
|
|
||||||
return {
|
|
||||||
cwd: parsed.cwd,
|
|
||||||
action: "checkout" as const,
|
|
||||||
refName: parsed.target.branch,
|
|
||||||
};
|
|
||||||
case "checkout-pr":
|
|
||||||
return {
|
|
||||||
cwd: parsed.cwd,
|
|
||||||
action: "checkout" as const,
|
|
||||||
githubPrNumber: parsed.target.prNumber,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error("unreachable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmdError(code: string, message: string, details?: string): CommandError {
|
function cmdError(code: string, message: string, details?: string): CommandError {
|
||||||
return details ? { code, message, details } : { code, message };
|
return details ? { code, message, details } : { code, message };
|
||||||
}
|
}
|
||||||
@@ -137,7 +29,7 @@ export async function runCreateCommand(
|
|||||||
_command: Command,
|
_command: Command,
|
||||||
): Promise<SingleResult<WorktreeCreateResult>> {
|
): Promise<SingleResult<WorktreeCreateResult>> {
|
||||||
const cwd = options.cwd ?? process.cwd();
|
const cwd = options.cwd ?? process.cwd();
|
||||||
const parsed = buildCreateWorktreeInput(options, cwd);
|
const request = buildCreateWorktreeRequest(options, cwd);
|
||||||
|
|
||||||
const host = getDaemonHost({ host: options.host });
|
const host = getDaemonHost({ host: options.host });
|
||||||
let client: DaemonClient;
|
let client: DaemonClient;
|
||||||
@@ -153,7 +45,7 @@ export async function runCreateCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client.createPaseoWorktree(toDaemonCreateInput(parsed));
|
const response = await client.createPaseoWorktree(request);
|
||||||
|
|
||||||
const workspace = response.workspace;
|
const workspace = response.workspace;
|
||||||
if (!workspace || response.error) {
|
if (!workspace || response.error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user