aboutsummaryrefslogtreecommitdiff
path: root/diff
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-04-02 06:23:30 +0000
committerGravatar Runxi Yu2026-04-02 06:28:39 +0000
commita041d523de389b65b98a5373a8034041db2a8d83 (patch)
tree7b423dc735f463be616045f2c3c2095a7737aca7 /diff
parentresearch: Add dynamic pack resources (diff)
signatureNo signature
*: Remove
Diffstat (limited to 'diff')
-rw-r--r--diff/diff.go2
-rw-r--r--diff/lines/chunk.go20
-rw-r--r--diff/lines/diff.go231
-rw-r--r--diff/lines/diff_test.go333
-rw-r--r--diff/trees/diff.go22
-rw-r--r--diff/trees/diff_recursive.go176
-rw-r--r--diff/trees/diff_test.go255
-rw-r--r--diff/trees/entry.go15
-rw-r--r--diff/trees/kind.go15
-rw-r--r--diff/trees/path.go17
10 files changed, 0 insertions, 1086 deletions
diff --git a/diff/diff.go b/diff/diff.go
deleted file mode 100644
index 74b62119..00000000
--- a/diff/diff.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package diff encapsulates diff-providing subpackages for direct use.
-package diff
diff --git a/diff/lines/chunk.go b/diff/lines/chunk.go
deleted file mode 100644
index b5856d29..00000000
--- a/diff/lines/chunk.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package lines
-
-// Chunk represents a contiguous region of lines categorized
-// as unchanged, deleted, or added.
-type Chunk struct {
- Kind ChunkKind
- Data []byte
-}
-
-// ChunkKind enumerates the type of diff chunk.
-type ChunkKind int
-
-const (
- // ChunkKindUnchanged represents an unchanged diff chunk.
- ChunkKindUnchanged ChunkKind = iota
- // ChunkKindDeleted represents a deleted diff chunk.
- ChunkKindDeleted
- // ChunkKindAdded represents an added diff chunk.
- ChunkKindAdded
-)
diff --git a/diff/lines/diff.go b/diff/lines/diff.go
deleted file mode 100644
index 6f958923..00000000
--- a/diff/lines/diff.go
+++ /dev/null
@@ -1,231 +0,0 @@
-// Package lines provides routines to perform line-based diffs.
-package lines
-
-import "bytes"
-
-// Diff performs a line-based diff.
-// Lines are bytes up to and including '\n' (final line may lack '\n').
-func Diff(oldB, newB []byte) ([]Chunk, error) { //nolint:maintidx
- type lineRef struct {
- base []byte
- start int
- end int
- }
-
- split := func(b []byte) []lineRef {
- if len(b) == 0 {
- return nil
- }
-
- var res []lineRef
-
- start := 0
-
- for i := range b {
- if b[i] == '\n' {
- res = append(res, lineRef{base: b, start: start, end: i + 1})
- start = i + 1
- }
- }
-
- if start < len(b) {
- res = append(res, lineRef{base: b, start: start, end: len(b)})
- }
-
- return res
- }
-
- oldLines := split(oldB)
- newLines := split(newB)
-
- n := len(oldLines)
-
- m := len(newLines)
- if n == 0 && m == 0 {
- return nil, nil
- }
-
- idOf := make(map[string]int)
- nextID := 0
- oldIDs := make([]int, n)
-
- for i, ln := range oldLines {
- key := string(ln.base[ln.start:ln.end])
-
- id, ok := idOf[key]
- if !ok {
- id = nextID
- idOf[key] = id
- nextID++
- }
-
- oldIDs[i] = id
- }
-
- newIDs := make([]int, m)
-
- for i, ln := range newLines {
- key := string(ln.base[ln.start:ln.end])
-
- id, ok := idOf[key]
- if !ok {
- id = nextID
- idOf[key] = id
- nextID++
- }
-
- newIDs[i] = id
- }
-
- maxDist := n + m
- offset := maxDist
- trace := make([][]int, 0, maxDist+1)
-
- Vprev := make([]int, 2*maxDist+1)
- for i := range Vprev {
- Vprev[i] = -1
- }
-
- x0 := 0
-
- y0 := 0
- for x0 < n && y0 < m && oldIDs[x0] == newIDs[y0] {
- x0++
- y0++
- }
-
- Vprev[offset+0] = x0
- trace = append(trace, append([]int(nil), Vprev...))
-
- found := x0 >= n && y0 >= m
-
- for D := 1; D <= maxDist && !found; D++ {
- V := make([]int, 2*maxDist+1)
- for i := range V {
- V[i] = -1
- }
-
- for k := -D; k <= D; k += 2 {
- var x int
- if k == -D || (k != D && Vprev[offset+(k-1)] < Vprev[offset+(k+1)]) {
- x = Vprev[offset+(k+1)]
- } else {
- x = Vprev[offset+(k-1)] + 1
- }
-
- y := x - k
-
- for x < n && y < m && oldIDs[x] == newIDs[y] {
- x++
- y++
- }
-
- V[offset+k] = x
-
- if x >= n && y >= m {
- trace = append(trace, V)
- found = true
-
- break
- }
- }
-
- if !found {
- trace = append(trace, V)
- Vprev = V
- }
- }
-
- type edit struct {
- kind ChunkKind
- lineref lineRef
- }
-
- revEdits := make([]edit, 0, n+m)
-
- x := n
-
- y := m
- for D := len(trace) - 1; D >= 0; D-- {
- k := x - y
-
- var (
- prevK int
- prevX int
- prevY int
- )
-
- if D > 0 {
- prevV := trace[D-1]
- if k == -D || (k != D && prevV[offset+(k-1)] < prevV[offset+(k+1)]) {
- prevK = k + 1
- } else {
- prevK = k - 1
- }
-
- prevX = prevV[offset+prevK]
- prevY = prevX - prevK
- }
-
- for x > prevX && y > prevY {
- x--
- y--
-
- revEdits = append(revEdits, edit{kind: ChunkKindUnchanged, lineref: oldLines[x]})
- }
-
- if D == 0 {
- break
- }
-
- if x == prevX {
- y--
- revEdits = append(revEdits, edit{kind: ChunkKindAdded, lineref: newLines[y]})
- } else {
- x--
- revEdits = append(revEdits, edit{kind: ChunkKindDeleted, lineref: oldLines[x]})
- }
- }
-
- for i, j := 0, len(revEdits)-1; i < j; i, j = i+1, j-1 {
- revEdits[i], revEdits[j] = revEdits[j], revEdits[i]
- }
-
- var out []Chunk
-
- type meta struct {
- base []byte
- start int
- end int
- }
-
- var metas []meta
-
- for _, e := range revEdits {
- curBase := e.lineref.base
- curStart := e.lineref.start
- curEnd := e.lineref.end
-
- if len(out) == 0 || out[len(out)-1].Kind != e.kind {
- out = append(out, Chunk{Kind: e.kind, Data: curBase[curStart:curEnd]})
- metas = append(metas, meta{base: curBase, start: curStart, end: curEnd})
-
- continue
- }
-
- lastIdx := len(out) - 1
- lastMeta := metas[lastIdx]
-
- if bytes.Equal(lastMeta.base, curBase) && lastMeta.end == curStart {
- metas[lastIdx].end = curEnd
- out[lastIdx].Data = curBase[metas[lastIdx].start:metas[lastIdx].end]
-
- continue
- }
-
- out[lastIdx].Data = append(out[lastIdx].Data, curBase[curStart:curEnd]...)
- metas[lastIdx] = meta{base: nil, start: 0, end: 0}
- }
-
- return out, nil
-}
diff --git a/diff/lines/diff_test.go b/diff/lines/diff_test.go
deleted file mode 100644
index c5d5be9f..00000000
--- a/diff/lines/diff_test.go
+++ /dev/null
@@ -1,333 +0,0 @@
-package lines_test
-
-import (
- "bytes"
- "strconv"
- "strings"
- "testing"
-
- "codeberg.org/lindenii/furgit/diff/lines"
-)
-
-func TestDiff(t *testing.T) { //nolint:maintidx
- t.Parallel()
-
- tests := []struct {
- name string
- oldInput string
- newInput string
- expected []lines.Chunk
- }{
- {
- name: "empty inputs produce no chunks",
- oldInput: "",
- newInput: "",
- expected: []lines.Chunk{},
- },
- {
- name: "only additions",
- oldInput: "",
- newInput: "alpha\nbeta\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindAdded, Data: []byte("alpha\nbeta\n")},
- },
- },
- {
- name: "only deletions",
- oldInput: "alpha\nbeta\n",
- newInput: "",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindDeleted, Data: []byte("alpha\nbeta\n")},
- },
- },
- {
- name: "unchanged content is grouped",
- oldInput: "same\nlines\n",
- newInput: "same\nlines\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("same\nlines\n")},
- },
- },
- {
- name: "insertion in the middle",
- oldInput: "a\nb\nc\n",
- newInput: "a\nb\nX\nc\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("a\nb\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("X\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("c\n")},
- },
- },
- {
- name: "replacement without trailing newline",
- oldInput: "first\nsecond",
- newInput: "first\nsecond\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("first\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("second")},
- {Kind: lines.ChunkKindAdded, Data: []byte("second\n")},
- },
- },
- {
- name: "line replacement",
- oldInput: "a\nb\nc\n",
- newInput: "a\nB\nc\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("a\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("b\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("B\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("c\n")},
- },
- },
- {
- name: "swap adjacent lines",
- oldInput: "A\nB\n",
- newInput: "B\nA\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindDeleted, Data: []byte("A\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("B\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("A\n")},
- },
- },
- {
- name: "indentation change is a full line replacement",
- oldInput: "func main() {\n\treturn\n}\n",
- newInput: "func main() {\n return\n}\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("func main() {\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("\treturn\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte(" return\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("}\n")},
- },
- },
- {
- name: "commenting out lines",
- oldInput: "code\n",
- newInput: "// code\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindDeleted, Data: []byte("code\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("// code\n")},
- },
- },
- {
- name: "reducing repeating lines",
- oldInput: "log\nlog\nlog\n",
- newInput: "log\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("log\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("log\nlog\n")},
- },
- },
- {
- name: "expanding repeating lines",
- oldInput: "tick\n",
- newInput: "tick\ntick\ntick\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("tick\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("tick\ntick\n")},
- },
- },
- {
- name: "interleaved modifications",
- oldInput: "keep\nchange\nkeep\nchange\n",
- newInput: "keep\nfixed\nkeep\nfixed\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("keep\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("change\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("fixed\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("keep\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("change\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("fixed\n")},
- },
- },
- {
- name: "large common header and footer",
- oldInput: "header\nheader\nheader\nOLD\nfooter\nfooter\n",
- newInput: "header\nheader\nheader\nNEW\nfooter\nfooter\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("header\nheader\nheader\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("OLD\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("NEW\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("footer\nfooter\n")},
- },
- },
- {
- name: "completely different content",
- oldInput: "apple\nbanana\n",
- newInput: "cherry\ndate\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindDeleted, Data: []byte("apple\nbanana\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("cherry\ndate\n")},
- },
- },
- {
- name: "unicode and emoji changes",
- oldInput: "Hello 🌍\nYay\n",
- newInput: "Hello 🌎\nYay\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindDeleted, Data: []byte("Hello 🌍\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("Hello 🌎\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("Yay\n")},
- },
- },
- {
- name: "binary data with embedded newlines",
- oldInput: "\x00\x01\n\x02\x03\n",
- newInput: "\x00\x01\n\x02\xFF\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("\x00\x01\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("\x02\x03\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("\x02\xFF\n")},
- },
- },
- {
- name: "adding trailing newline to last line",
- oldInput: "Line 1\nLine 2",
- newInput: "Line 1\nLine 2\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("Line 1\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("Line 2")},
- {Kind: lines.ChunkKindAdded, Data: []byte("Line 2\n")},
- },
- },
- {
- name: "removing trailing newline",
- oldInput: "A\nB\n",
- newInput: "A\nB",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("A\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("B\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("B")},
- },
- },
- {
- name: "inserting blank lines",
- oldInput: "A\nB\n",
- newInput: "A\n\n\nB\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("A\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("\n\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("B\n")},
- },
- },
- {
- name: "collapsing blank lines",
- oldInput: "A\n\n\n\nB\n",
- newInput: "A\nB\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("A\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("\n\n\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("B\n")},
- },
- },
- {
- name: "case sensitivity check",
- oldInput: "FOO\nbar\n",
- newInput: "foo\nbar\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindDeleted, Data: []byte("FOO\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("foo\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("bar\n")},
- },
- },
- {
- name: "partial line match is full mismatch",
- oldInput: "The quick brown fox\n",
- newInput: "The quick brown fox jumps\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindDeleted, Data: []byte("The quick brown fox\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("The quick brown fox jumps\n")},
- },
- },
- {
- name: "inserting middle content",
- oldInput: "Top\nBottom\n",
- newInput: "Top\nMiddle\nBottom\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("Top\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("Middle\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("Bottom\n")},
- },
- },
- {
- name: "block move simulated",
- oldInput: "BlockA\nBlockB\nBlockC\n",
- newInput: "BlockA\nBlockC\nBlockB\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("BlockA\n")},
- {Kind: lines.ChunkKindDeleted, Data: []byte("BlockB\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("BlockC\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("BlockB\n")},
- },
- },
- {
- name: "alternating additions",
- oldInput: "A\nB\nC\n",
- newInput: "A\n1\nB\n2\nC\n",
- expected: []lines.Chunk{
- {Kind: lines.ChunkKindUnchanged, Data: []byte("A\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("1\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("B\n")},
- {Kind: lines.ChunkKindAdded, Data: []byte("2\n")},
- {Kind: lines.ChunkKindUnchanged, Data: []byte("C\n")},
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
- chunks, err := lines.Diff([]byte(tt.oldInput), []byte(tt.newInput))
- if err != nil {
- t.Fatalf("Diff returned error: %v", err)
- }
-
- if len(chunks) != len(tt.expected) {
- t.Fatalf("expected %d chunks, got %d: %s", len(tt.expected), len(chunks), formatChunks(chunks))
- }
-
- for i := range tt.expected {
- if chunks[i].Kind != tt.expected[i].Kind {
- t.Fatalf("chunk %d kind mismatch: got %v, want %v; chunks: %s", i, chunks[i].Kind, tt.expected[i].Kind, formatChunks(chunks))
- }
-
- if !bytes.Equal(chunks[i].Data, tt.expected[i].Data) {
- t.Fatalf("chunk %d data mismatch: got %q, want %q; chunks: %s", i, string(chunks[i].Data), string(tt.expected[i].Data), formatChunks(chunks))
- }
- }
- })
- }
-}
-
-func formatChunks(chunks []lines.Chunk) string {
- var b strings.Builder
- b.WriteByte('[')
-
- for i, chunk := range chunks {
- if i > 0 {
- b.WriteString(", ")
- }
-
- b.WriteString(chunkKindName(chunk.Kind))
- b.WriteByte(':')
- b.WriteString(strconv.Quote(string(chunk.Data)))
- }
-
- b.WriteByte(']')
-
- return b.String()
-}
-
-func chunkKindName(kind lines.ChunkKind) string {
- switch kind {
- case lines.ChunkKindUnchanged:
- return "U"
- case lines.ChunkKindDeleted:
- return "D"
- case lines.ChunkKindAdded:
- return "A"
- default:
- return "?"
- }
-}
diff --git a/diff/trees/diff.go b/diff/trees/diff.go
deleted file mode 100644
index 0f3cf1f2..00000000
--- a/diff/trees/diff.go
+++ /dev/null
@@ -1,22 +0,0 @@
-// Package trees provides recursive diffs between Git tree objects.
-package trees
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-// Diff compares two trees and returns recursive differences.
-//
-// readTree is used to lazily load child trees by object ID when recursion
-// reaches directory entries.
-func Diff(a, b *tree.Tree, readTree func(objectid.ObjectID) (*tree.Tree, error)) ([]Entry, error) {
- var out []Entry
-
- err := diffRecursive(a, b, nil, readTree, &out)
- if err != nil {
- return nil, err
- }
-
- return out, nil
-}
diff --git a/diff/trees/diff_recursive.go b/diff/trees/diff_recursive.go
deleted file mode 100644
index 98848b24..00000000
--- a/diff/trees/diff_recursive.go
+++ /dev/null
@@ -1,176 +0,0 @@
-package trees
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-func diffRecursive(a, b *tree.Tree, prefix []byte, readTree func(objectid.ObjectID) (*tree.Tree, error), out *[]Entry) error {
- if a == nil && b == nil {
- return nil
- }
-
- if a == nil {
- for i := range b.Entries {
- entry := &b.Entries[i]
- full := joinPath(prefix, entry.Name)
-
- *out = append(*out, Entry{Path: full, Kind: EntryKindAdded, Old: nil, New: entry})
- if entry.Mode != tree.FileModeDir {
- continue
- }
-
- sub, err := readTree(entry.ID)
- if err != nil {
- return err
- }
-
- err = diffRecursive(nil, sub, full, readTree, out)
- if err != nil {
- return err
- }
- }
-
- return nil
- }
-
- if b == nil {
- for i := range a.Entries {
- entry := &a.Entries[i]
- full := joinPath(prefix, entry.Name)
-
- *out = append(*out, Entry{Path: full, Kind: EntryKindDeleted, Old: entry, New: nil})
- if entry.Mode != tree.FileModeDir {
- continue
- }
-
- sub, err := readTree(entry.ID)
- if err != nil {
- return err
- }
-
- err = diffRecursive(sub, nil, full, readTree, out)
- if err != nil {
- return err
- }
- }
-
- return nil
- }
-
- i := 0
-
- j := 0
- for i < len(a.Entries) && j < len(b.Entries) {
- left := &a.Entries[i]
- right := &b.Entries[j]
-
- cmp := tree.TreeEntryNameCompare(
- left.Name,
- left.Mode,
- right.Name,
- right.Mode == tree.FileModeDir,
- )
- switch {
- case cmp < 0:
- full := joinPath(prefix, left.Name)
-
- *out = append(*out, Entry{Path: full, Kind: EntryKindDeleted, Old: left, New: nil})
- if left.Mode == tree.FileModeDir {
- sub, err := readTree(left.ID)
- if err != nil {
- return err
- }
-
- err = diffRecursive(sub, nil, full, readTree, out)
- if err != nil {
- return err
- }
- }
-
- i++
- case cmp > 0:
- full := joinPath(prefix, right.Name)
-
- *out = append(*out, Entry{Path: full, Kind: EntryKindAdded, Old: nil, New: right})
- if right.Mode == tree.FileModeDir {
- sub, err := readTree(right.ID)
- if err != nil {
- return err
- }
-
- err = diffRecursive(nil, sub, full, readTree, out)
- if err != nil {
- return err
- }
- }
-
- j++
- default:
- full := joinPath(prefix, left.Name)
-
- modified := left.Mode != right.Mode || left.ID != right.ID
- if modified {
- *out = append(*out, Entry{Path: full, Kind: EntryKindModified, Old: left, New: right})
- }
-
- if left.Mode == tree.FileModeDir && right.Mode == tree.FileModeDir && left.ID != right.ID {
- leftSub, err := readTree(left.ID)
- if err != nil {
- return err
- }
-
- rightSub, err := readTree(right.ID)
- if err != nil {
- return err
- }
-
- err = diffRecursive(leftSub, rightSub, full, readTree, out)
- if err != nil {
- return err
- }
- }
-
- i++
- j++
- }
- }
-
- for ; i < len(a.Entries); i++ {
- left := &a.Entries[i]
- full := joinPath(prefix, left.Name)
-
- *out = append(*out, Entry{Path: full, Kind: EntryKindDeleted, Old: left, New: nil})
- if left.Mode == tree.FileModeDir {
- sub, err := readTree(left.ID)
- if err != nil {
- return err
- }
-
- err = diffRecursive(sub, nil, full, readTree, out)
- if err != nil {
- return err
- }
- }
- }
-
- for ; j < len(b.Entries); j++ {
- right := &b.Entries[j]
- full := joinPath(prefix, right.Name)
-
- *out = append(*out, Entry{Path: full, Kind: EntryKindAdded, Old: nil, New: right})
- if right.Mode == tree.FileModeDir {
- sub, err := readTree(right.ID)
- if err != nil {
- return err
- }
-
- err = diffRecursive(nil, sub, full, readTree, out)
- if err != nil {
- return err
- }
- }
- }
-
- return nil
-}
diff --git a/diff/trees/diff_test.go b/diff/trees/diff_test.go
deleted file mode 100644
index 50989a4c..00000000
--- a/diff/trees/diff_test.go
+++ /dev/null
@@ -1,255 +0,0 @@
-package trees_test
-
-import (
- "errors"
- "testing"
-
- "codeberg.org/lindenii/furgit/diff/trees"
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/store/loose"
- "codeberg.org/lindenii/furgit/object/tree"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func TestDiffComplexNestedChanges(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: false})
-
- writeTestFile(t, repo, "README.md", "initial readme\n")
- writeTestFile(t, repo, "unchanged.txt", "leave me as-is\n")
- writeTestFile(t, repo, "dir/file_a.txt", "alpha v1\n")
- writeTestFile(t, repo, "dir/nested/file_b.txt", "beta v1\n")
- writeTestFile(t, repo, "dir/nested/deeper/file_c.txt", "gamma v1\n")
- writeTestFile(t, repo, "dir/nested/deeper/old.txt", "old branch\n")
- writeTestFile(t, repo, "treeB/legacy.txt", "legacy root\n")
- writeTestFile(t, repo, "treeB/sub/retired.txt", "retired\n")
-
- repo.Run(t, "add", ".")
- baseTreeID := parseID(t, algo, repo.Run(t, "write-tree"))
-
- writeTestFile(t, repo, "README.md", "updated readme\n")
- repo.Run(t, "rm", "-f", "dir/file_a.txt")
- writeTestFile(t, repo, "dir/nested/file_b.txt", "beta v2\n")
- repo.Run(t, "rm", "-f", "dir/nested/deeper/old.txt")
- writeTestFile(t, repo, "dir/nested/deeper/new.txt", "new branch entry\n")
- writeTestFile(t, repo, "dir/nested/deeper/branch/info.md", "branch info\n")
- writeTestFile(t, repo, "dir/nested/deeper/branch/subbranch/leaf.txt", "leaf data\n")
- writeTestFile(t, repo, "dir/nested/deeper/branch/subbranch/deep/final.txt", "final artifact\n")
- writeTestFile(t, repo, "dir/newchild.txt", "brand new sibling\n")
- repo.Run(t, "rm", "-r", "-f", "treeB")
- writeTestFile(t, repo, "features/alpha/README.md", "alpha docs\n")
- writeTestFile(t, repo, "features/alpha/beta/gamma.txt", "gamma payload\n")
- writeTestFile(t, repo, "modules/v2/core/main.go", "package core\n")
- writeTestFile(t, repo, "root_addition.txt", "root level file\n")
-
- repo.Run(t, "add", ".")
- updatedTreeID := parseID(t, algo, repo.Run(t, "write-tree"))
-
- store := openLooseStore(t, repo, algo)
- readTree := makeReadTree(t, store, algo)
- baseTree := mustReadTree(t, readTree, baseTreeID)
- updatedTree := mustReadTree(t, readTree, updatedTreeID)
-
- diffs, err := trees.Diff(baseTree, updatedTree, readTree)
- if err != nil {
- t.Fatalf("trees.Diff: %v", err)
- }
-
- expected := map[string]diffExpectation{
- "README.md": {kind: trees.EntryKindModified},
- "dir": {kind: trees.EntryKindModified},
- "dir/file_a.txt": {kind: trees.EntryKindDeleted, newNil: true},
- "dir/newchild.txt": {kind: trees.EntryKindAdded, oldNil: true},
- "dir/nested": {kind: trees.EntryKindModified},
- "dir/nested/file_b.txt": {kind: trees.EntryKindModified},
- "dir/nested/deeper": {kind: trees.EntryKindModified},
- "dir/nested/deeper/old.txt": {kind: trees.EntryKindDeleted, newNil: true},
- "dir/nested/deeper/new.txt": {kind: trees.EntryKindAdded, oldNil: true},
- "dir/nested/deeper/branch": {kind: trees.EntryKindAdded, oldNil: true},
- "dir/nested/deeper/branch/info.md": {kind: trees.EntryKindAdded, oldNil: true},
- "dir/nested/deeper/branch/subbranch": {kind: trees.EntryKindAdded, oldNil: true},
- "dir/nested/deeper/branch/subbranch/leaf.txt": {kind: trees.EntryKindAdded, oldNil: true},
- "dir/nested/deeper/branch/subbranch/deep": {kind: trees.EntryKindAdded, oldNil: true},
- "dir/nested/deeper/branch/subbranch/deep/final.txt": {
- kind: trees.EntryKindAdded,
- oldNil: true,
- },
- "features": {kind: trees.EntryKindAdded, oldNil: true},
- "features/alpha": {kind: trees.EntryKindAdded, oldNil: true},
- "features/alpha/README.md": {kind: trees.EntryKindAdded, oldNil: true},
- "features/alpha/beta": {kind: trees.EntryKindAdded, oldNil: true},
- "features/alpha/beta/gamma.txt": {kind: trees.EntryKindAdded, oldNil: true},
- "modules": {kind: trees.EntryKindAdded, oldNil: true},
- "modules/v2": {kind: trees.EntryKindAdded, oldNil: true},
- "modules/v2/core": {kind: trees.EntryKindAdded, oldNil: true},
- "modules/v2/core/main.go": {kind: trees.EntryKindAdded, oldNil: true},
- "root_addition.txt": {kind: trees.EntryKindAdded, oldNil: true},
- "treeB": {kind: trees.EntryKindDeleted, newNil: true},
- "treeB/legacy.txt": {kind: trees.EntryKindDeleted, newNil: true},
- "treeB/sub": {kind: trees.EntryKindDeleted, newNil: true},
- "treeB/sub/retired.txt": {kind: trees.EntryKindDeleted, newNil: true},
- }
-
- checkDiffs(t, diffs, expected)
- })
-}
-
-func TestDiffDirectoryAddDeleteDeep(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: false})
-
- writeTestFile(t, repo, "old_dir/old.txt", "stale directory\n")
- writeTestFile(t, repo, "old_dir/sub1/legacy.txt", "legacy path\n")
- writeTestFile(t, repo, "old_dir/sub1/nested/end.txt", "legacy end\n")
-
- repo.Run(t, "add", ".")
- originalTreeID := parseID(t, algo, repo.Run(t, "write-tree"))
-
- repo.Run(t, "rm", "-r", "-f", "old_dir")
- writeTestFile(t, repo, "fresh/alpha/beta/new.txt", "brand new directory\n")
- writeTestFile(t, repo, "fresh/alpha/docs/note.md", "docs note\n")
- writeTestFile(t, repo, "fresh/alpha/beta/gamma/delta.txt", "delta payload\n")
-
- repo.Run(t, "add", ".")
- nextTreeID := parseID(t, algo, repo.Run(t, "write-tree"))
-
- store := openLooseStore(t, repo, algo)
- readTree := makeReadTree(t, store, algo)
- originalTree := mustReadTree(t, readTree, originalTreeID)
- nextTree := mustReadTree(t, readTree, nextTreeID)
-
- diffs, err := trees.Diff(originalTree, nextTree, readTree)
- if err != nil {
- t.Fatalf("trees.Diff: %v", err)
- }
-
- expected := map[string]diffExpectation{
- "fresh": {kind: trees.EntryKindAdded, oldNil: true},
- "fresh/alpha": {kind: trees.EntryKindAdded, oldNil: true},
- "fresh/alpha/beta": {kind: trees.EntryKindAdded, oldNil: true},
- "fresh/alpha/beta/new.txt": {kind: trees.EntryKindAdded, oldNil: true},
- "fresh/alpha/beta/gamma": {kind: trees.EntryKindAdded, oldNil: true},
- "fresh/alpha/beta/gamma/delta.txt": {kind: trees.EntryKindAdded, oldNil: true},
- "fresh/alpha/docs": {kind: trees.EntryKindAdded, oldNil: true},
- "fresh/alpha/docs/note.md": {kind: trees.EntryKindAdded, oldNil: true},
- "old_dir": {kind: trees.EntryKindDeleted, newNil: true},
- "old_dir/old.txt": {kind: trees.EntryKindDeleted, newNil: true},
- "old_dir/sub1": {kind: trees.EntryKindDeleted, newNil: true},
- "old_dir/sub1/legacy.txt": {kind: trees.EntryKindDeleted, newNil: true},
- "old_dir/sub1/nested": {kind: trees.EntryKindDeleted, newNil: true},
- "old_dir/sub1/nested/end.txt": {kind: trees.EntryKindDeleted, newNil: true},
- }
-
- checkDiffs(t, diffs, expected)
- })
-}
-
-type diffExpectation struct {
- kind trees.EntryKind
- oldNil bool
- newNil bool
-}
-
-func writeTestFile(t *testing.T, repo *testgit.TestRepo, path, data string) {
- t.Helper()
-
- repo.WriteFileAll(t, path, []byte(data), 0o755, 0o644)
-}
-
-func openLooseStore(t *testing.T, repo *testgit.TestRepo, algo objectid.Algorithm) *loose.Store {
- t.Helper()
-
- root := repo.OpenObjectsRoot(t)
-
- store, err := loose.New(root, algo)
- if err != nil {
- t.Fatalf("loose.New: %v", err)
- }
-
- t.Cleanup(func() { _ = store.Close() })
-
- return store
-}
-
-func makeReadTree(t *testing.T, store *loose.Store, algo objectid.Algorithm) func(objectid.ObjectID) (*tree.Tree, error) {
- t.Helper()
-
- return func(id objectid.ObjectID) (*tree.Tree, error) {
- ty, content, err := store.ReadBytesContent(id)
- if err != nil {
- return nil, err
- }
-
- if ty != objecttype.TypeTree {
- return nil, errors.New("diff/trees test: object is not a tree")
- }
-
- return tree.Parse(content, algo)
- }
-}
-
-func mustReadTree(t *testing.T, readTree func(objectid.ObjectID) (*tree.Tree, error), id objectid.ObjectID) *tree.Tree {
- t.Helper()
-
- tree, err := readTree(id)
- if err != nil {
- t.Fatalf("read tree %s: %v", id, err)
- }
-
- return tree
-}
-
-func parseID(t *testing.T, algo objectid.Algorithm, hex string) objectid.ObjectID {
- t.Helper()
-
- id, err := objectid.ParseHex(algo, hex)
- if err != nil {
- t.Fatalf("parse object id %q: %v", hex, err)
- }
-
- return id
-}
-
-func checkDiffs(t *testing.T, diffs []trees.Entry, expected map[string]diffExpectation) {
- t.Helper()
-
- got := make(map[string]trees.Entry, len(diffs))
- for _, diff := range diffs {
- path := string(diff.Path)
- if _, exists := got[path]; exists {
- t.Fatalf("duplicate diff path %q", path)
- }
-
- got[path] = diff
- }
-
- if len(got) != len(expected) {
- t.Fatalf("diff count = %d, want %d", len(got), len(expected))
- }
-
- for path, want := range expected {
- diff, ok := got[path]
- if !ok {
- t.Fatalf("missing diff for %q", path)
- }
-
- if diff.Kind != want.kind {
- t.Errorf("%s kind = %v, want %v", path, diff.Kind, want.kind)
- }
-
- if (diff.Old == nil) != want.oldNil {
- t.Errorf("%s old nil = %v, want %v", path, diff.Old == nil, want.oldNil)
- }
-
- if (diff.New == nil) != want.newNil {
- t.Errorf("%s new nil = %v, want %v", path, diff.New == nil, want.newNil)
- }
-
- if diff.Kind == trees.EntryKindModified && diff.Old != nil && diff.New != nil && diff.Old.ID == diff.New.ID {
- t.Errorf("%s modified entry should change IDs", path)
- }
- }
-}
diff --git a/diff/trees/entry.go b/diff/trees/entry.go
deleted file mode 100644
index 84813a79..00000000
--- a/diff/trees/entry.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package trees
-
-import "codeberg.org/lindenii/furgit/object/tree"
-
-// Entry is one recursive tree difference at a path.
-type Entry struct {
- // Path is the slash-separated path relative to the diff root.
- Path []byte
- // Kind is the difference kind for this path.
- Kind EntryKind
- // Old is the old tree entry (nil when Kind is EntryKindAdded).
- Old *tree.TreeEntry
- // New is the new tree entry (nil when Kind is EntryKindDeleted).
- New *tree.TreeEntry
-}
diff --git a/diff/trees/kind.go b/diff/trees/kind.go
deleted file mode 100644
index 6fdc6e0d..00000000
--- a/diff/trees/kind.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package trees
-
-// EntryKind identifies a tree-diff entry kind.
-type EntryKind int
-
-const (
- // EntryKindInvalid indicates an invalid diff entry kind.
- EntryKindInvalid EntryKind = iota
- // EntryKindDeleted indicates a deleted path.
- EntryKindDeleted
- // EntryKindAdded indicates an added path.
- EntryKindAdded
- // EntryKindModified indicates a modified path.
- EntryKindModified
-)
diff --git a/diff/trees/path.go b/diff/trees/path.go
deleted file mode 100644
index e40f3de5..00000000
--- a/diff/trees/path.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package trees
-
-func joinPath(prefix, name []byte) []byte {
- if len(prefix) == 0 {
- out := make([]byte, len(name))
- copy(out, name)
-
- return out
- }
-
- out := make([]byte, len(prefix)+1+len(name))
- copy(out, prefix)
- out[len(prefix)] = '/'
- copy(out[len(prefix)+1:], name)
-
- return out
-}