index.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { App } from "../app/app"
  2. import { $ } from "bun"
  3. import path from "path"
  4. import fs from "fs/promises"
  5. import { Log } from "../util/log"
  6. import { Global } from "../global"
  7. import { z } from "zod"
  8. import { Config } from "../config/config"
  9. export namespace Snapshot {
  10. const log = Log.create({ service: "snapshot" })
  11. export function init() {
  12. Array.fromAsync(
  13. new Bun.Glob("**/snapshot").scan({
  14. absolute: true,
  15. onlyFiles: false,
  16. cwd: Global.Path.data,
  17. }),
  18. ).then((files) => {
  19. for (const file of files) {
  20. fs.rmdir(file, { recursive: true })
  21. }
  22. })
  23. }
  24. export async function track() {
  25. const app = App.info()
  26. if (!app.git) return
  27. const cfg = await Config.get()
  28. if (cfg.snapshot === false) return
  29. const git = gitdir()
  30. if (await fs.mkdir(git, { recursive: true })) {
  31. await $`git init`
  32. .env({
  33. ...process.env,
  34. GIT_DIR: git,
  35. GIT_WORK_TREE: app.path.root,
  36. })
  37. .quiet()
  38. .nothrow()
  39. log.info("initialized")
  40. }
  41. await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
  42. const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).nothrow().text()
  43. log.info("tracking", { hash, cwd: app.path.cwd, git })
  44. return hash.trim()
  45. }
  46. export const Patch = z.object({
  47. hash: z.string(),
  48. files: z.string().array(),
  49. })
  50. export type Patch = z.infer<typeof Patch>
  51. export async function patch(hash: string): Promise<Patch> {
  52. const app = App.info()
  53. const git = gitdir()
  54. await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
  55. const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(app.path.cwd).text()
  56. return {
  57. hash,
  58. files: files
  59. .trim()
  60. .split("\n")
  61. .map((x) => x.trim())
  62. .filter(Boolean)
  63. .map((x) => path.join(app.path.root, x)),
  64. }
  65. }
  66. export async function restore(snapshot: string) {
  67. log.info("restore", { commit: snapshot })
  68. const app = App.info()
  69. const git = gitdir()
  70. await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
  71. .quiet()
  72. .cwd(app.path.root)
  73. }
  74. export async function revert(patches: Patch[]) {
  75. const files = new Set<string>()
  76. const git = gitdir()
  77. for (const item of patches) {
  78. for (const file of item.files) {
  79. if (files.has(file)) continue
  80. log.info("reverting", { file, hash: item.hash })
  81. const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}`
  82. .quiet()
  83. .cwd(App.info().path.root)
  84. .nothrow()
  85. if (result.exitCode !== 0) {
  86. log.info("file not found in history, deleting", { file })
  87. await fs.unlink(file).catch(() => {})
  88. }
  89. files.add(file)
  90. }
  91. }
  92. }
  93. export async function diff(hash: string) {
  94. const app = App.info()
  95. const git = gitdir()
  96. const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(app.path.root).text()
  97. return result.trim()
  98. }
  99. function gitdir() {
  100. const app = App.info()
  101. return path.join(app.path.data, "snapshots")
  102. }
  103. }