mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-14 15:32:52 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5b22986b4 |
@@ -22,10 +22,10 @@ const snapshotConfigFile = "opencode.gitconfig"
|
|||||||
const snapshotConfigInclude = `[include]
|
const snapshotConfigInclude = `[include]
|
||||||
path = ${snapshotConfigFile}
|
path = ${snapshotConfigFile}
|
||||||
`
|
`
|
||||||
const snapshotConfig = `[core]
|
const snapshotConfig = (input: { autocrlf: string; symlinks: string }) => `[core]
|
||||||
autocrlf = false
|
autocrlf = ${input.autocrlf}
|
||||||
longpaths = true
|
longpaths = true
|
||||||
symlinks = true
|
symlinks = ${input.symlinks}
|
||||||
fsmonitor = false
|
fsmonitor = false
|
||||||
untrackedCache = true
|
untrackedCache = true
|
||||||
[feature]
|
[feature]
|
||||||
@@ -341,6 +341,46 @@ const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sourceConfig = Effect.fnUntraced(function* (
|
||||||
|
repository: Repository | undefined,
|
||||||
|
key: string,
|
||||||
|
fallback: string,
|
||||||
|
allowInput: boolean,
|
||||||
|
) {
|
||||||
|
if (!repository) return fallback
|
||||||
|
const result = yield* execute(
|
||||||
|
repository.worktree,
|
||||||
|
proc,
|
||||||
|
)(["config", "--get", key]).pipe(
|
||||||
|
Effect.mapError(
|
||||||
|
(cause) =>
|
||||||
|
new OperationError({
|
||||||
|
operation: "create",
|
||||||
|
directory: repository.worktree,
|
||||||
|
message: `Failed to resolve ${key}`,
|
||||||
|
cause,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (result.exitCode === 0) {
|
||||||
|
const value = result.text.trim().toLowerCase()
|
||||||
|
if (allowInput && value === "input") return value
|
||||||
|
if (["true", "yes", "on", "1"].includes(value)) return "true"
|
||||||
|
if (["false", "no", "off", "0"].includes(value)) return "false"
|
||||||
|
return yield* new OperationError({
|
||||||
|
operation: "create",
|
||||||
|
directory: repository.worktree,
|
||||||
|
message: `Invalid ${key} value: ${value}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (result.exitCode === 1) return fallback
|
||||||
|
return yield* new OperationError({
|
||||||
|
operation: "create",
|
||||||
|
directory: repository.worktree,
|
||||||
|
message: result.stderr.trim() || `Failed to resolve ${key}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const create = Effect.fn("Git.repo.create")(function* (input: {
|
const create = Effect.fn("Git.repo.create")(function* (input: {
|
||||||
worktree: AbsolutePath
|
worktree: AbsolutePath
|
||||||
gitDirectory: AbsolutePath
|
gitDirectory: AbsolutePath
|
||||||
@@ -363,14 +403,20 @@ const layer = Layer.effect(
|
|||||||
commonDirectory: input.gitDirectory,
|
commonDirectory: input.gitDirectory,
|
||||||
})
|
})
|
||||||
yield* repositoryOperation("create", repository, ["init"])
|
yield* repositoryOperation("create", repository, ["init"])
|
||||||
yield* Effect.gen(function* () {
|
const semantics = {
|
||||||
yield* fs.writeFileString(path.join(input.gitDirectory, snapshotConfigFile), snapshotConfig)
|
autocrlf: yield* sourceConfig(input.seed, "core.autocrlf", "false", true),
|
||||||
|
symlinks: yield* sourceConfig(input.seed, "core.symlinks", "true", false),
|
||||||
|
}
|
||||||
|
const reseed = yield* Effect.gen(function* () {
|
||||||
|
const owned = path.join(input.gitDirectory, snapshotConfigFile)
|
||||||
|
const desired = snapshotConfig(semantics)
|
||||||
|
const previous = yield* fs.readFileString(owned).pipe(Effect.catch(() => Effect.succeed("")))
|
||||||
|
yield* fs.writeFileString(owned, desired)
|
||||||
const config = path.join(input.gitDirectory, "config")
|
const config = path.join(input.gitDirectory, "config")
|
||||||
const current = yield* fs.readFileString(config)
|
const current = yield* fs.readFileString(config)
|
||||||
if (current.includes(snapshotConfigInclude)) return
|
const base = current.replace(/^\s*path\s*=\s*opencode\.gitconfig\s*\r?\n?/gm, "").trimEnd()
|
||||||
yield* fs.writeFileString(config, `${current.endsWith("\n") ? "\n" : "\n\n"}${snapshotConfigInclude}`, {
|
yield* fs.writeFileString(config, `${base}\n\n${snapshotConfigInclude}`)
|
||||||
flag: "a",
|
return previous !== desired
|
||||||
})
|
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.mapError(
|
Effect.mapError(
|
||||||
(cause) =>
|
(cause) =>
|
||||||
@@ -410,6 +456,7 @@ const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if (!reseed) return repository
|
||||||
yield* fs
|
yield* fs
|
||||||
.copyFile(path.join(input.seed.gitDirectory, "index"), path.join(input.gitDirectory, "index"))
|
.copyFile(path.join(input.seed.gitDirectory, "index"), path.join(input.gitDirectory, "index"))
|
||||||
.pipe(Effect.catch(() => Effect.void))
|
.pipe(Effect.catch(() => Effect.void))
|
||||||
|
|||||||
@@ -83,11 +83,9 @@ const layer = Layer.effect(
|
|||||||
const gitDirectory = AbsolutePath.make(
|
const gitDirectory = AbsolutePath.make(
|
||||||
path.join(global.data, "snapshot", location.project.id, Hash.fast(worktree)),
|
path.join(global.data, "snapshot", location.project.id, Hash.fast(worktree)),
|
||||||
)
|
)
|
||||||
const snapshotRepository = (yield* fs.existsSafe(path.join(gitDirectory, "HEAD")))
|
const snapshotRepository = yield* git.repo
|
||||||
? new Git.Repository({ worktree, gitDirectory, commonDirectory: gitDirectory })
|
.create({ worktree, gitDirectory, seed: source })
|
||||||
: yield* git.repo
|
.pipe(Effect.mapError((cause) => failure("capture", cause)))
|
||||||
.create({ worktree, gitDirectory, seed: source })
|
|
||||||
.pipe(Effect.mapError((cause) => failure("capture", cause)))
|
|
||||||
return { source, worktree, snapshotRepository }
|
return { source, worktree, snapshotRepository }
|
||||||
}).pipe(Effect.forkIn(lifetime)),
|
}).pipe(Effect.forkIn(lifetime)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -103,14 +103,10 @@ type GitOps = ReturnType<typeof makeGit>
|
|||||||
const cfg = [
|
const cfg = [
|
||||||
"--no-optional-locks",
|
"--no-optional-locks",
|
||||||
"-c",
|
"-c",
|
||||||
"core.autocrlf=false",
|
|
||||||
"-c",
|
|
||||||
"core.fsmonitor=false",
|
"core.fsmonitor=false",
|
||||||
"-c",
|
"-c",
|
||||||
"core.longpaths=true",
|
"core.longpaths=true",
|
||||||
"-c",
|
"-c",
|
||||||
"core.symlinks=true",
|
|
||||||
"-c",
|
|
||||||
"core.quotepath=false",
|
"core.quotepath=false",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
|||||||
@@ -148,21 +148,34 @@ describe("Git trees", () => {
|
|||||||
const git = yield* Git.Service
|
const git = yield* Git.Service
|
||||||
const source = yield* git.repo.discover(AbsolutePath.make(root.path))
|
const source = yield* git.repo.discover(AbsolutePath.make(root.path))
|
||||||
if (!source) throw new Error("Repository not found")
|
if (!source) throw new Error("Repository not found")
|
||||||
|
yield* Effect.promise(() => $`git config core.autocrlf true`.cwd(root.path).quiet())
|
||||||
|
yield* Effect.promise(() => $`git config core.symlinks false`.cwd(root.path).quiet())
|
||||||
const storage = AbsolutePath.make(path.join(root.path, ".snapshot storage"))
|
const storage = AbsolutePath.make(path.join(root.path, ".snapshot storage"))
|
||||||
const repository = yield* git.repo.create({ worktree: source.worktree, gitDirectory: storage, seed: source })
|
const repository = yield* git.repo.create({ worktree: source.worktree, gitDirectory: storage, seed: source })
|
||||||
yield* Effect.promise(() => $`git --git-dir ${storage} config --add include.path first.gitconfig`.quiet())
|
yield* Effect.promise(() => $`git --git-dir ${storage} config --add include.path first.gitconfig`.quiet())
|
||||||
yield* Effect.promise(() => $`git --git-dir ${storage} config --add include.path second.gitconfig`.quiet())
|
yield* Effect.promise(() => $`git --git-dir ${storage} config --add include.path second.gitconfig`.quiet())
|
||||||
yield* Effect.promise(() => $`git --git-dir ${storage} config core.autocrlf true`.quiet())
|
yield* Effect.promise(() => $`git --git-dir ${storage} config core.autocrlf false`.quiet())
|
||||||
|
yield* Effect.promise(() => $`git --git-dir ${storage} config core.symlinks true`.quiet())
|
||||||
yield* git.repo.create({ worktree: source.worktree, gitDirectory: storage, seed: source })
|
yield* git.repo.create({ worktree: source.worktree, gitDirectory: storage, seed: source })
|
||||||
expect(
|
expect(
|
||||||
yield* Effect.promise(() => $`git --git-dir ${storage} config --local --includes core.autocrlf`.text()),
|
yield* Effect.promise(() => $`git --git-dir ${storage} config --local --includes core.autocrlf`.text()),
|
||||||
|
).toBe("true\n")
|
||||||
|
expect(
|
||||||
|
yield* Effect.promise(() => $`git --git-dir ${storage} config --local --includes core.symlinks`.text()),
|
||||||
).toBe("false\n")
|
).toBe("false\n")
|
||||||
|
yield* Effect.promise(() => $`git config core.autocrlf input`.cwd(root.path).quiet())
|
||||||
|
yield* git.repo.create({ worktree: source.worktree, gitDirectory: storage, seed: source })
|
||||||
|
expect(
|
||||||
|
yield* Effect.promise(() => $`git --git-dir ${storage} config --local --includes core.autocrlf`.text()),
|
||||||
|
).toBe("input\n")
|
||||||
|
yield* Effect.promise(() => $`git config core.autocrlf true`.cwd(root.path).quiet())
|
||||||
|
yield* git.repo.create({ worktree: source.worktree, gitDirectory: storage, seed: source })
|
||||||
expect(
|
expect(
|
||||||
(yield* Effect.promise(() => fs.readFile(path.join(storage, "config"), "utf8"))).match(/opencode\.gitconfig/g),
|
(yield* Effect.promise(() => fs.readFile(path.join(storage, "config"), "utf8"))).match(/opencode\.gitconfig/g),
|
||||||
).toHaveLength(1)
|
).toHaveLength(1)
|
||||||
expect(
|
expect(
|
||||||
yield* Effect.promise(() => $`git --git-dir ${storage} config --local --get-all include.path`.text()),
|
yield* Effect.promise(() => $`git --git-dir ${storage} config --local --get-all include.path`.text()),
|
||||||
).toBe("opencode.gitconfig\nfirst.gitconfig\nsecond.gitconfig\n")
|
).toBe("first.gitconfig\nsecond.gitconfig\nopencode.gitconfig\n")
|
||||||
yield* git.index.refresh({ repository, scope: RelativePath.make("scope") })
|
yield* git.index.refresh({ repository, scope: RelativePath.make("scope") })
|
||||||
const before = yield* git.tree.write(repository)
|
const before = yield* git.tree.write(repository)
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,51 @@ describe("Snapshot", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
testEffect(Layer.empty).live("repairs existing storage with source repository semantics", () =>
|
||||||
|
Effect.acquireUseRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const project = path.join(tmp.path, "project")
|
||||||
|
const file = path.join(project, "line-endings.txt")
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
|
await fs.mkdir(project)
|
||||||
|
await fs.writeFile(file, "before\n")
|
||||||
|
await initGit(project, true)
|
||||||
|
await $`git config core.autocrlf false`.cwd(project).quiet()
|
||||||
|
})
|
||||||
|
|
||||||
|
const git = yield* Git.Service.pipe(Effect.provide(AppNodeBuilder.build(Git.node)))
|
||||||
|
const source = yield* git.repo.discover(AbsolutePath.make(project))
|
||||||
|
if (!source) throw new Error("Repository not found")
|
||||||
|
const location = yield* Location.Service.pipe(
|
||||||
|
Effect.provide(
|
||||||
|
AppNodeBuilder.build(Location.boundNode(Location.Ref.make({ directory: AbsolutePath.make(project) }))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield* git.repo.create({
|
||||||
|
worktree: source.worktree,
|
||||||
|
gitDirectory: AbsolutePath.make(path.join(tmp.path, "snapshot", location.project.id, Hash.fast(project))),
|
||||||
|
seed: source,
|
||||||
|
})
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
|
await $`git config core.autocrlf true`.cwd(project).quiet()
|
||||||
|
await fs.rm(file)
|
||||||
|
await $`git checkout -- line-endings.txt`.cwd(project).quiet()
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
const snapshot = yield* Snapshot.Service
|
||||||
|
const before = yield* snapshot.capture()
|
||||||
|
expect(before).toBeDefined()
|
||||||
|
yield* Effect.promise(() => fs.writeFile(file, "before\n"))
|
||||||
|
expect(yield* snapshot.capture()).toBe(before)
|
||||||
|
}).pipe(Effect.provide(snapshotLayer(tmp.path, project)))
|
||||||
|
}),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
testEffect(Layer.empty).live("treats capture outside Git as unavailable", () =>
|
testEffect(Layer.empty).live("treats capture outside Git as unavailable", () =>
|
||||||
Effect.acquireUseRelease(
|
Effect.acquireUseRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
|
|||||||
@@ -93,6 +93,42 @@ describe("Vcs", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live("respects repository line ending configuration", () =>
|
||||||
|
withGit((directory) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
|
await $`git config core.autocrlf true`.cwd(directory).quiet()
|
||||||
|
await fs.writeFile(path.join(directory, "line-endings.txt"), "before\n")
|
||||||
|
await commitAll(directory, "line endings")
|
||||||
|
await fs.rm(path.join(directory, "line-endings.txt"))
|
||||||
|
await $`git checkout -- line-endings.txt`.cwd(directory).quiet()
|
||||||
|
})
|
||||||
|
|
||||||
|
const vcs = yield* Vcs.Service
|
||||||
|
expect(yield* vcs.status()).toEqual([])
|
||||||
|
expect(yield* vcs.diff("working")).toEqual([])
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("respects repository symlink configuration", () =>
|
||||||
|
withGit((directory) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
|
const blob = await $`printf target.txt | git hash-object -w --stdin`.cwd(directory).text()
|
||||||
|
await $`git update-index --add --cacheinfo 120000,${blob.trim()},link.txt`.cwd(directory).quiet()
|
||||||
|
await $`git commit -m symlink`.cwd(directory).quiet()
|
||||||
|
await $`git config core.symlinks false`.cwd(directory).quiet()
|
||||||
|
await $`git checkout-index -f link.txt`.cwd(directory).quiet()
|
||||||
|
})
|
||||||
|
|
||||||
|
const vcs = yield* Vcs.Service
|
||||||
|
expect(yield* vcs.status()).toEqual([])
|
||||||
|
expect(yield* vcs.diff("working")).toEqual([])
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
it.live("caches branch info and publishes HEAD changes", () =>
|
it.live("caches branch info and publishes HEAD changes", () =>
|
||||||
withGit((directory) =>
|
withGit((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
Reference in New Issue
Block a user