1package diff
2
3import (
4 "fmt"
5 "strings"
6
7 "arche/internal/object"
8 "arche/internal/repo"
9
10 "github.com/sergi/go-diff/diffmatchpatch"
11)
12
13type FileDiff struct {
14 Path string
15 Status rune
16 Patch string
17}
18
19func TreeDiff(r *repo.Repo, treeA, treeB [32]byte) ([]FileDiff, error) {
20 aFiles := make(map[string][32]byte)
21 bFiles := make(map[string][32]byte)
22
23 if treeA != object.ZeroID {
24 if err := flattenTree(r, treeA, "", aFiles); err != nil {
25 return nil, fmt.Errorf("diff tree A: %w", err)
26 }
27 }
28 if treeB != object.ZeroID {
29 if err := flattenTree(r, treeB, "", bFiles); err != nil {
30 return nil, fmt.Errorf("diff tree B: %w", err)
31 }
32 }
33
34 var out []FileDiff
35 dmp := diffmatchpatch.New()
36
37 for path, aID := range aFiles {
38 if _, ok := bFiles[path]; !ok {
39 content, _ := readBlob(r, aID)
40 out = append(out, FileDiff{
41 Path: path,
42 Status: 'D',
43 Patch: unifiedDiff(dmp, path, content, ""),
44 })
45 }
46 }
47
48 for path, bID := range bFiles {
49 if _, ok := aFiles[path]; !ok {
50 content, _ := readBlob(r, bID)
51 out = append(out, FileDiff{
52 Path: path,
53 Status: 'A',
54 Patch: unifiedDiff(dmp, path, "", content),
55 })
56 }
57 }
58
59 for path, aID := range aFiles {
60 bID, ok := bFiles[path]
61 if !ok {
62 continue
63 }
64 if aID == bID {
65 continue
66 }
67 aContent, _ := readBlob(r, aID)
68 bContent, _ := readBlob(r, bID)
69 if aContent == bContent {
70 continue
71 }
72 out = append(out, FileDiff{
73 Path: path,
74 Status: 'M',
75 Patch: unifiedDiff(dmp, path, aContent, bContent),
76 })
77 }
78
79 sortFileDiffs(out)
80 return out, nil
81}
82
83func CommitDiff(r *repo.Repo, commitID [32]byte) ([]FileDiff, error) {
84 c, err := r.ReadCommit(commitID)
85 if err != nil {
86 return nil, err
87 }
88 var parentTree [32]byte
89 if len(c.Parents) > 0 {
90 parent, err := r.ReadCommit(c.Parents[0])
91 if err != nil {
92 return nil, err
93 }
94 parentTree = parent.TreeID
95 }
96 return TreeDiff(r, parentTree, c.TreeID)
97}
98
99func FlattenTree(r *repo.Repo, treeID [32]byte, out map[string][32]byte) error {
100 return flattenTree(r, treeID, "", out)
101}
102
103func UnifiedDiff(path, before, after string) string {
104 dmp := diffmatchpatch.New()
105 return unifiedDiff(dmp, path, before, after)
106}
107
108func unifiedDiff(dmp *diffmatchpatch.DiffMatchPatch, path, before, after string) string {
109 if !isPrintable(before) || !isPrintable(after) {
110 if before == "" {
111 return fmt.Sprintf("--- /dev/null\n+++ b/%s\n(binary file added)\n", path)
112 }
113 if after == "" {
114 return fmt.Sprintf("--- a/%s\n+++ /dev/null\n(binary file deleted)\n", path)
115 }
116 return fmt.Sprintf("--- a/%s\n+++ b/%s\n(binary file modified)\n", path, path)
117 }
118
119 diffs := dmp.DiffMain(before, after, true)
120 dmp.DiffCleanupSemantic(diffs)
121
122 hasChange := false
123 for _, d := range diffs {
124 if d.Type != diffmatchpatch.DiffEqual {
125 hasChange = true
126 break
127 }
128 }
129 if !hasChange {
130 return ""
131 }
132
133 var sb strings.Builder
134 aPath, bPath := "a/"+path, "b/"+path
135 if before == "" {
136 aPath = "/dev/null"
137 }
138 if after == "" {
139 bPath = "/dev/null"
140 }
141 fmt.Fprintf(&sb, "--- %s\n+++ %s\n", aPath, bPath)
142 beforeLines := len(splitLines(before))
143 afterLines := len(splitLines(after))
144 fmt.Fprintf(&sb, "@@ -1,%d +1,%d @@\n", beforeLines, afterLines)
145
146 for _, diff := range diffs {
147 for _, line := range splitLines(diff.Text) {
148 switch diff.Type {
149 case diffmatchpatch.DiffEqual:
150 fmt.Fprintf(&sb, " %s\n", line)
151 case diffmatchpatch.DiffInsert:
152 fmt.Fprintf(&sb, "+%s\n", line)
153 case diffmatchpatch.DiffDelete:
154 fmt.Fprintf(&sb, "-%s\n", line)
155 }
156 }
157 }
158 return sb.String()
159}
160
161func splitLines(s string) []string {
162 if s == "" {
163 return nil
164 }
165 lines := strings.Split(s, "\n")
166 if len(lines) > 0 && lines[len(lines)-1] == "" {
167 lines = lines[:len(lines)-1]
168 }
169 return lines
170}
171
172func isPrintable(s string) bool {
173 for _, b := range []byte(s) {
174 if b == 0 {
175 return false
176 }
177 }
178 return true
179}
180
181func readBlob(r *repo.Repo, id [32]byte) (string, error) {
182 content, err := r.ReadBlob(id)
183 if err != nil {
184 return "", err
185 }
186 return string(content), nil
187}
188
189func flattenTree(r *repo.Repo, treeID [32]byte, prefix string, out map[string][32]byte) error {
190 if treeID == object.ZeroID {
191 return nil
192 }
193 t, err := r.ReadTree(treeID)
194 if err != nil {
195 return err
196 }
197 for _, e := range t.Entries {
198 rel := join(prefix, e.Name)
199 switch e.Mode {
200 case object.ModeDir:
201 if err := flattenTree(r, e.ObjectID, rel, out); err != nil {
202 return err
203 }
204 default:
205 out[rel] = e.ObjectID
206 }
207 }
208 return nil
209}
210
211func join(prefix, name string) string {
212 if prefix == "" {
213 return name
214 }
215 return prefix + "/" + name
216}
217
218func sortFileDiffs(diffs []FileDiff) {
219 for i := 1; i < len(diffs); i++ {
220 for j := i; j > 0 && diffs[j].Path < diffs[j-1].Path; j-- {
221 diffs[j], diffs[j-1] = diffs[j-1], diffs[j]
222 }
223 }
224}