Source File
diff.go
Belonging Package
internal/diff
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package diff
import (
)
// A pair is a pair of values tracked for both the x and y side of a diff.
// It is typically a pair of line indexes.
type pair struct{ x, y int }
// Diff returns an anchored diff of the two texts old and new
// in the “unified diff” format. If old and new are identical,
// Diff returns a nil slice (no output).
//
// Unix diff implementations typically look for a diff with
// the smallest number of lines inserted and removed,
// which can in the worst case take time quadratic in the
// number of lines in the texts. As a result, many implementations
// either can be made to run for a long time or cut off the search
// after a predetermined amount of work.
//
// In contrast, this implementation looks for a diff with the
// smallest number of “unique” lines inserted and removed,
// where unique means a line that appears just once in both old and new.
// We call this an “anchored diff” because the unique lines anchor
// the chosen matching regions. An anchored diff is usually clearer
// than a standard diff, because the algorithm does not try to
// reuse unrelated blank lines or closing braces.
// The algorithm also guarantees to run in O(n log n) time
// instead of the standard O(n²) time.
//
// Some systems call this approach a “patience diff,” named for
// the “patience sorting” algorithm, itself named for a solitaire card game.
// We avoid that name for two reasons. First, the name has been used
// for a few different variants of the algorithm, so it is imprecise.
// Second, the name is frequently interpreted as meaning that you have
// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm,
// when in fact the algorithm is faster than the standard one.
func ( string, []byte, string, []byte) []byte {
if bytes.Equal(, ) {
return nil
}
:= lines()
:= lines()
// Print diff header.
var bytes.Buffer
fmt.Fprintf(&, "diff %s %s\n", , )
fmt.Fprintf(&, "--- %s\n", )
fmt.Fprintf(&, "+++ %s\n", )
// Loop over matches to consider,
// expanding each match to include surrounding lines,
// and then printing diff chunks.
// To avoid setup/teardown cases outside the loop,
// tgs returns a leading {0,0} and trailing {len(x), len(y)} pair
// in the sequence of matches.
var (
pair // printed up to x[:done.x] and y[:done.y]
pair // start lines of current chunk
pair // number of lines from each side in current chunk
[]string // lines for current chunk
)
for , := range tgs(, ) {
if .x < .x {
// Already handled scanning forward from earlier match.
continue
}
// Expand matching lines as far as possible,
// establishing that x[start.x:end.x] == y[start.y:end.y].
// Note that on the first (or last) iteration we may (or definitely do)
// have an empty match: start.x==end.x and start.y==end.y.
:=
for .x > .x && .y > .y && [.x-1] == [.y-1] {
.x--
.y--
}
:=
for .x < len() && .y < len() && [.x] == [.y] {
.x++
.y++
}
// Emit the mismatched lines before start into this chunk.
// (No effect on first sentinel iteration, when start = {0,0}.)
for , := range [.x:.x] {
= append(, "-"+)
.x++
}
for , := range [.y:.y] {
= append(, "+"+)
.y++
}
// If we're not at EOF and have too few common lines,
// the chunk includes all the common lines and continues.
const = 3 // number of context lines
if (.x < len() || .y < len()) &&
(.x-.x < || (len() > 0 && .x-.x < 2*)) {
for , := range [.x:.x] {
= append(, " "+)
.x++
.y++
}
=
continue
}
// End chunk with common lines for context.
if len() > 0 {
:= .x - .x
if > {
=
}
for , := range [.x : .x+] {
= append(, " "+)
.x++
.y++
}
= pair{.x + , .y + }
// Format and emit chunk.
// Convert line numbers to 1-indexed.
// Special case: empty file shows up as 0,0 not 1,0.
if .x > 0 {
.x++
}
if .y > 0 {
.y++
}
fmt.Fprintf(&, "@@ -%d,%d +%d,%d @@\n", .x, .x, .y, .y)
for , := range {
.WriteString()
}
.x = 0
.y = 0
= [:0]
}
// If we reached EOF, we're done.
if .x >= len() && .y >= len() {
break
}
// Otherwise start a new chunk.
= pair{.x - , .y - }
for , := range [.x:.x] {
= append(, " "+)
.x++
.y++
}
=
}
return .Bytes()
}
// lines returns the lines in the file x, including newlines.
// If the file does not end in a newline, one is supplied
// along with a warning about the missing newline.
func lines( []byte) []string {
:= strings.SplitAfter(string(), "\n")
if [len()-1] == "" {
= [:len()-1]
} else {
// Treat last line as having a message about the missing newline attached,
// using the same text as BSD/GNU diff (including the leading backslash).
[len()-1] += "\n\\ No newline at end of file\n"
}
return
}
// tgs returns the pairs of indexes of the longest common subsequence
// of unique lines in x and y, where a unique line is one that appears
// once in x and once in y.
//
// The longest common subsequence algorithm is as described in
// Thomas G. Szymanski, “A Special Case of the Maximal Common
// Subsequence Problem,” Princeton TR #170 (January 1975),
// available at https://research.swtch.com/tgs170.pdf.
func tgs(, []string) []pair {
// Count the number of times each string appears in a and b.
// We only care about 0, 1, many, counted as 0, -1, -2
// for the x side and 0, -4, -8 for the y side.
// Using negative numbers now lets us distinguish positive line numbers later.
:= make(map[string]int)
for , := range {
if := []; > -2 {
[] = - 1
}
}
for , := range {
if := []; > -8 {
[] = - 4
}
}
// Now unique strings can be identified by m[s] = -1+-4.
//
// Gather the indexes of those strings in x and y, building:
// xi[i] = increasing indexes of unique strings in x.
// yi[i] = increasing indexes of unique strings in y.
// inv[i] = index j such that x[xi[i]] = y[yi[j]].
var , , []int
for , := range {
if [] == -1+-4 {
[] = len()
= append(, )
}
}
for , := range {
if , := []; && >= 0 {
= append(, )
= append(, )
}
}
// Apply Algorithm A from Szymanski's paper.
// In those terms, A = J = inv and B = [0, n).
// We add sentinel pairs {0,0}, and {len(x),len(y)}
// to the returned sequence, to help the processing loop.
:=
:= len()
:= make([]int, )
:= make([]int, )
for := range {
[] = + 1
}
for := 0; < ; ++ {
:= sort.Search(, func( int) bool {
return [] >= []
})
[] = []
[] = + 1
}
:= 0
for , := range {
if < {
=
}
}
:= make([]pair, 2+)
[1+] = pair{len(), len()} // sentinel at end
:=
for := - 1; >= 0; -- {
if [] == && [] < {
[] = pair{[], [[]]}
--
}
}
[0] = pair{0, 0} // sentinel at start
return
}
The pages are generated with Golds v0.7.0-preview. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |