|
|
@@ -1,7 +1,7 @@
|
|
|
export * as Git from "./git"
|
|
|
|
|
|
import path from "path"
|
|
|
-import { Context, Effect, Layer, Schema, Stream } from "effect"
|
|
|
+import { Context, Effect, Layer, Schema } from "effect"
|
|
|
import { ChildProcess } from "effect/unstable/process"
|
|
|
import { AbsolutePath, RelativePath } from "./schema"
|
|
|
import { FSUtil } from "@opencode-ai/util/fs-util"
|
|
|
@@ -35,9 +35,6 @@ const snapshotConfig = `[core]
|
|
|
threads = true
|
|
|
`
|
|
|
|
|
|
-export const ChangeSet = Schema.String.pipe(Schema.brand("Git.ChangeSet"))
|
|
|
-export type ChangeSet = typeof ChangeSet.Type
|
|
|
-
|
|
|
export const TreeID = Schema.String.pipe(Schema.brand("Git.TreeID"))
|
|
|
export type TreeID = typeof TreeID.Type
|
|
|
|
|
|
@@ -72,13 +69,6 @@ export class WorktreeError extends Schema.TaggedErrorClass<WorktreeError>()("Git
|
|
|
cause: Schema.optional(Schema.Defect()),
|
|
|
}) {}
|
|
|
|
|
|
-export class PatchError extends Schema.TaggedErrorClass<PatchError>()("Git.PatchError", {
|
|
|
- operation: Schema.Literals(["capture", "apply", "reset"]),
|
|
|
- directory: AbsolutePath,
|
|
|
- message: Schema.String,
|
|
|
- cause: Schema.optional(Schema.Defect()),
|
|
|
-}) {}
|
|
|
-
|
|
|
export interface Interface {
|
|
|
readonly repo: {
|
|
|
readonly discover: (input: AbsolutePath) => Effect.Effect<Repository | undefined>
|
|
|
@@ -115,20 +105,6 @@ export interface Interface {
|
|
|
) => Effect.Effect<void, OperationError>
|
|
|
readonly resetHard: (repository: Repository, revision: string) => Effect.Effect<void, OperationError>
|
|
|
}
|
|
|
- readonly change: {
|
|
|
- readonly capture: (input: { repository: Repository; path: AbsolutePath }) => Effect.Effect<ChangeSet, PatchError>
|
|
|
- readonly apply: (input: {
|
|
|
- repository: Repository
|
|
|
- path: AbsolutePath
|
|
|
- changes: ChangeSet
|
|
|
- }) => Effect.Effect<void, PatchError>
|
|
|
- readonly discard: (input: {
|
|
|
- repository: Repository
|
|
|
- path: AbsolutePath
|
|
|
- index: "preserve" | "reset"
|
|
|
- untracked: "preserve" | "remove"
|
|
|
- }) => Effect.Effect<void, PatchError>
|
|
|
- }
|
|
|
readonly worktree: {
|
|
|
readonly create: (input: {
|
|
|
repository: Repository
|
|
|
@@ -678,132 +654,6 @@ const layer = Layer.effect(
|
|
|
),
|
|
|
)
|
|
|
|
|
|
- const capture = Effect.fn("Git.change.capture")(function* (input: { repository: Repository; path: AbsolutePath }) {
|
|
|
- const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
|
|
|
- const tracked = yield* execute(
|
|
|
- input.repository.worktree,
|
|
|
- proc,
|
|
|
- )(["diff", "--binary", "HEAD", "--", scope]).pipe(
|
|
|
- Effect.mapError(
|
|
|
- (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
|
|
|
- ),
|
|
|
- )
|
|
|
- if (tracked.exitCode !== 0) {
|
|
|
- return yield* new PatchError({
|
|
|
- operation: "capture",
|
|
|
- directory: input.path,
|
|
|
- message: tracked.stderr.trim() || tracked.text.trim() || "Failed to capture tracked changes",
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- const untracked = yield* execute(
|
|
|
- input.repository.worktree,
|
|
|
- proc,
|
|
|
- )(["ls-files", "--others", "--exclude-standard", "-z", "--", scope]).pipe(
|
|
|
- Effect.mapError(
|
|
|
- (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
|
|
|
- ),
|
|
|
- )
|
|
|
- if (untracked.exitCode !== 0) {
|
|
|
- return yield* new PatchError({
|
|
|
- operation: "capture",
|
|
|
- directory: input.path,
|
|
|
- message: untracked.stderr.trim() || untracked.text.trim() || "Failed to list untracked changes",
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- const created = yield* Effect.forEach(untracked.text.split("\0").filter(Boolean), (file) =>
|
|
|
- execute(
|
|
|
- input.repository.worktree,
|
|
|
- proc,
|
|
|
- )(["diff", "--binary", "--no-index", "--", "/dev/null", file]).pipe(
|
|
|
- Effect.mapError(
|
|
|
- (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
|
|
|
- ),
|
|
|
- Effect.flatMap((result) =>
|
|
|
- // git diff --no-index returns 1 when differences were found.
|
|
|
- result.exitCode === 0 || result.exitCode === 1
|
|
|
- ? Effect.succeed(result.text)
|
|
|
- : Effect.fail(
|
|
|
- new PatchError({
|
|
|
- operation: "capture",
|
|
|
- directory: input.path,
|
|
|
- message:
|
|
|
- result.stderr.trim() || result.text.trim() || `Failed to capture untracked change: ${file}`,
|
|
|
- }),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
- )
|
|
|
- return ChangeSet.make([tracked.text, ...created].filter(Boolean).join("\n"))
|
|
|
- })
|
|
|
-
|
|
|
- const apply = Effect.fn("Git.change.apply")(function* (input: {
|
|
|
- repository: Repository
|
|
|
- path: AbsolutePath
|
|
|
- changes: ChangeSet
|
|
|
- }) {
|
|
|
- const result = yield* proc
|
|
|
- .run(
|
|
|
- ChildProcess.make("git", ["apply", "-"], {
|
|
|
- cwd: input.path,
|
|
|
- extendEnv: true,
|
|
|
- stdin: Stream.make(new TextEncoder().encode(input.changes)),
|
|
|
- }),
|
|
|
- )
|
|
|
- .pipe(
|
|
|
- Effect.mapError(
|
|
|
- (cause) => new PatchError({ operation: "apply", directory: input.path, message: cause.message, cause }),
|
|
|
- ),
|
|
|
- )
|
|
|
- if (result.exitCode === 0) return
|
|
|
- return yield* new PatchError({
|
|
|
- operation: "apply",
|
|
|
- directory: input.path,
|
|
|
- message:
|
|
|
- result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Failed to apply changes",
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- const discard = Effect.fn("Git.change.discard")(function* (input: {
|
|
|
- repository: Repository
|
|
|
- path: AbsolutePath
|
|
|
- index: "preserve" | "reset"
|
|
|
- untracked: "preserve" | "remove"
|
|
|
- }) {
|
|
|
- const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
|
|
|
- const restore = yield* execute(
|
|
|
- input.repository.worktree,
|
|
|
- proc,
|
|
|
- )(input.index === "reset" ? ["checkout", "HEAD", "--", scope] : ["checkout", "--", scope]).pipe(
|
|
|
- Effect.mapError(
|
|
|
- (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
|
|
|
- ),
|
|
|
- )
|
|
|
- if (restore.exitCode !== 0) {
|
|
|
- return yield* new PatchError({
|
|
|
- operation: "reset",
|
|
|
- directory: input.path,
|
|
|
- message: restore.stderr.trim() || restore.text.trim() || "Failed to restore tracked changes",
|
|
|
- })
|
|
|
- }
|
|
|
- if (input.untracked === "preserve") return
|
|
|
- const clean = yield* execute(
|
|
|
- input.repository.worktree,
|
|
|
- proc,
|
|
|
- )(["clean", "-fd", "--", scope]).pipe(
|
|
|
- Effect.mapError(
|
|
|
- (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
|
|
|
- ),
|
|
|
- )
|
|
|
- if (clean.exitCode === 0) return
|
|
|
- return yield* new PatchError({
|
|
|
- operation: "reset",
|
|
|
- directory: input.path,
|
|
|
- message: clean.stderr.trim() || clean.text.trim() || "Failed to clean untracked changes",
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
const worktreeRun = Effect.fnUntraced(function* (
|
|
|
operation: "create" | "remove" | "list",
|
|
|
repository: Repository,
|
|
|
@@ -879,7 +729,6 @@ const layer = Layer.effect(
|
|
|
remote: { get: remote },
|
|
|
history: { head, branch, defaultRemoteBranch: remoteHead, rootCommits: roots },
|
|
|
sync: { fetchRemotes: fetch, fetchBranch, checkoutRemoteBranch: checkout, resetHard: reset },
|
|
|
- change: { capture, apply, discard },
|
|
|
worktree: { create: worktreeCreate, remove: worktreeRemove, list: worktreeList },
|
|
|
index: { refresh, ignored },
|
|
|
tree: {
|