patch.go

  1package git
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"math"
  7	"strings"
  8	"sync"
  9
 10	git "github.com/aymanbagabas/git-module"
 11	"github.com/dustin/go-humanize/english"
 12	"github.com/sergi/go-diff/diffmatchpatch"
 13)
 14
 15// DiffSection is a wrapper to git.DiffSection with helper methods.
 16type DiffSection struct {
 17	*git.DiffSection
 18
 19	initOnce sync.Once
 20	dmp      *diffmatchpatch.DiffMatchPatch
 21}
 22
 23// diffFor computes inline diff for the given line.
 24func (s *DiffSection) diffFor(line *git.DiffLine) string {
 25	fallback := line.Content
 26
 27	// Find equivalent diff line, ignore when not found.
 28	var diff1, diff2 string
 29	switch line.Type {
 30	case git.DiffLineAdd:
 31		compareLine := s.DiffSection.Line(git.DiffLineDelete, line.RightLine)
 32		if compareLine == nil {
 33			return fallback
 34		}
 35
 36		diff1 = compareLine.Content
 37		diff2 = line.Content
 38
 39	case git.DiffLineDelete:
 40		compareLine := s.DiffSection.Line(git.DiffLineAdd, line.LeftLine)
 41		if compareLine == nil {
 42			return fallback
 43		}
 44
 45		diff1 = line.Content
 46		diff2 = compareLine.Content
 47
 48	case git.DiffLinePlain, git.DiffLineSection:
 49		return fallback
 50	default:
 51		return fallback
 52	}
 53
 54	s.initOnce.Do(func() {
 55		s.dmp = diffmatchpatch.New()
 56		s.dmp.DiffEditCost = 100
 57	})
 58
 59	diffs := s.dmp.DiffMain(diff1[1:], diff2[1:], true)
 60	diffs = s.dmp.DiffCleanupEfficiency(diffs)
 61
 62	return diffsToString(diffs, line.Type)
 63}
 64
 65func diffsToString(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) string {
 66	buf := bytes.NewBuffer(nil)
 67
 68	// Reproduce signs which are cutted for inline diff before.
 69	switch lineType {
 70	case git.DiffLineAdd:
 71		buf.WriteByte('+')
 72	case git.DiffLineDelete:
 73		buf.WriteByte('-')
 74	case git.DiffLinePlain, git.DiffLineSection:
 75		// No prefix for these line types
 76	}
 77
 78	for i := range diffs {
 79		switch {
 80		case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DiffLineAdd:
 81			buf.WriteString(diffs[i].Text)
 82		case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DiffLineDelete:
 83			buf.WriteString(diffs[i].Text)
 84		case diffs[i].Type == diffmatchpatch.DiffEqual:
 85			buf.WriteString(diffs[i].Text)
 86		}
 87	}
 88
 89	return buf.String()
 90}
 91
 92// DiffFile is a wrapper to git.DiffFile with helper methods.
 93type DiffFile struct {
 94	*git.DiffFile
 95	Sections []*DiffSection
 96}
 97
 98// DiffFileChange represents a file diff.
 99type DiffFileChange struct {
100	hash string
101	name string
102	mode git.EntryMode
103}
104
105// Hash returns the diff file hash.
106func (f *DiffFileChange) Hash() string {
107	return f.hash
108}
109
110// Name returns the diff name.
111func (f *DiffFileChange) Name() string {
112	return f.name
113}
114
115// Mode returns the diff file mode.
116func (f *DiffFileChange) Mode() git.EntryMode {
117	return f.mode
118}
119
120// Files returns the diff files.
121func (f *DiffFile) Files() (from *DiffFileChange, to *DiffFileChange) {
122	if f.DiffFile.OldIndex != git.EmptyID {
123		from = &DiffFileChange{
124			hash: f.DiffFile.OldIndex,
125			name: f.DiffFile.OldName(),
126			mode: f.DiffFile.OldMode(),
127		}
128	}
129	if f.DiffFile.Index != git.EmptyID {
130		to = &DiffFileChange{
131			hash: f.DiffFile.Index,
132			name: f.DiffFile.Name,
133			mode: f.DiffFile.Mode(),
134		}
135	}
136	return
137}
138
139// FileStats
140type FileStats []*DiffFile
141
142// String returns a string representation of file stats.
143func (fs FileStats) String() string {
144	return printStats(fs)
145}
146
147func printStats(stats FileStats) string {
148	padLength := float64(len(" "))
149	newlineLength := float64(len("\n"))
150	separatorLength := float64(len("|"))
151	// Soft line length limit. The text length calculation below excludes
152	// length of the change number. Adding that would take it closer to 80,
153	// but probably not more than 80, until it's a huge number.
154	lineLength := 72.0
155
156	// Get the longest filename and longest total change.
157	var longestLength float64
158	var longestTotalChange float64
159	for _, fs := range stats {
160		if int(longestLength) < len(fs.DiffFile.Name) {
161			longestLength = float64(len(fs.DiffFile.Name))
162		}
163		totalChange := fs.DiffFile.NumAdditions() + fs.DiffFile.NumDeletions()
164		if int(longestTotalChange) < totalChange {
165			longestTotalChange = float64(totalChange)
166		}
167	}
168
169	// Parts of the output:
170	// <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
171	// example: " main.go | 10 +++++++--- "
172
173	// <pad><filename><pad>
174	leftTextLength := padLength + longestLength + padLength
175
176	// <pad><number><pad><+++++/-----><newline>
177	// Excluding number length here.
178	rightTextLength := padLength + padLength + newlineLength
179
180	totalTextArea := leftTextLength + separatorLength + rightTextLength
181	heightOfHistogram := lineLength - totalTextArea
182
183	// Scale the histogram.
184	var scaleFactor float64
185	if longestTotalChange > heightOfHistogram {
186		// Scale down to heightOfHistogram.
187		scaleFactor = longestTotalChange / heightOfHistogram
188	} else {
189		scaleFactor = 1.0
190	}
191
192	taddc := 0
193	tdelc := 0
194	output := strings.Builder{}
195	for _, fs := range stats {
196		taddc += fs.DiffFile.NumAdditions()
197		tdelc += fs.DiffFile.NumDeletions()
198		addn := float64(fs.DiffFile.NumAdditions())
199		deln := float64(fs.DiffFile.NumDeletions())
200		addc := int(math.Floor(addn / scaleFactor))
201		delc := int(math.Floor(deln / scaleFactor))
202		if addc < 0 {
203			addc = 0
204		}
205		if delc < 0 {
206			delc = 0
207		}
208		adds := strings.Repeat("+", addc)
209		dels := strings.Repeat("-", delc)
210		diffLines := fmt.Sprint(fs.DiffFile.NumAdditions() + fs.DiffFile.NumDeletions())
211		totalDiffLines := fmt.Sprint(int(longestTotalChange))
212		fmt.Fprintf(&output, "%s | %s %s%s\n",
213			fs.DiffFile.Name+strings.Repeat(" ", int(longestLength)-len(fs.DiffFile.Name)),
214			strings.Repeat(" ", len(totalDiffLines)-len(diffLines))+diffLines,
215			adds,
216			dels)
217	}
218	files := len(stats)
219	fc := fmt.Sprintf("%s changed", english.Plural(files, "file", ""))
220	ins := fmt.Sprintf("%s(+)", english.Plural(taddc, "insertion", ""))
221	dels := fmt.Sprintf("%s(-)", english.Plural(tdelc, "deletion", ""))
222	fmt.Fprint(&output, fc)
223	if taddc > 0 {
224		fmt.Fprintf(&output, ", %s", ins)
225	}
226	if tdelc > 0 {
227		fmt.Fprintf(&output, ", %s", dels)
228	}
229	fmt.Fprint(&output, "\n")
230
231	return output.String()
232}
233
234// Diff is a wrapper around git.Diff with helper methods.
235type Diff struct {
236	*git.Diff
237	Files []*DiffFile
238}
239
240// FileStats returns the diff file stats.
241func (d *Diff) Stats() FileStats {
242	return d.Files
243}
244
245const (
246	dstPrefix = "b/"
247	srcPrefix = "a/"
248)
249
250func appendPathLines(lines []string, fromPath, toPath string, isBinary bool) []string {
251	if isBinary {
252		return append(lines,
253			fmt.Sprintf("Binary files %s and %s differ", fromPath, toPath),
254		)
255	}
256	return append(lines,
257		fmt.Sprintf("--- %s", fromPath),
258		fmt.Sprintf("+++ %s", toPath),
259	)
260}
261
262func writeFilePatchHeader(sb *strings.Builder, filePatch *DiffFile) {
263	from, to := filePatch.Files()
264	if from == nil && to == nil {
265		return
266	}
267	isBinary := filePatch.DiffFile.IsBinary()
268
269	var lines []string
270	switch {
271	case from != nil && to != nil:
272		hashEquals := from.Hash() == to.Hash()
273		lines = append(lines,
274			fmt.Sprintf("diff --git %s%s %s%s",
275				srcPrefix, from.Name(), dstPrefix, to.Name()),
276		)
277		if from.Mode() != to.Mode() {
278			lines = append(lines,
279				fmt.Sprintf("old mode %o", from.Mode()),
280				fmt.Sprintf("new mode %o", to.Mode()),
281			)
282		}
283		if from.Name() != to.Name() {
284			lines = append(lines,
285				fmt.Sprintf("rename from %s", from.Name()),
286				fmt.Sprintf("rename to %s", to.Name()),
287			)
288		}
289		if from.Mode() != to.Mode() && !hashEquals {
290			lines = append(lines,
291				fmt.Sprintf("index %s..%s", from.Hash(), to.Hash()),
292			)
293		} else if !hashEquals {
294			lines = append(lines,
295				fmt.Sprintf("index %s..%s %o", from.Hash(), to.Hash(), from.Mode()),
296			)
297		}
298		if !hashEquals {
299			lines = appendPathLines(lines, srcPrefix+from.Name(), dstPrefix+to.Name(), isBinary)
300		}
301	case from == nil:
302		lines = append(lines,
303			fmt.Sprintf("diff --git %s %s", srcPrefix+to.Name(), dstPrefix+to.Name()),
304			fmt.Sprintf("new file mode %o", to.Mode()),
305			fmt.Sprintf("index %s..%s", git.EmptyID, to.Hash()),
306		)
307		lines = appendPathLines(lines, "/dev/null", dstPrefix+to.Name(), isBinary)
308	case to == nil:
309		lines = append(lines,
310			fmt.Sprintf("diff --git %s %s", srcPrefix+from.Name(), dstPrefix+from.Name()),
311			fmt.Sprintf("deleted file mode %o", from.Mode()),
312			fmt.Sprintf("index %s..%s", from.Hash(), git.EmptyID),
313		)
314		lines = appendPathLines(lines, srcPrefix+from.Name(), "/dev/null", isBinary)
315	}
316
317	sb.WriteString(lines[0])
318	for _, line := range lines[1:] {
319		sb.WriteByte('\n')
320		sb.WriteString(line)
321	}
322	sb.WriteByte('\n')
323}
324
325// Patch returns the diff as a patch.
326func (d *Diff) Patch() string {
327	var p strings.Builder
328	for _, f := range d.Files {
329		writeFilePatchHeader(&p, f)
330		for _, s := range f.Sections {
331			for _, l := range s.DiffSection.Lines {
332				p.WriteString(s.diffFor(l))
333				p.WriteString("\n")
334			}
335		}
336	}
337	return p.String()
338}
339
340func toDiff(ddiff *git.Diff) *Diff {
341	files := make([]*DiffFile, 0, len(ddiff.Files))
342	for _, df := range ddiff.Files {
343		sections := make([]*DiffSection, 0, len(df.Sections))
344		for _, ds := range df.Sections {
345			sections = append(sections, &DiffSection{
346				DiffSection: ds,
347			})
348		}
349		files = append(files, &DiffFile{
350			DiffFile: df,
351			Sections: sections,
352		})
353	}
354	diff := &Diff{
355		Diff:  ddiff,
356		Files: files,
357	}
358	return diff
359}