index.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import { $ } from "bun"
  2. import path from "path"
  3. import fs from "fs/promises"
  4. import { Log } from "../util/log"
  5. import { Global } from "../global"
  6. import z from "zod/v4"
  7. import { Config } from "../config/config"
  8. import { Instance } from "../project/instance"
  9. export namespace Snapshot {
  10. const log = Log.create({ service: "snapshot" })
  11. export async function track() {
  12. if (Instance.project.vcs !== "git") return
  13. const cfg = await Config.get()
  14. if (cfg.snapshot === false) return
  15. const git = gitdir()
  16. if (await fs.mkdir(git, { recursive: true })) {
  17. await $`git init`
  18. .env({
  19. ...process.env,
  20. GIT_DIR: git,
  21. GIT_WORK_TREE: Instance.worktree,
  22. })
  23. .quiet()
  24. .nothrow()
  25. log.info("initialized")
  26. }
  27. await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
  28. const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Instance.directory).nothrow().text()
  29. log.info("tracking", { hash, cwd: Instance.directory, git })
  30. return hash.trim()
  31. }
  32. export const Patch = z.object({
  33. hash: z.string(),
  34. files: z.string().array(),
  35. })
  36. export type Patch = z.infer<typeof Patch>
  37. export async function patch(hash: string): Promise<Patch> {
  38. const git = gitdir()
  39. await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
  40. const result = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.quiet().cwd(Instance.directory).nothrow()
  41. // If git diff fails, return empty patch
  42. if (result.exitCode !== 0) {
  43. log.warn("failed to get diff", { hash, exitCode: result.exitCode })
  44. return { hash, files: [] }
  45. }
  46. const files = result.text()
  47. return {
  48. hash,
  49. files: files
  50. .trim()
  51. .split("\n")
  52. .map((x) => x.trim())
  53. .filter(Boolean)
  54. .map((x) => path.join(Instance.worktree, x)),
  55. }
  56. }
  57. export async function restore(snapshot: string) {
  58. log.info("restore", { commit: snapshot })
  59. const git = gitdir()
  60. const result = await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
  61. .quiet()
  62. .cwd(Instance.worktree)
  63. .nothrow()
  64. if (result.exitCode !== 0) {
  65. log.error("failed to restore snapshot", {
  66. snapshot,
  67. exitCode: result.exitCode,
  68. stderr: result.stderr.toString(),
  69. stdout: result.stdout.toString(),
  70. })
  71. }
  72. }
  73. export async function revert(patches: Patch[]) {
  74. const files = new Set<string>()
  75. const git = gitdir()
  76. for (const item of patches) {
  77. for (const file of item.files) {
  78. if (files.has(file)) continue
  79. log.info("reverting", { file, hash: item.hash })
  80. const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}`
  81. .quiet()
  82. .cwd(Instance.worktree)
  83. .nothrow()
  84. if (result.exitCode !== 0) {
  85. const relativePath = path.relative(Instance.worktree, file)
  86. const checkTree = await $`git --git-dir=${git} ls-tree ${item.hash} -- ${relativePath}`
  87. .quiet()
  88. .cwd(Instance.worktree)
  89. .nothrow()
  90. if (checkTree.exitCode === 0 && checkTree.text().trim()) {
  91. log.info("file existed in snapshot but checkout failed, keeping", { file })
  92. } else {
  93. log.info("file did not exist in snapshot, deleting", { file })
  94. await fs.unlink(file).catch(() => {})
  95. }
  96. }
  97. files.add(file)
  98. }
  99. }
  100. }
  101. export async function diff(hash: string) {
  102. const git = gitdir()
  103. await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
  104. const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(Instance.worktree).nothrow()
  105. if (result.exitCode !== 0) {
  106. log.warn("failed to get diff", {
  107. hash,
  108. exitCode: result.exitCode,
  109. stderr: result.stderr.toString(),
  110. stdout: result.stdout.toString(),
  111. })
  112. return ""
  113. }
  114. return result.text().trim()
  115. }
  116. function gitdir() {
  117. const project = Instance.project
  118. return path.join(Global.Path.data, "snapshot", project.id)
  119. }
  120. }