// Copyright 2019 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 ( "fmt" "log" "strings" ) // DefaultContextLines is the number of unchanged lines of surrounding // context displayed by Unified. Use ToUnified to specify a different value. const DefaultContextLines = 3 // Unified returns a unified diff of the old and new strings. // The old and new labels are the names of the old and new files. // If the strings are equal, it returns the empty string. func Unified(oldLabel, newLabel, old, new string) string { edits := Strings(old, new) unified, err := ToUnified(oldLabel, newLabel, old, edits, DefaultContextLines) if err != nil { // Can't happen: edits are consistent. log.Fatalf("internal error in diff.Unified: %v", err) } return unified } // ToUnified applies the edits to content and returns a unified diff, // with contextLines lines of (unchanged) context around each diff hunk. // The old and new labels are the names of the content and result files. // It returns an error if the edits are inconsistent; see ApplyEdits. func ToUnified(oldLabel, newLabel, content string, edits []Edit, contextLines int) (string, error) { u, err := toUnified(oldLabel, newLabel, content, edits, contextLines) if err != nil { return "", err } return u.String(), nil } // unified represents a set of edits as a unified diff. type unified struct { // from is the name of the original file. from string // to is the name of the modified file. to string // hunks is the set of edit hunks needed to transform the file content. hunks []*hunk } // Hunk represents a contiguous set of line edits to apply. type hunk struct { // The line in the original source where the hunk starts. fromLine int // The line in the original source where the hunk finishes. toLine int // The set of line based edits to apply. lines []line } // Line represents a single line operation to apply as part of a Hunk. type line struct { // kind is the type of line this represents, deletion, insertion or copy. kind opKind // content is the content of this line. // For deletion it is the line being removed, for all others it is the line // to put in the output. content string } // opKind is used to denote the type of operation a line represents. type opKind int const ( // opDelete is the operation kind for a line that is present in the input // but not in the output. opDelete opKind = iota // opInsert is the operation kind for a line that is new in the output. opInsert // opEqual is the operation kind for a line that is the same in the input and // output, often used to provide context around edited lines. opEqual ) // String returns a human readable representation of an OpKind. It is not // intended for machine processing. func (k opKind) String() string { switch k { case opDelete: return "delete" case opInsert: return "insert" case opEqual: return "equal" default: panic("unknown operation kind") } } // toUnified takes a file contents and a sequence of edits, and calculates // a unified diff that represents those edits. func toUnified(fromName, toName string, content string, edits []Edit, contextLines int) (unified, error) { gap := contextLines * 2 u := unified{ from: fromName, to: toName, } if len(edits) == 0 { return u, nil } var err error edits, err = lineEdits(content, edits) // expand to whole lines if err != nil { return u, err } lines := splitLines(content) var h *hunk last := 0 toLine := 0 for _, edit := range edits { // Compute the zero-based line numbers of the edit start and end. // TODO(adonovan): opt: compute incrementally, avoid O(n^2). start := strings.Count(content[:edit.Start], "\n") end := strings.Count(content[:edit.End], "\n") if edit.End == len(content) && len(content) > 0 && content[len(content)-1] != '\n' { end++ // EOF counts as an implicit newline } switch { case h != nil && start == last: //direct extension case h != nil && start <= last+gap: //within range of previous lines, add the joiners addEqualLines(h, lines, last, start) default: //need to start a new hunk if h != nil { // add the edge to the previous hunk addEqualLines(h, lines, last, last+contextLines) u.hunks = append(u.hunks, h) } toLine += start - last h = &hunk{ fromLine: start + 1, toLine: toLine + 1, } // add the edge to the new hunk delta := addEqualLines(h, lines, start-contextLines, start) h.fromLine -= delta h.toLine -= delta } last = start for i := start; i < end; i++ { h.lines = append(h.lines, line{kind: opDelete, content: lines[i]}) last++ } if edit.New != "" { for _, content := range splitLines(edit.New) { h.lines = append(h.lines, line{kind: opInsert, content: content}) toLine++ } } } if h != nil { // add the edge to the final hunk addEqualLines(h, lines, last, last+contextLines) u.hunks = append(u.hunks, h) } return u, nil } func splitLines(text string) []string { lines := strings.SplitAfter(text, "\n") if lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] } return lines } func addEqualLines(h *hunk, lines []string, start, end int) int { delta := 0 for i := start; i < end; i++ { if i < 0 { continue } if i >= len(lines) { return delta } h.lines = append(h.lines, line{kind: opEqual, content: lines[i]}) delta++ } return delta } // String converts a unified diff to the standard textual form for that diff. // The output of this function can be passed to tools like patch. func (u unified) String() string { if len(u.hunks) == 0 { return "" } b := new(strings.Builder) fmt.Fprintf(b, "--- %s\n", u.from) fmt.Fprintf(b, "+++ %s\n", u.to) for _, hunk := range u.hunks { fromCount, toCount := 0, 0 for _, l := range hunk.lines { switch l.kind { case opDelete: fromCount++ case opInsert: toCount++ default: fromCount++ toCount++ } } fmt.Fprint(b, "@@") if fromCount > 1 { fmt.Fprintf(b, " -%d,%d", hunk.fromLine, fromCount) } else if hunk.fromLine == 1 && fromCount == 0 { // Match odd GNU diff -u behavior adding to empty file. fmt.Fprintf(b, " -0,0") } else { fmt.Fprintf(b, " -%d", hunk.fromLine) } if toCount > 1 { fmt.Fprintf(b, " +%d,%d", hunk.toLine, toCount) } else if hunk.toLine == 1 && toCount == 0 { // Match odd GNU diff -u behavior adding to empty file. fmt.Fprintf(b, " +0,0") } else { fmt.Fprintf(b, " +%d", hunk.toLine) } fmt.Fprint(b, " @@\n") for _, l := range hunk.lines { switch l.kind { case opDelete: fmt.Fprintf(b, "-%s", l.content) case opInsert: fmt.Fprintf(b, "+%s", l.content) default: fmt.Fprintf(b, " %s", l.content) } if !strings.HasSuffix(l.content, "\n") { fmt.Fprintf(b, "\n\\ No newline at end of file\n") } } } return b.String() }