arche / internal/diff/diff.go

commit 154431fd
  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}