1package git23import (4 "bytes"5 "fmt"6 "math"7 "strings"8 "sync"910 "github.com/aymanbagabas/git-module"11 "github.com/dustin/go-humanize/english"12 "github.com/sergi/go-diff/diffmatchpatch"13)1415// DiffSection is a wrapper to git.DiffSection with helper methods.16type DiffSection struct {17 *git.DiffSection1819 initOnce sync.Once20 dmp *diffmatchpatch.DiffMatchPatch21}2223// diffFor computes inline diff for the given line.24func (s *DiffSection) diffFor(line *git.DiffLine) string {25 fallback := line.Content2627 // Find equivalent diff line, ignore when not found.28 var diff1, diff2 string29 switch line.Type {30 case git.DiffLineAdd:31 compareLine := s.Line(git.DiffLineDelete, line.RightLine)32 if compareLine == nil {33 return fallback34 }3536 diff1 = compareLine.Content37 diff2 = line.Content3839 case git.DiffLineDelete:40 compareLine := s.Line(git.DiffLineAdd, line.LeftLine)41 if compareLine == nil {42 return fallback43 }4445 diff1 = line.Content46 diff2 = compareLine.Content4748 default:49 return fallback50 }5152 s.initOnce.Do(func() {53 s.dmp = diffmatchpatch.New()54 s.dmp.DiffEditCost = 10055 })5657 diffs := s.dmp.DiffMain(diff1[1:], diff2[1:], true)58 diffs = s.dmp.DiffCleanupEfficiency(diffs)5960 return diffsToString(diffs, line.Type)61}6263func diffsToString(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) string {64 buf := bytes.NewBuffer(nil)6566 // 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 }7374 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 }8485 return buf.String()86}8788// DiffFile is a wrapper to git.DiffFile with helper methods.89type DiffFile struct {90 *git.DiffFile91 Sections []*DiffSection92}9394// DiffFileChange represents a file diff.95type DiffFileChange struct {96 hash string97 name string98 mode git.EntryMode99}100101// Hash returns the diff file hash.102func (f *DiffFileChange) Hash() string {103 return f.hash104}105106// Name returns the diff name.107func (f *DiffFileChange) Name() string {108 return f.name109}110111// Mode returns the diff file mode.112func (f *DiffFileChange) Mode() git.EntryMode {113 return f.mode114}115116// Files returns the diff files.117func (f *DiffFile) Files() (from *DiffFileChange, to *DiffFileChange) {118 if f.OldIndex != ZeroID {119 from = &DiffFileChange{120 hash: f.OldIndex,121 name: f.OldName(),122 mode: f.OldMode(),123 }124 }125 if f.Index != ZeroID {126 to = &DiffFileChange{127 hash: f.Index,128 name: f.Name,129 mode: f.Mode(),130 }131 }132 return133}134135// FileStats136type FileStats []*DiffFile137138// String returns a string representation of file stats.139func (fs FileStats) String() string {140 return printStats(fs)141}142143func printStats(stats FileStats) string {144 padLength := float64(len(" "))145 newlineLength := float64(len("\n"))146 separatorLength := float64(len("|"))147 // Soft line length limit. The text length calculation below excludes148 // length of the change number. Adding that would take it closer to 80,149 // but probably not more than 80, until it's a huge number.150 lineLength := 72.0151152 // Get the longest filename and longest total change.153 var longestLength float64154 var longestTotalChange float64155 for _, fs := range stats {156 if int(longestLength) < len(fs.Name) {157 longestLength = float64(len(fs.Name))158 }159 totalChange := fs.NumAdditions() + fs.NumDeletions()160 if int(longestTotalChange) < totalChange {161 longestTotalChange = float64(totalChange)162 }163 }164165 // Parts of the output:166 // <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>167 // example: " main.go | 10 +++++++--- "168169 // <pad><filename><pad>170 leftTextLength := padLength + longestLength + padLength171172 // <pad><number><pad><+++++/-----><newline>173 // Excluding number length here.174 rightTextLength := padLength + padLength + newlineLength175176 totalTextArea := leftTextLength + separatorLength + rightTextLength177 heightOfHistogram := lineLength - totalTextArea178179 // Scale the histogram.180 var scaleFactor float64181 if longestTotalChange > heightOfHistogram {182 // Scale down to heightOfHistogram.183 scaleFactor = longestTotalChange / heightOfHistogram184 } else {185 scaleFactor = 1.0186 }187188 taddc := 0189 tdelc := 0190 output := strings.Builder{}191 for _, fs := range stats {192 taddc += fs.NumAdditions()193 tdelc += fs.NumDeletions()194 addn := float64(fs.NumAdditions())195 deln := float64(fs.NumDeletions())196 addc := int(math.Floor(addn / scaleFactor))197 delc := int(math.Floor(deln / scaleFactor))198 if addc < 0 {199 addc = 0200 }201 if delc < 0 {202 delc = 0203 }204 adds := strings.Repeat("+", addc)205 dels := strings.Repeat("-", delc)206 diffLines := fmt.Sprint(fs.NumAdditions() + fs.NumDeletions())207 totalDiffLines := fmt.Sprint(int(longestTotalChange))208 fmt.Fprintf(&output, "%s | %s %s%s\n",209 fs.Name+strings.Repeat(" ", int(longestLength)-len(fs.Name)),210 strings.Repeat(" ", len(totalDiffLines)-len(diffLines))+diffLines,211 adds,212 dels)213 }214 files := len(stats)215 fc := fmt.Sprintf("%s changed", english.Plural(files, "file", ""))216 ins := fmt.Sprintf("%s(+)", english.Plural(taddc, "insertion", ""))217 dels := fmt.Sprintf("%s(-)", english.Plural(tdelc, "deletion", ""))218 fmt.Fprint(&output, fc)219 if taddc > 0 {220 fmt.Fprintf(&output, ", %s", ins)221 }222 if tdelc > 0 {223 fmt.Fprintf(&output, ", %s", dels)224 }225 fmt.Fprint(&output, "\n")226227 return output.String()228}229230// Diff is a wrapper around git.Diff with helper methods.231type Diff struct {232 *git.Diff233 Files []*DiffFile234}235236// FileStats returns the diff file stats.237func (d *Diff) Stats() FileStats {238 return d.Files239}240241const (242 dstPrefix = "b/"243 srcPrefix = "a/"244)245246func appendPathLines(lines []string, fromPath, toPath string, isBinary bool) []string {247 if isBinary {248 return append(lines,249 fmt.Sprintf("Binary files %s and %s differ", fromPath, toPath),250 )251 }252 return append(lines,253 fmt.Sprintf("--- %s", fromPath),254 fmt.Sprintf("+++ %s", toPath),255 )256}257258func writeFilePatchHeader(sb *strings.Builder, filePatch *DiffFile) {259 from, to := filePatch.Files()260 if from == nil && to == nil {261 return262 }263 isBinary := filePatch.IsBinary()264265 var lines []string266 switch {267 case from != nil && to != nil:268 hashEquals := from.Hash() == to.Hash()269 lines = append(lines,270 fmt.Sprintf("diff --git %s%s %s%s",271 srcPrefix, from.Name(), dstPrefix, to.Name()),272 )273 if from.Mode() != to.Mode() {274 lines = append(lines,275 fmt.Sprintf("old mode %o", from.Mode()),276 fmt.Sprintf("new mode %o", to.Mode()),277 )278 }279 if from.Name() != to.Name() {280 lines = append(lines,281 fmt.Sprintf("rename from %s", from.Name()),282 fmt.Sprintf("rename to %s", to.Name()),283 )284 }285 if from.Mode() != to.Mode() && !hashEquals {286 lines = append(lines,287 fmt.Sprintf("index %s..%s", from.Hash(), to.Hash()),288 )289 } else if !hashEquals {290 lines = append(lines,291 fmt.Sprintf("index %s..%s %o", from.Hash(), to.Hash(), from.Mode()),292 )293 }294 if !hashEquals {295 lines = appendPathLines(lines, srcPrefix+from.Name(), dstPrefix+to.Name(), isBinary)296 }297 case from == nil:298 lines = append(lines,299 fmt.Sprintf("diff --git %s %s", srcPrefix+to.Name(), dstPrefix+to.Name()),300 fmt.Sprintf("new file mode %o", to.Mode()),301 fmt.Sprintf("index %s..%s", ZeroID, to.Hash()),302 )303 lines = appendPathLines(lines, "/dev/null", dstPrefix+to.Name(), isBinary)304 case to == nil:305 lines = append(lines,306 fmt.Sprintf("diff --git %s %s", srcPrefix+from.Name(), dstPrefix+from.Name()),307 fmt.Sprintf("deleted file mode %o", from.Mode()),308 fmt.Sprintf("index %s..%s", from.Hash(), ZeroID),309 )310 lines = appendPathLines(lines, srcPrefix+from.Name(), "/dev/null", isBinary)311 }312313 sb.WriteString(lines[0])314 for _, line := range lines[1:] {315 sb.WriteByte('\n')316 sb.WriteString(line)317 }318 sb.WriteByte('\n')319}320321// Patch returns the diff as a patch.322func (d *Diff) Patch() string {323 var p strings.Builder324 for _, f := range d.Files {325 writeFilePatchHeader(&p, f)326 for _, s := range f.Sections {327 for _, l := range s.Lines {328 p.WriteString(s.diffFor(l))329 p.WriteString("\n")330 }331 }332 }333 return p.String()334}335336func toDiff(ddiff *git.Diff) *Diff {337 files := make([]*DiffFile, 0, len(ddiff.Files))338 for _, df := range ddiff.Files {339 sections := make([]*DiffSection, 0, len(df.Sections))340 for _, ds := range df.Sections {341 sections = append(sections, &DiffSection{342 DiffSection: ds,343 })344 }345 files = append(files, &DiffFile{346 DiffFile: df,347 Sections: sections,348 })349 }350 diff := &Diff{351 Diff: ddiff,352 Files: files,353 }354 return diff355}