git.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { describe, expect } from "bun:test"
  2. import { $ } from "bun"
  3. import fs from "fs/promises"
  4. import path from "path"
  5. import { Effect } from "effect"
  6. import { Git } from "@opencode-ai/core/git"
  7. import { AbsolutePath } from "@opencode-ai/core/schema"
  8. import { branch, commit, gitRemote } from "./fixture/git"
  9. import { tmpdir } from "./fixture/tmpdir"
  10. import { testEffect } from "./lib/effect"
  11. const it = testEffect(Git.defaultLayer)
  12. describe("Git", () => {
  13. it.live("clones a remote and reads checkout metadata", () =>
  14. withRemote((fixture) =>
  15. Effect.gen(function* () {
  16. const git = yield* Git.Service
  17. const target = path.join(fixture.root, "checkout")
  18. const result = yield* git.clone({ remote: fixture.remote, target })
  19. expect(result.exitCode).toBe(0)
  20. expect(yield* git.origin(target)).toBe(fixture.remote)
  21. expect(yield* git.head(target)).toBeString()
  22. expect(yield* git.branch(target)).toBe("main")
  23. expect(yield* git.remoteHead(target)).toBe("origin/main")
  24. expect(yield* read(path.join(target, "README.md"))).toBe("one\n")
  25. }),
  26. ),
  27. )
  28. it.live("fetches, checks out, and resets remote changes", () =>
  29. withRemote((fixture) =>
  30. Effect.gen(function* () {
  31. const git = yield* Git.Service
  32. const target = path.join(fixture.root, "checkout")
  33. yield* git.clone({ remote: fixture.remote, target })
  34. yield* Effect.promise(() => commit(fixture.source, "two\n", "second"))
  35. expect((yield* git.fetch(target)).exitCode).toBe(0)
  36. expect((yield* git.reset(target, "origin/main")).exitCode).toBe(0)
  37. expect(yield* read(path.join(target, "README.md"))).toBe("two\n")
  38. yield* Effect.promise(() => branch(fixture.source, "feature/docs", "feature\n"))
  39. expect((yield* git.fetchBranch(target, "feature/docs")).exitCode).toBe(0)
  40. expect((yield* git.checkout(target, "feature/docs")).exitCode).toBe(0)
  41. expect((yield* git.reset(target, "origin/feature/docs")).exitCode).toBe(0)
  42. expect(yield* git.branch(target)).toBe("feature/docs")
  43. expect(yield* read(path.join(target, "README.md"))).toBe("feature\n")
  44. }),
  45. ),
  46. )
  47. })
  48. function withRemote<A, E, R>(body: (fixture: Awaited<ReturnType<typeof gitRemote>>) => Effect.Effect<A, E, R>) {
  49. return Effect.acquireUseRelease(
  50. Effect.promise(async () => {
  51. const root = await tmpdir()
  52. return { root, fixture: await gitRemote(root.path) }
  53. }),
  54. (input) => body(input.fixture),
  55. (input) => Effect.promise(() => input.root[Symbol.asyncDispose]()),
  56. )
  57. }
  58. function read(file: string) {
  59. return Effect.promise(() => fs.readFile(file, "utf8")).pipe(Effect.map((content) => content.replace(/\r\n/g, "\n")))
  60. }
  61. async function initRepo(directory: string) {
  62. await $`git init`.cwd(directory).quiet()
  63. await $`git config core.fsmonitor false`.cwd(directory).quiet()
  64. await $`git config commit.gpgsign false`.cwd(directory).quiet()
  65. await $`git config user.email test@opencode.test`.cwd(directory).quiet()
  66. await $`git config user.name Test`.cwd(directory).quiet()
  67. await $`git commit --allow-empty -m root`.cwd(directory).quiet()
  68. }
  69. describe("Git worktrees", () => {
  70. it.live("creates, lists, and removes linked worktrees", () =>
  71. Effect.gen(function* () {
  72. const root = yield* Effect.acquireRelease(
  73. Effect.promise(() => tmpdir()),
  74. (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
  75. )
  76. yield* Effect.promise(() => initRepo(root.path))
  77. const directory = AbsolutePath.make(yield* Effect.promise(() => fs.realpath(root.path)))
  78. const worktree = AbsolutePath.make(`${root.path}-git-worktree`)
  79. yield* Effect.addFinalizer(() =>
  80. Effect.promise(() => fs.rm(worktree, { recursive: true, force: true })).pipe(Effect.ignore),
  81. )
  82. const git = yield* Git.Service
  83. const repo = { directory, store: AbsolutePath.make(path.join(directory, ".git")) }
  84. yield* git.worktreeCreate({ repo, directory: worktree })
  85. expect((yield* git.worktreeList(repo)).some((entry) => entry.endsWith("-git-worktree"))).toBe(true)
  86. const linked = yield* git.find(worktree)
  87. expect(linked?.directory).toBe(AbsolutePath.make(yield* Effect.promise(() => fs.realpath(worktree))))
  88. expect(linked?.store).toBe(repo.store)
  89. if (!linked) throw new Error("Linked worktree not found")
  90. yield* git.worktreeRemove({ repo: linked, directory: worktree, force: false })
  91. expect((yield* git.worktreeList(repo)).some((entry) => entry.endsWith("-git-worktree"))).toBe(false)
  92. }),
  93. )
  94. })