index.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { z } from "zod"
  2. import { Bus } from "../bus"
  3. import { $ } from "bun"
  4. import { createPatch } from "diff"
  5. import path from "path"
  6. import * as git from "isomorphic-git"
  7. import { App } from "../app/app"
  8. import fs from "fs"
  9. import { Log } from "../util/log"
  10. export namespace File {
  11. const log = Log.create({ service: "file" })
  12. export const Info = z
  13. .object({
  14. path: z.string(),
  15. added: z.number().int(),
  16. removed: z.number().int(),
  17. status: z.enum(["added", "deleted", "modified"]),
  18. })
  19. .openapi({
  20. ref: "File",
  21. })
  22. export type Info = z.infer<typeof Info>
  23. export const Event = {
  24. Edited: Bus.event(
  25. "file.edited",
  26. z.object({
  27. file: z.string(),
  28. }),
  29. ),
  30. }
  31. export async function status() {
  32. const app = App.info()
  33. if (!app.git) return []
  34. const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
  35. const changedFiles: Info[] = []
  36. if (diffOutput.trim()) {
  37. const lines = diffOutput.trim().split("\n")
  38. for (const line of lines) {
  39. const [added, removed, filepath] = line.split("\t")
  40. changedFiles.push({
  41. path: filepath,
  42. added: added === "-" ? 0 : parseInt(added, 10),
  43. removed: removed === "-" ? 0 : parseInt(removed, 10),
  44. status: "modified",
  45. })
  46. }
  47. }
  48. const untrackedOutput = await $`git ls-files --others --exclude-standard`.cwd(app.path.cwd).quiet().nothrow().text()
  49. if (untrackedOutput.trim()) {
  50. const untrackedFiles = untrackedOutput.trim().split("\n")
  51. for (const filepath of untrackedFiles) {
  52. try {
  53. const content = await Bun.file(path.join(app.path.root, filepath)).text()
  54. const lines = content.split("\n").length
  55. changedFiles.push({
  56. path: filepath,
  57. added: lines,
  58. removed: 0,
  59. status: "added",
  60. })
  61. } catch {
  62. continue
  63. }
  64. }
  65. }
  66. // Get deleted files
  67. const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
  68. if (deletedOutput.trim()) {
  69. const deletedFiles = deletedOutput.trim().split("\n")
  70. for (const filepath of deletedFiles) {
  71. changedFiles.push({
  72. path: filepath,
  73. added: 0,
  74. removed: 0, // Could get original line count but would require another git command
  75. status: "deleted",
  76. })
  77. }
  78. }
  79. return changedFiles.map((x) => ({
  80. ...x,
  81. path: path.relative(app.path.cwd, path.join(app.path.root, x.path)),
  82. }))
  83. }
  84. export async function read(file: string) {
  85. using _ = log.time("read", { file })
  86. const app = App.info()
  87. const full = path.join(app.path.cwd, file)
  88. const content = await Bun.file(full)
  89. .text()
  90. .catch(() => "")
  91. .then((x) => x.trim())
  92. if (app.git) {
  93. const rel = path.relative(app.path.root, full)
  94. const diff = await git.status({
  95. fs,
  96. dir: app.path.root,
  97. filepath: rel,
  98. })
  99. if (diff !== "unmodified") {
  100. const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text()
  101. const patch = createPatch(file, original, content, "old", "new", {
  102. context: Infinity,
  103. })
  104. return { type: "patch", content: patch }
  105. }
  106. }
  107. return { type: "raw", content }
  108. }
  109. }