feat(app): reset worktree
This commit is contained in:
@@ -159,6 +159,31 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/worktree/reset",
|
||||
describeRoute({
|
||||
summary: "Reset worktree",
|
||||
description: "Reset a worktree branch to the primary default branch.",
|
||||
operationId: "worktree.reset",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Worktree reset",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("json", Worktree.reset.schema),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
await Worktree.reset(body)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/resource",
|
||||
describeRoute({
|
||||
|
||||
@@ -43,6 +43,16 @@ export namespace Worktree {
|
||||
|
||||
export type RemoveInput = z.infer<typeof RemoveInput>
|
||||
|
||||
export const ResetInput = z
|
||||
.object({
|
||||
directory: z.string(),
|
||||
})
|
||||
.meta({
|
||||
ref: "WorktreeResetInput",
|
||||
})
|
||||
|
||||
export type ResetInput = z.infer<typeof ResetInput>
|
||||
|
||||
export const NotGitError = NamedError.create(
|
||||
"WorktreeNotGitError",
|
||||
z.object({
|
||||
@@ -78,6 +88,13 @@ export namespace Worktree {
|
||||
}),
|
||||
)
|
||||
|
||||
export const ResetFailedError = NamedError.create(
|
||||
"WorktreeResetFailedError",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
const ADJECTIVES = [
|
||||
"brave",
|
||||
"calm",
|
||||
@@ -280,4 +297,114 @@ export namespace Worktree {
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
export const reset = fn(ResetInput, async (input) => {
|
||||
if (Instance.project.vcs !== "git") {
|
||||
throw new NotGitError({ message: "Worktrees are only supported for git projects" })
|
||||
}
|
||||
|
||||
const directory = path.resolve(input.directory)
|
||||
if (directory === path.resolve(Instance.worktree)) {
|
||||
throw new ResetFailedError({ message: "Cannot reset the primary workspace" })
|
||||
}
|
||||
|
||||
const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
|
||||
if (list.exitCode !== 0) {
|
||||
throw new ResetFailedError({ message: errorText(list) || "Failed to read git worktrees" })
|
||||
}
|
||||
|
||||
const lines = outputText(list.stdout)
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => {
|
||||
if (!line) return acc
|
||||
if (line.startsWith("worktree ")) {
|
||||
acc.push({ path: line.slice("worktree ".length).trim() })
|
||||
return acc
|
||||
}
|
||||
const current = acc[acc.length - 1]
|
||||
if (!current) return acc
|
||||
if (line.startsWith("branch ")) {
|
||||
current.branch = line.slice("branch ".length).trim()
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const entry = entries.find((item) => item.path && path.resolve(item.path) === directory)
|
||||
if (!entry?.path) {
|
||||
throw new ResetFailedError({ message: "Worktree not found" })
|
||||
}
|
||||
|
||||
const remoteList = await $`git remote`.quiet().nothrow().cwd(Instance.worktree)
|
||||
if (remoteList.exitCode !== 0) {
|
||||
throw new ResetFailedError({ message: errorText(remoteList) || "Failed to list git remotes" })
|
||||
}
|
||||
|
||||
const remotes = outputText(remoteList.stdout)
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const remote = remotes.includes("origin")
|
||||
? "origin"
|
||||
: remotes.length === 1
|
||||
? remotes[0]
|
||||
: remotes.includes("upstream")
|
||||
? "upstream"
|
||||
: ""
|
||||
|
||||
const remoteHead = remote
|
||||
? await $`git symbolic-ref refs/remotes/${remote}/HEAD`.quiet().nothrow().cwd(Instance.worktree)
|
||||
: { exitCode: 1, stdout: undefined, stderr: undefined }
|
||||
|
||||
const remoteRef = remoteHead.exitCode === 0 ? outputText(remoteHead.stdout) : ""
|
||||
const remoteTarget = remoteRef ? remoteRef.replace(/^refs\/remotes\//, "") : ""
|
||||
const remoteBranch = remote && remoteTarget.startsWith(`${remote}/`) ? remoteTarget.slice(`${remote}/`.length) : ""
|
||||
|
||||
const mainCheck = await $`git show-ref --verify --quiet refs/heads/main`.quiet().nothrow().cwd(Instance.worktree)
|
||||
const masterCheck = await $`git show-ref --verify --quiet refs/heads/master`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(Instance.worktree)
|
||||
const localBranch = mainCheck.exitCode === 0 ? "main" : masterCheck.exitCode === 0 ? "master" : ""
|
||||
|
||||
const target = remoteBranch ? `${remote}/${remoteBranch}` : localBranch
|
||||
if (!target) {
|
||||
throw new ResetFailedError({ message: "Default branch not found" })
|
||||
}
|
||||
|
||||
if (remoteBranch) {
|
||||
const fetch = await $`git fetch ${remote} ${remoteBranch}`.quiet().nothrow().cwd(Instance.worktree)
|
||||
if (fetch.exitCode !== 0) {
|
||||
throw new ResetFailedError({ message: errorText(fetch) || `Failed to fetch ${target}` })
|
||||
}
|
||||
}
|
||||
|
||||
const checkout = await $`git checkout ${target}`.quiet().nothrow().cwd(entry.path)
|
||||
if (checkout.exitCode !== 0) {
|
||||
throw new ResetFailedError({ message: errorText(checkout) || `Failed to checkout ${target}` })
|
||||
}
|
||||
|
||||
const worktreeBranch = entry.branch?.replace(/^refs\/heads\//, "")
|
||||
if (!worktreeBranch) {
|
||||
throw new ResetFailedError({ message: "Worktree branch not found" })
|
||||
}
|
||||
|
||||
const reset = await $`git reset --hard ${target}`.quiet().nothrow().cwd(entry.path)
|
||||
if (reset.exitCode !== 0) {
|
||||
throw new ResetFailedError({ message: errorText(reset) || "Failed to reset worktree" })
|
||||
}
|
||||
|
||||
const branchReset = await $`git branch -f ${worktreeBranch} ${target}`.quiet().nothrow().cwd(entry.path)
|
||||
if (branchReset.exitCode !== 0) {
|
||||
throw new ResetFailedError({ message: errorText(branchReset) || "Failed to update worktree branch" })
|
||||
}
|
||||
|
||||
const checkoutBranch = await $`git checkout ${worktreeBranch}`.quiet().nothrow().cwd(entry.path)
|
||||
if (checkoutBranch.exitCode !== 0) {
|
||||
throw new ResetFailedError({ message: errorText(checkoutBranch) || "Failed to checkout worktree branch" })
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user