mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-24 06:45:22 +00:00
server: reset worktrees against each repo's default branch
This commit is contained in:
@@ -19,7 +19,7 @@ export namespace Git {
|
||||
] as const
|
||||
|
||||
const out = (result: { text(): string }) => result.text().trim()
|
||||
const split = (text: string) => text.split("\0").filter(Boolean)
|
||||
const nuls = (text: string) => text.split("\0").filter(Boolean)
|
||||
const fail = (err: unknown) =>
|
||||
({
|
||||
exitCode: 1,
|
||||
@@ -72,52 +72,14 @@ export namespace Git {
|
||||
readonly stats: (cwd: string, ref: string) => Effect.Effect<Stat[]>
|
||||
}
|
||||
|
||||
const kind = (code?: string): Kind => {
|
||||
const kind = (code: string): Kind => {
|
||||
if (code === "??") return "added"
|
||||
if (code?.includes("U")) return "modified"
|
||||
if (code?.includes("A") && !code.includes("D")) return "added"
|
||||
if (code?.includes("D") && !code.includes("A")) return "deleted"
|
||||
if (code.includes("U")) return "modified"
|
||||
if (code.includes("A") && !code.includes("D")) return "added"
|
||||
if (code.includes("D") && !code.includes("A")) return "deleted"
|
||||
return "modified"
|
||||
}
|
||||
|
||||
const parseStatus = (text: string) =>
|
||||
split(text).flatMap((item) => {
|
||||
const file = item.slice(3)
|
||||
if (!file) return []
|
||||
const code = item.slice(0, 2)
|
||||
return [{ file, code, status: kind(code) } satisfies Item]
|
||||
})
|
||||
|
||||
const parseNames = (text: string) => {
|
||||
const list = split(text)
|
||||
return list.flatMap((code, idx) => {
|
||||
if (idx % 2 !== 0) return []
|
||||
const file = list[idx + 1]
|
||||
if (!code || !file) return []
|
||||
return [{ file, code, status: kind(code) } satisfies Item]
|
||||
})
|
||||
}
|
||||
|
||||
const parseStats = (text: string) =>
|
||||
split(text).flatMap((item) => {
|
||||
const a = item.indexOf("\t")
|
||||
const b = item.indexOf("\t", a + 1)
|
||||
if (a === -1 || b === -1) return []
|
||||
const file = item.slice(b + 1)
|
||||
if (!file) return []
|
||||
const adds = item.slice(0, a)
|
||||
const dels = item.slice(a + 1, b)
|
||||
const additions = adds === "-" ? 0 : Number.parseInt(adds || "0", 10)
|
||||
const deletions = dels === "-" ? 0 : Number.parseInt(dels || "0", 10)
|
||||
return [
|
||||
{
|
||||
file,
|
||||
additions: Number.isFinite(additions) ? additions : 0,
|
||||
deletions: Number.isFinite(deletions) ? deletions : 0,
|
||||
} satisfies Stat,
|
||||
]
|
||||
})
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Git") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
@@ -233,23 +195,51 @@ export namespace Git {
|
||||
})
|
||||
|
||||
const status = Effect.fn("Git.status")(function* (cwd: string) {
|
||||
return parseStatus(
|
||||
return nuls(
|
||||
yield* text(["status", "--porcelain=v1", "--untracked-files=all", "--no-renames", "-z", "--", "."], {
|
||||
cwd,
|
||||
}),
|
||||
)
|
||||
).flatMap((item) => {
|
||||
const file = item.slice(3)
|
||||
if (!file) return []
|
||||
const code = item.slice(0, 2)
|
||||
return [{ file, code, status: kind(code) } satisfies Item]
|
||||
})
|
||||
})
|
||||
|
||||
const diff = Effect.fn("Git.diff")(function* (cwd: string, ref: string) {
|
||||
return parseNames(
|
||||
const list = nuls(
|
||||
yield* text(["diff", "--no-ext-diff", "--no-renames", "--name-status", "-z", ref, "--", "."], { cwd }),
|
||||
)
|
||||
return list.flatMap((code, idx) => {
|
||||
if (idx % 2 !== 0) return []
|
||||
const file = list[idx + 1]
|
||||
if (!code || !file) return []
|
||||
return [{ file, code, status: kind(code) } satisfies Item]
|
||||
})
|
||||
})
|
||||
|
||||
const stats = Effect.fn("Git.stats")(function* (cwd: string, ref: string) {
|
||||
return parseStats(
|
||||
return nuls(
|
||||
yield* text(["diff", "--no-ext-diff", "--no-renames", "--numstat", "-z", ref, "--", "."], { cwd }),
|
||||
)
|
||||
).flatMap((item) => {
|
||||
const a = item.indexOf("\t")
|
||||
const b = item.indexOf("\t", a + 1)
|
||||
if (a === -1 || b === -1) return []
|
||||
const file = item.slice(b + 1)
|
||||
if (!file) return []
|
||||
const adds = item.slice(0, a)
|
||||
const dels = item.slice(a + 1, b)
|
||||
const additions = adds === "-" ? 0 : Number.parseInt(adds || "0", 10)
|
||||
const deletions = dels === "-" ? 0 : Number.parseInt(dels || "0", 10)
|
||||
return [
|
||||
{
|
||||
file,
|
||||
additions: Number.isFinite(additions) ? additions : 0,
|
||||
deletions: Number.isFinite(deletions) ? deletions : 0,
|
||||
} satisfies Stat,
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
|
||||
@@ -331,9 +331,10 @@ export namespace Server {
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const [branch, default_branch] = await Promise.all([Vcs.branch(), Vcs.defaultBranch()])
|
||||
return c.json({
|
||||
branch: await Vcs.branch(),
|
||||
default_branch: await Vcs.defaultBranch(),
|
||||
branch,
|
||||
default_branch,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -573,49 +573,18 @@ export namespace Worktree {
|
||||
throw new ResetFailedError({ message: "Worktree not found" })
|
||||
}
|
||||
|
||||
const remoteList = await Git.run(["remote"], { 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.run(["symbolic-ref", `refs/remotes/${remote}/HEAD`], { 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.run(["show-ref", "--verify", "--quiet", "refs/heads/main"], {
|
||||
cwd: Instance.worktree,
|
||||
})
|
||||
const masterCheck = await Git.run(["show-ref", "--verify", "--quiet", "refs/heads/master"], {
|
||||
cwd: Instance.worktree,
|
||||
})
|
||||
const localBranch = mainCheck.exitCode === 0 ? "main" : masterCheck.exitCode === 0 ? "master" : ""
|
||||
|
||||
const target = remoteBranch ? `${remote}/${remoteBranch}` : localBranch
|
||||
if (!target) {
|
||||
const base = await Git.defaultBranch(Instance.worktree)
|
||||
if (!base) {
|
||||
throw new ResetFailedError({ message: "Default branch not found" })
|
||||
}
|
||||
|
||||
if (remoteBranch) {
|
||||
const fetch = await Git.run(["fetch", remote, remoteBranch], { cwd: Instance.worktree })
|
||||
const sep = base.ref.indexOf("/")
|
||||
if (base.ref !== base.name && sep > 0) {
|
||||
const remote = base.ref.slice(0, sep)
|
||||
const branch = base.ref.slice(sep + 1)
|
||||
const fetch = await Git.run(["fetch", remote, branch], { cwd: Instance.worktree })
|
||||
if (fetch.exitCode !== 0) {
|
||||
throw new ResetFailedError({ message: errorText(fetch) || `Failed to fetch ${target}` })
|
||||
throw new ResetFailedError({ message: errorText(fetch) || `Failed to fetch ${base.ref}` })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +594,7 @@ export namespace Worktree {
|
||||
|
||||
const worktreePath = entry.path
|
||||
|
||||
const resetToTarget = await Git.run(["reset", "--hard", target], { cwd: worktreePath })
|
||||
const resetToTarget = await Git.run(["reset", "--hard", base.ref], { cwd: worktreePath })
|
||||
if (resetToTarget.exitCode !== 0) {
|
||||
throw new ResetFailedError({ message: errorText(resetToTarget) || "Failed to reset worktree to target" })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user