Something went wrong. Try again.
forked from charmbracelet/soft-serve
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356package git
import ( "bytes" "fmt" "math" "strings" "sync"
"github.com/aymanbagabas/git-module" "github.com/dustin/go-humanize/english" "github.com/sergi/go-diff/diffmatchpatch")
// DiffSection is a wrapper to git.DiffSection with helper methods.type DiffSection struct { *git.DiffSection
initOnce sync.Once dmp *diffmatchpatch.DiffMatchPatch}
// diffFor computes inline diff for the given line.func (s *DiffSection) diffFor(line *git.DiffLine) string { fallback := line.Content
// Find equivalent diff line, ignore when not found. var diff1, diff2 string switch line.Type { case git.DiffLineAdd: compareLine := s.Line(git.DiffLineDelete, line.RightLine) if compareLine == nil { return fallback }
diff1 = compareLine.Content diff2 = line.Content
case git.DiffLineDelete: compareLine := s.Line(git.DiffLineAdd, line.LeftLine) if compareLine == nil { return fallback }
diff1 = line.Content diff2 = compareLine.Content
default: return fallback }
s.initOnce.Do(func() { s.dmp = diffmatchpatch.New() s.dmp.DiffEditCost = 100 })
diffs := s.dmp.DiffMain(diff1[1:], diff2[1:], true) diffs = s.dmp.DiffCleanupEfficiency(diffs)
return diffsToString(diffs, line.Type)}
func diffsToString(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) string { buf := bytes.NewBuffer(nil)
// Reproduce signs that were cut off for inline diff before. switch lineType { case git.DiffLineAdd: buf.WriteByte('+') case git.DiffLineDelete: buf.WriteByte('-') }
for i := range diffs { switch { case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DiffLineAdd: buf.WriteString(diffs[i].Text) case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DiffLineDelete: buf.WriteString(diffs[i].Text) case diffs[i].Type == diffmatchpatch.DiffEqual: buf.WriteString(diffs[i].Text) } }
return buf.String()}
// DiffFile is a wrapper to git.DiffFile with helper methods.type DiffFile struct { *git.DiffFile Sections []*DiffSection}
// DiffFileChange represents a file diff.type DiffFileChange struct { hash string name string mode git.EntryMode}
// Hash returns the diff file hash.func (f *DiffFileChange) Hash() string { return f.hash}
// Name returns the diff name.func (f *DiffFileChange) Name() string { return f.name}
// Mode returns the diff file mode.func (f *DiffFileChange) Mode() git.EntryMode { return f.mode}
// Files returns the diff files.func (f *DiffFile) Files() (from *DiffFileChange, to *DiffFileChange) { if f.OldIndex != ZeroID { from = &DiffFileChange{ hash: f.OldIndex, name: f.OldName(), mode: f.OldMode(), } } if f.Index != ZeroID { to = &DiffFileChange{ hash: f.Index, name: f.Name, mode: f.Mode(), } } return}
// FileStatstype FileStats []*DiffFile
// String returns a string representation of file stats.func (fs FileStats) String() string { return printStats(fs)}
func printStats(stats FileStats) string { padLength := float64(len(" ")) newlineLength := float64(len("\n")) separatorLength := float64(len("|")) // Soft line length limit. The text length calculation below excludes // length of the change number. Adding that would take it closer to 80, // but probably not more than 80, until it's a huge number. lineLength := 72.0
// Get the longest filename and longest total change. var longestLength float64 var longestTotalChange float64 for _, fs := range stats { if int(longestLength) < len(fs.Name) { longestLength = float64(len(fs.Name)) } totalChange := fs.NumAdditions() + fs.NumDeletions() if int(longestTotalChange) < totalChange { longestTotalChange = float64(totalChange) } }
// Parts of the output: // <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline> // example: " main.go | 10 +++++++--- "
// <pad><filename><pad> leftTextLength := padLength + longestLength + padLength
// <pad><number><pad><+++++/-----><newline> // Excluding number length here. rightTextLength := padLength + padLength + newlineLength
totalTextArea := leftTextLength + separatorLength + rightTextLength heightOfHistogram := lineLength - totalTextArea
// Scale the histogram. var scaleFactor float64 if longestTotalChange > heightOfHistogram { // Scale down to heightOfHistogram. scaleFactor = longestTotalChange / heightOfHistogram } else { scaleFactor = 1.0 }
taddc := 0 tdelc := 0 output := strings.Builder{} for _, fs := range stats { taddc += fs.NumAdditions() tdelc += fs.NumDeletions() addn := float64(fs.NumAdditions()) deln := float64(fs.NumDeletions()) addc := int(math.Floor(addn / scaleFactor)) delc := int(math.Floor(deln / scaleFactor)) if addc < 0 { addc = 0 } if delc < 0 { delc = 0 } adds := strings.Repeat("+", addc) dels := strings.Repeat("-", delc) diffLines := fmt.Sprint(fs.NumAdditions() + fs.NumDeletions()) totalDiffLines := fmt.Sprint(int(longestTotalChange)) fmt.Fprintf(&output, "%s | %s %s%s\n", fs.Name+strings.Repeat(" ", int(longestLength)-len(fs.Name)), strings.Repeat(" ", len(totalDiffLines)-len(diffLines))+diffLines, adds, dels) } files := len(stats) fc := fmt.Sprintf("%s changed", english.Plural(files, "file", "")) ins := fmt.Sprintf("%s(+)", english.Plural(taddc, "insertion", "")) dels := fmt.Sprintf("%s(-)", english.Plural(tdelc, "deletion", "")) fmt.Fprint(&output, fc) if taddc > 0 { fmt.Fprintf(&output, ", %s", ins) } if tdelc > 0 { fmt.Fprintf(&output, ", %s", dels) } fmt.Fprint(&output, "\n")
return output.String()}
// Diff is a wrapper around git.Diff with helper methods.type Diff struct { *git.Diff Files []*DiffFile}
// FileStats returns the diff file stats.func (d *Diff) Stats() FileStats { return d.Files}
const ( dstPrefix = "b/" srcPrefix = "a/")
func appendPathLines(lines []string, fromPath, toPath string, isBinary bool) []string { if isBinary { return append(lines, fmt.Sprintf("Binary files %s and %s differ", fromPath, toPath), ) } return append(lines, fmt.Sprintf("--- %s", fromPath), fmt.Sprintf("+++ %s", toPath), )}
func writeFilePatchHeader(sb *strings.Builder, filePatch *DiffFile) { from, to := filePatch.Files() if from == nil && to == nil { return } isBinary := filePatch.IsBinary()
var lines []string switch { case from != nil && to != nil: hashEquals := from.Hash() == to.Hash() lines = append(lines, fmt.Sprintf("diff --git %s%s %s%s", srcPrefix, from.Name(), dstPrefix, to.Name()), ) if from.Mode() != to.Mode() { lines = append(lines, fmt.Sprintf("old mode %o", from.Mode()), fmt.Sprintf("new mode %o", to.Mode()), ) } if from.Name() != to.Name() { lines = append(lines, fmt.Sprintf("rename from %s", from.Name()), fmt.Sprintf("rename to %s", to.Name()), ) } if from.Mode() != to.Mode() && !hashEquals { lines = append(lines, fmt.Sprintf("index %s..%s", from.Hash(), to.Hash()), ) } else if !hashEquals { lines = append(lines, fmt.Sprintf("index %s..%s %o", from.Hash(), to.Hash(), from.Mode()), ) } if !hashEquals { lines = appendPathLines(lines, srcPrefix+from.Name(), dstPrefix+to.Name(), isBinary) } case from == nil: lines = append(lines, fmt.Sprintf("diff --git %s %s", srcPrefix+to.Name(), dstPrefix+to.Name()), fmt.Sprintf("new file mode %o", to.Mode()), fmt.Sprintf("index %s..%s", ZeroID, to.Hash()), ) lines = appendPathLines(lines, "/dev/null", dstPrefix+to.Name(), isBinary) case to == nil: lines = append(lines, fmt.Sprintf("diff --git %s %s", srcPrefix+from.Name(), dstPrefix+from.Name()), fmt.Sprintf("deleted file mode %o", from.Mode()), fmt.Sprintf("index %s..%s", from.Hash(), ZeroID), ) lines = appendPathLines(lines, srcPrefix+from.Name(), "/dev/null", isBinary) }
sb.WriteString(lines[0]) for _, line := range lines[1:] { sb.WriteByte('\n') sb.WriteString(line) } sb.WriteByte('\n')}
// Patch returns the diff as a patch.func (d *Diff) Patch() string { var p strings.Builder for _, f := range d.Files { writeFilePatchHeader(&p, f) for _, s := range f.Sections { for _, l := range s.Lines { p.WriteString(s.diffFor(l)) p.WriteString("\n") } } } return p.String()}
func toDiff(ddiff *git.Diff) *Diff { files := make([]*DiffFile, 0, len(ddiff.Files)) for _, df := range ddiff.Files { sections := make([]*DiffSection, 0, len(df.Sections)) for _, ds := range df.Sections { sections = append(sections, &DiffSection{ DiffSection: ds, }) } files = append(files, &DiffFile{ DiffFile: df, Sections: sections, }) } diff := &Diff{ Diff: ddiff, Files: files, } return diff}