123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 | // Package diff compares texts line by line, renders unified diffs, and
// applies them.
package diff
import (
"fmt"
"strings"
)
// Kind says what a line of a diff does.
type Kind byte
const (
Same Kind = ' '
Del Kind = '-'
Add Kind = '+'
)
// Line is one line of a diff, without its newline.
type Line struct {
Kind Kind
Text string
}
// SplitLines splits text into lines without their newlines. A final newline
// does not start another line.
func SplitLines(s string) []string {
if s == "" {
return nil
}
return strings.Split(strings.TrimSuffix(s, "\n"), "\n")
}
// maxCells bounds the work of the line-matching table; bigger changes are
// shown as replacing everything between the common start and end.
const maxCells = 8_000_000
// Lines diffs a against b.
func Lines(a, b []string) []Line {
var out []Line
// The common start and end need no table.
pre := 0
for pre < len(a) && pre < len(b) && a[pre] == b[pre] {
pre++
}
suf := 0
for suf < len(a)-pre && suf < len(b)-pre && a[len(a)-1-suf] == b[len(b)-1-suf] {
suf++
}
for _, l := range a[:pre] {
out = append(out, Line{Same, l})
}
ma, mb := a[pre:len(a)-suf], b[pre:len(b)-suf]
n, m := len(ma), len(mb)
if n*m > maxCells {
for _, l := range ma {
out = append(out, Line{Del, l})
}
for _, l := range mb {
out = append(out, Line{Add, l})
}
} else {
// lcs[i][j] is the longest common subsequence of ma[i:] and mb[j:].
lcs := make([][]int32, n+1)
for i := range lcs {
lcs[i] = make([]int32, m+1)
}
for i := n - 1; i >= 0; i-- {
for j := m - 1; j >= 0; j-- {
if ma[i] == mb[j] {
lcs[i][j] = lcs[i+1][j+1] + 1
} else {
lcs[i][j] = max(lcs[i+1][j], lcs[i][j+1])
}
}
}
i, j := 0, 0
for i < n || j < m {
switch {
case i < n && j < m && ma[i] == mb[j]:
out = append(out, Line{Same, ma[i]})
i, j = i+1, j+1
case i < n && (j == m || lcs[i+1][j] >= lcs[i][j+1]):
out = append(out, Line{Del, ma[i]})
i++
default:
out = append(out, Line{Add, mb[j]})
j++
}
}
}
for _, l := range a[len(a)-suf:] {
out = append(out, Line{Same, l})
}
return out
}
// Hunk is a run of changes with context around it.
type Hunk struct {
OldStart, NewStart int // 1-based
Lines []Line
}
// Hunks groups a diff into hunks with up to context unchanged lines around
// each change; changes closer than that share a hunk.
func Hunks(lines []Line, context int) []Hunk {
// Mark the lines within context of a change, then cut runs of marked
// lines into hunks.
keep := make([]bool, len(lines))
for i, l := range lines {
if l.Kind == Same {
continue
}
for k := max(i-context, 0); k <= min(i+context, len(lines)-1); k++ {
keep[k] = true
}
}
var hunks []Hunk
oldLine, newLine := 1, 1
var cur *Hunk
for i, l := range lines {
if keep[i] {
if cur == nil {
cur = &Hunk{OldStart: oldLine, NewStart: newLine}
}
cur.Lines = append(cur.Lines, l)
} else if cur != nil {
hunks = append(hunks, *cur)
cur = nil
}
switch l.Kind {
case Same:
oldLine++
newLine++
case Del:
oldLine++
case Add:
newLine++
}
}
if cur != nil {
hunks = append(hunks, *cur)
}
return hunks
}
// Count returns the lines a hunk covers in the old and new text.
func (h Hunk) Count() (old, new int) {
for _, l := range h.Lines {
if l.Kind != Add {
old++
}
if l.Kind != Del {
new++
}
}
return old, new
}
// splitKeep splits text into lines, each keeping its newline, so a last
// line without one differs from the same line with one.
func splitKeep(s string) []string {
lines := strings.SplitAfter(s, "\n")
if lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
return lines
}
// noNewline marks a line that ends its text without a newline.
const noNewline = "\\ No newline at end of file"
// Unified renders the diff between two texts in unified format, with
// headers naming path; "" if they are the same. A change to the final
// newline shows, as in diff(1).
func Unified(path, a, b string, context int) string {
hunks := Hunks(Lines(splitKeep(a), splitKeep(b)), context)
if len(hunks) == 0 {
return ""
}
var sb strings.Builder
fmt.Fprintf(&sb, "--- %s\n+++ %s\n", path, path)
for _, h := range hunks {
o, n := h.Count()
fmt.Fprintf(&sb, "@@ -%d,%d +%d,%d @@\n", h.OldStart, o, h.NewStart, n)
for _, l := range h.Lines {
sb.WriteByte(byte(l.Kind))
text, nl := strings.CutSuffix(l.Text, "\n")
sb.WriteString(text)
sb.WriteByte('\n')
if !nl {
sb.WriteString(noNewline + "\n")
}
}
}
return sb.String()
}
// Stat counts the lines added and removed between two texts.
func Stat(a, b string) (added, removed int) {
for _, l := range Lines(splitKeep(a), splitKeep(b)) {
switch l.Kind {
case Add:
added++
case Del:
removed++
}
}
return added, removed
}
|