feat(core): copy file changes when warping (#26190)
This commit is contained in:
@@ -6,7 +6,7 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { Git } from "@/git"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { zod, zodObject } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
|
||||
const log = Log.create({ service: "vcs" })
|
||||
@@ -239,11 +239,39 @@ export const FileDiff = Schema.Struct({
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type FileDiff = Schema.Schema.Type<typeof FileDiff>
|
||||
|
||||
export const FileStatus = Schema.Struct({
|
||||
file: Schema.String,
|
||||
additions: NonNegativeInt,
|
||||
deletions: NonNegativeInt,
|
||||
status: Schema.Literals(["added", "deleted", "modified"]),
|
||||
})
|
||||
.annotate({ identifier: "VcsFileStatus" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type FileStatus = Schema.Schema.Type<typeof FileStatus>
|
||||
|
||||
export const ApplyInput = Schema.Struct({
|
||||
patch: Schema.String,
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s), zodObject: zodObject(s) })))
|
||||
export type ApplyInput = Schema.Schema.Type<typeof ApplyInput>
|
||||
|
||||
export const ApplyResult = Schema.Struct({
|
||||
applied: Schema.Boolean,
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ApplyResult = Schema.Schema.Type<typeof ApplyResult>
|
||||
|
||||
export class PatchApplyError extends Schema.TaggedErrorClass<PatchApplyError>()("VcsPatchApplyError", {
|
||||
message: Schema.String,
|
||||
reason: Schema.Literals(["non-git", "not-clean"]),
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly init: () => Effect.Effect<void>
|
||||
readonly branch: () => Effect.Effect<string | undefined>
|
||||
readonly defaultBranch: () => Effect.Effect<string | undefined>
|
||||
readonly status: () => Effect.Effect<FileStatus[]>
|
||||
readonly diff: (mode: Mode) => Effect.Effect<FileDiff[]>
|
||||
readonly diffRaw: () => Effect.Effect<string>
|
||||
readonly apply: (input: ApplyInput) => Effect.Effect<ApplyResult, PatchApplyError>
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -304,6 +332,31 @@ export const layer: Layer.Layer<Service, never, Git.Service | Bus.Service> = Lay
|
||||
defaultBranch: Effect.fn("Vcs.defaultBranch")(function* () {
|
||||
return yield* InstanceState.use(state, (x) => x.root?.name)
|
||||
}),
|
||||
status: Effect.fn("Vcs.status")(function* () {
|
||||
const ctx = yield* InstanceState.context
|
||||
if (ctx.project.vcs !== "git") return []
|
||||
const ref = (yield* git.hasHead(ctx.directory)) ? "HEAD" : undefined
|
||||
const [list, stats] = yield* Effect.all(
|
||||
[git.status(ctx.directory), ref ? git.stats(ctx.directory, ref) : Effect.succeed([])],
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
const map = nums(stats)
|
||||
return yield* Effect.forEach(
|
||||
list.toSorted((a, b) => a.file.localeCompare(b.file)),
|
||||
(item) =>
|
||||
Effect.gen(function* () {
|
||||
const stat =
|
||||
map.get(item.file) ??
|
||||
(item.status === "added" ? yield* git.statUntracked(ctx.worktree, item.file) : undefined)
|
||||
return {
|
||||
file: item.file,
|
||||
additions: stat?.additions ?? 0,
|
||||
deletions: stat?.deletions ?? 0,
|
||||
status: item.status,
|
||||
} satisfies FileStatus
|
||||
}),
|
||||
)
|
||||
}),
|
||||
diff: Effect.fn("Vcs.diff")(function* (mode: Mode) {
|
||||
const value = yield* InstanceState.get(state)
|
||||
const ctx = yield* InstanceState.context
|
||||
@@ -318,6 +371,36 @@ export const layer: Layer.Layer<Service, never, Git.Service | Bus.Service> = Lay
|
||||
if (!ref) return []
|
||||
return yield* diffAgainstRef(git, ctx.directory, ref)
|
||||
}),
|
||||
diffRaw: Effect.fn("Vcs.diffRaw")(function* () {
|
||||
const ctx = yield* InstanceState.context
|
||||
if (ctx.project.vcs !== "git") return ""
|
||||
const [hasHead, status] = yield* Effect.all([git.hasHead(ctx.directory), git.status(ctx.directory)], {
|
||||
concurrency: 2,
|
||||
})
|
||||
const tracked = hasHead ? (yield* git.patchAll(ctx.directory, "HEAD")).text : ""
|
||||
const untracked = yield* Effect.forEach(
|
||||
status.filter((item) => item.code === "??"),
|
||||
(item) => git.patchUntracked(ctx.directory, item.file).pipe(Effect.map((patch) => patch.text)),
|
||||
)
|
||||
return [tracked, ...untracked].filter(Boolean).join("\n")
|
||||
}),
|
||||
apply: Effect.fn("Vcs.apply")(function* (input: ApplyInput) {
|
||||
const ctx = yield* InstanceState.context
|
||||
if (ctx.project.vcs !== "git") {
|
||||
return yield* new PatchApplyError({
|
||||
message: "Patch can't be applied because the project is not git-based",
|
||||
reason: "non-git",
|
||||
})
|
||||
}
|
||||
const applied = yield* git.applyPatch(ctx.directory, input.patch)
|
||||
if (applied.exitCode !== 0) {
|
||||
return yield* new PatchApplyError({
|
||||
message: "Patch can't be applied",
|
||||
reason: "not-clean",
|
||||
})
|
||||
}
|
||||
return { applied: true }
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user