diff options
| author | 2026-04-02 06:23:30 +0000 | |
|---|---|---|
| committer | 2026-04-02 06:28:39 +0000 | |
| commit | a041d523de389b65b98a5373a8034041db2a8d83 (patch) | |
| tree | 7b423dc735f463be616045f2c3c2095a7737aca7 /diff | |
| parent | research: Add dynamic pack resources (diff) | |
| signature | No signature | |
*: Remove
Diffstat (limited to 'diff')
| -rw-r--r-- | diff/diff.go | 2 | ||||
| -rw-r--r-- | diff/lines/chunk.go | 20 | ||||
| -rw-r--r-- | diff/lines/diff.go | 231 | ||||
| -rw-r--r-- | diff/lines/diff_test.go | 333 | ||||
| -rw-r--r-- | diff/trees/diff.go | 22 | ||||
| -rw-r--r-- | diff/trees/diff_recursive.go | 176 | ||||
| -rw-r--r-- | diff/trees/diff_test.go | 255 | ||||
| -rw-r--r-- | diff/trees/entry.go | 15 | ||||
| -rw-r--r-- | diff/trees/kind.go | 15 | ||||
| -rw-r--r-- | diff/trees/path.go | 17 |
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 -} |
