vcs-hg.test.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { $ } from "bun"
  2. import { describe, expect } from "bun:test"
  3. import fs from "fs/promises"
  4. import path from "path"
  5. import { Effect, Layer } from "effect"
  6. import { LayerNode } from "@opencode-ai/util/effect/layer-node"
  7. import { Location } from "@opencode-ai/core/location"
  8. import { AbsolutePath } from "@opencode-ai/core/schema"
  9. import { Vcs } from "@opencode-ai/core/vcs"
  10. import { location } from "./fixture/location"
  11. import { tmpdir } from "./fixture/tmpdir"
  12. import { it } from "./lib/effect"
  13. const describeHg = Bun.which("hg") ? describe : describe.skip
  14. const provide = (directory: string) =>
  15. Effect.provide(
  16. LayerNode.compile(Vcs.node, [
  17. [
  18. Location.node,
  19. Layer.succeed(
  20. Location.Service,
  21. Location.Service.of(
  22. location(
  23. { directory: AbsolutePath.make(directory) },
  24. { vcs: { type: "hg", store: AbsolutePath.make(path.join(directory, ".hg")) } },
  25. ),
  26. ),
  27. ),
  28. ],
  29. ]),
  30. )
  31. const withTmp = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
  32. Effect.acquireRelease(
  33. Effect.promise(() => tmpdir()),
  34. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  35. ).pipe(Effect.flatMap((tmp) => f(tmp.path)))
  36. async function hg(directory: string, ...args: string[]) {
  37. await $`hg ${args}`.cwd(directory).env({ ...process.env, HGPLAIN: "1" }).quiet()
  38. }
  39. async function commitAll(directory: string, message: string) {
  40. await hg(directory, "addremove", "-q")
  41. await hg(directory, "commit", "-q", "-m", message, "-u", "test")
  42. }
  43. describeHg("Vcs mercurial", () => {
  44. it.live("reports modified, missing, and untracked files", () =>
  45. withTmp((directory) =>
  46. Effect.gen(function* () {
  47. yield* Effect.promise(async () => {
  48. await hg(directory, "init")
  49. await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
  50. await fs.writeFile(path.join(directory, "gone.txt"), "bye\n")
  51. await commitAll(directory, "initial")
  52. await fs.writeFile(path.join(directory, "keep.txt"), "one\nthree\n")
  53. await fs.rm(path.join(directory, "gone.txt"))
  54. await fs.writeFile(path.join(directory, "new.txt"), "hello\nworld\n")
  55. })
  56. const vcs = yield* Vcs.Service
  57. const status = yield* vcs.status()
  58. expect(status).toEqual([
  59. { file: "gone.txt", additions: 0, deletions: 1, status: "deleted" },
  60. { file: "keep.txt", additions: 1, deletions: 1, status: "modified" },
  61. { file: "new.txt", additions: 2, deletions: 0, status: "added" },
  62. ])
  63. }).pipe(provide(directory)),
  64. ),
  65. )
  66. it.live("diffs the working copy with synthesized untracked and missing patches", () =>
  67. withTmp((directory) =>
  68. Effect.gen(function* () {
  69. yield* Effect.promise(async () => {
  70. await hg(directory, "init")
  71. await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
  72. await fs.writeFile(path.join(directory, "gone.txt"), "bye\n")
  73. await commitAll(directory, "initial")
  74. await fs.writeFile(path.join(directory, "keep.txt"), "one\nthree\n")
  75. await fs.rm(path.join(directory, "gone.txt"))
  76. await fs.writeFile(path.join(directory, "spaced name.txt"), "hello\n")
  77. })
  78. const vcs = yield* Vcs.Service
  79. const diff = yield* vcs.diff("working")
  80. expect(diff.map((item) => ({ file: item.file, status: item.status }))).toEqual([
  81. { file: "gone.txt", status: "deleted" },
  82. { file: "keep.txt", status: "modified" },
  83. { file: "spaced name.txt", status: "added" },
  84. ])
  85. expect(diff[0].patch).toContain("-bye")
  86. expect(diff[1].patch).toContain("-two")
  87. expect(diff[1].patch).toContain("+three")
  88. expect(diff[1].additions).toBe(1)
  89. expect(diff[1].deletions).toBe(1)
  90. expect(diff[2].patch).toContain("+hello")
  91. expect(diff[2].additions).toBe(1)
  92. }).pipe(provide(directory)),
  93. ),
  94. )
  95. it.live("respects the context option", () =>
  96. withTmp((directory) =>
  97. Effect.gen(function* () {
  98. const body = Array.from({ length: 20 }, (_, index) => `line-${index}`).join("\n") + "\n"
  99. yield* Effect.promise(async () => {
  100. await hg(directory, "init")
  101. await fs.writeFile(path.join(directory, "file.txt"), body)
  102. await commitAll(directory, "initial")
  103. await fs.writeFile(path.join(directory, "file.txt"), body.replace("line-10", "changed"))
  104. })
  105. const vcs = yield* Vcs.Service
  106. const full = yield* vcs.diff("working")
  107. expect(full[0].patch).toContain("line-0")
  108. expect(full[0].patch).toContain("line-19")
  109. const tight = yield* vcs.diff("working", { context: 1 })
  110. expect(tight[0].patch).toContain("line-9")
  111. expect(tight[0].patch).not.toContain("line-0")
  112. }).pipe(provide(directory)),
  113. ),
  114. )
  115. it.live("diffs before the first commit", () =>
  116. withTmp((directory) =>
  117. Effect.gen(function* () {
  118. yield* Effect.promise(async () => {
  119. await hg(directory, "init")
  120. await fs.writeFile(path.join(directory, "tracked.txt"), "a\nb\n")
  121. await hg(directory, "add", "-q", "tracked.txt")
  122. await fs.writeFile(path.join(directory, "loose.txt"), "hello\n")
  123. })
  124. const vcs = yield* Vcs.Service
  125. expect(yield* vcs.status()).toEqual([
  126. { file: "loose.txt", additions: 1, deletions: 0, status: "added" },
  127. { file: "tracked.txt", additions: 2, deletions: 0, status: "added" },
  128. ])
  129. const diff = yield* vcs.diff("working")
  130. expect(diff).toHaveLength(2)
  131. expect(diff[0].patch).toContain("+hello")
  132. expect(diff[1].patch).toContain("+a")
  133. }).pipe(provide(directory)),
  134. ),
  135. )
  136. it.live("diffs a named branch against the default branch", () =>
  137. withTmp((directory) =>
  138. Effect.gen(function* () {
  139. yield* Effect.promise(async () => {
  140. await hg(directory, "init")
  141. await fs.writeFile(path.join(directory, "file.txt"), "one\n")
  142. await commitAll(directory, "initial")
  143. })
  144. const vcs = yield* Vcs.Service
  145. expect(yield* vcs.diff("branch")).toEqual([])
  146. yield* Effect.promise(async () => {
  147. await hg(directory, "branch", "-q", "feature")
  148. await fs.writeFile(path.join(directory, "file.txt"), "one\ntwo\n")
  149. await commitAll(directory, "feature change")
  150. })
  151. const diff = yield* vcs.diff("branch")
  152. expect(diff.map((item) => ({ file: item.file, status: item.status }))).toEqual([
  153. { file: "file.txt", status: "modified" },
  154. ])
  155. expect(diff[0].patch).toContain("+two")
  156. }).pipe(provide(directory)),
  157. ),
  158. )
  159. })