patch.go

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