Просмотр исходного кода

fix(app): added/deleted file status now correctly calculated

Adam 2 месяцев назад
Родитель
Сommit
3b93e8d95c

+ 48 - 1
packages/app/src/components/file-tree.tsx

@@ -130,10 +130,57 @@ export default function FileTree(props: {
     const nodes = file.tree.children(props.path)
     const current = filter()
     if (!current) return nodes
-    return nodes.filter((node) => {
+
+    const parent = (path: string) => {
+      const idx = path.lastIndexOf("/")
+      if (idx === -1) return ""
+      return path.slice(0, idx)
+    }
+
+    const leaf = (path: string) => {
+      const idx = path.lastIndexOf("/")
+      return idx === -1 ? path : path.slice(idx + 1)
+    }
+
+    const out = nodes.filter((node) => {
       if (node.type === "file") return current.files.has(node.path)
       return current.dirs.has(node.path)
     })
+
+    const seen = new Set(out.map((node) => node.path))
+
+    for (const dir of current.dirs) {
+      if (parent(dir) !== props.path) continue
+      if (seen.has(dir)) continue
+      out.push({
+        name: leaf(dir),
+        path: dir,
+        absolute: dir,
+        type: "directory",
+        ignored: false,
+      })
+      seen.add(dir)
+    }
+
+    for (const item of current.files) {
+      if (parent(item) !== props.path) continue
+      if (seen.has(item)) continue
+      out.push({
+        name: leaf(item),
+        path: item,
+        absolute: item,
+        type: "file",
+        ignored: false,
+      })
+      seen.add(item)
+    }
+
+    return out.toSorted((a, b) => {
+      if (a.type !== b.type) {
+        return a.type === "directory" ? -1 : 1
+      }
+      return a.name.localeCompare(b.name)
+    })
   })
 
   const Node = (

+ 1 - 3
packages/app/src/pages/session.tsx

@@ -500,9 +500,7 @@ export default function Page() {
     const out = new Map<string, "add" | "del" | "mix">()
     for (const diff of diffs()) {
       const file = normalize(diff.file)
-      const add = diff.additions > 0
-      const del = diff.deletions > 0
-      const kind = add && del ? "mix" : add ? "add" : del ? "del" : "mix"
+      const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
 
       out.set(file, kind)
 

+ 19 - 0
packages/opencode/src/snapshot/index.ts

@@ -188,6 +188,7 @@ export namespace Snapshot {
       after: z.string(),
       additions: z.number(),
       deletions: z.number(),
+      status: z.enum(["added", "deleted", "modified"]).optional(),
     })
     .meta({
       ref: "FileDiff",
@@ -196,6 +197,23 @@ export namespace Snapshot {
   export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
     const git = gitdir()
     const result: FileDiff[] = []
+    const status = new Map<string, "added" | "deleted" | "modified">()
+
+    const statuses =
+      await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-status --no-renames ${from} ${to} -- .`
+        .quiet()
+        .cwd(Instance.directory)
+        .nothrow()
+        .text()
+
+    for (const line of statuses.trim().split("\n")) {
+      if (!line) continue
+      const [code, file] = line.split("\t")
+      if (!code || !file) continue
+      const kind = code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified"
+      status.set(file, kind)
+    }
+
     for await (const line of $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
       .quiet()
       .cwd(Instance.directory)
@@ -224,6 +242,7 @@ export namespace Snapshot {
         after,
         additions: Number.isFinite(added) ? added : 0,
         deletions: Number.isFinite(deleted) ? deleted : 0,
+        status: status.get(file) ?? "modified",
       })
     }
     return result

+ 46 - 0
packages/opencode/test/snapshot/snapshot.test.ts

@@ -749,6 +749,52 @@ test("revert preserves file that existed in snapshot when deleted then recreated
   })
 })
 
+test("diffFull sets status based on git change type", async () => {
+  await using tmp = await bootstrap()
+  await Instance.provide({
+    directory: tmp.path,
+    fn: async () => {
+      await Bun.write(`${tmp.path}/grow.txt`, "one\n")
+      await Bun.write(`${tmp.path}/trim.txt`, "line1\nline2\n")
+      await Bun.write(`${tmp.path}/delete.txt`, "gone")
+
+      const before = await Snapshot.track()
+      expect(before).toBeTruthy()
+
+      await Bun.write(`${tmp.path}/grow.txt`, "one\ntwo\n")
+      await Bun.write(`${tmp.path}/trim.txt`, "line1\n")
+      await $`rm ${tmp.path}/delete.txt`.quiet()
+      await Bun.write(`${tmp.path}/added.txt`, "new")
+
+      const after = await Snapshot.track()
+      expect(after).toBeTruthy()
+
+      const diffs = await Snapshot.diffFull(before!, after!)
+      expect(diffs.length).toBe(4)
+
+      const added = diffs.find((d) => d.file === "added.txt")
+      expect(added).toBeDefined()
+      expect(added!.status).toBe("added")
+
+      const deleted = diffs.find((d) => d.file === "delete.txt")
+      expect(deleted).toBeDefined()
+      expect(deleted!.status).toBe("deleted")
+
+      const grow = diffs.find((d) => d.file === "grow.txt")
+      expect(grow).toBeDefined()
+      expect(grow!.status).toBe("modified")
+      expect(grow!.additions).toBeGreaterThan(0)
+      expect(grow!.deletions).toBe(0)
+
+      const trim = diffs.find((d) => d.file === "trim.txt")
+      expect(trim).toBeDefined()
+      expect(trim!.status).toBe("modified")
+      expect(trim!.additions).toBe(0)
+      expect(trim!.deletions).toBeGreaterThan(0)
+    },
+  })
+})
+
 test("diffFull with new file additions", async () => {
   await using tmp = await bootstrap()
   await Instance.provide({

+ 1 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -96,6 +96,7 @@ export type FileDiff = {
   after: string
   additions: number
   deletions: number
+  status?: "added" | "deleted" | "modified"
 }
 
 export type UserMessage = {