Skip to content

Commit 6f90939

Browse files
committed
difflib: implement context diffs
1 parent 35c8dc4 commit 6f90939

File tree

3 files changed

+224
-1
lines changed

3 files changed

+224
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ The following class and functions have be ported:
55

66
* `SequenceMatcher`
77
* `unified_diff()`
8+
* `context_diff()`
89

9-
Related doctests have been ported as well.
10+
Related doctests and unittests have been ported as well.
1011

1112
I have barely used to code yet so do not consider it being production-ready.
1213
The API is likely to evolve too.

difflib/difflib.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,3 +626,120 @@ func GetUnifiedDiffString(diff UnifiedDiff) (string, error) {
626626
err := WriteUnifiedDiff(w, diff)
627627
return string(w.Bytes()), err
628628
}
629+
630+
// Convert range to the "ed" format.
631+
func formatRangeContext(start, stop int) string {
632+
// Per the diff spec at http://www.unix.org/single_unix_specification/
633+
beginning := start + 1 // lines start numbering with one
634+
length := stop - start
635+
if length == 0 {
636+
beginning -= 1 // empty ranges begin at line just before the range
637+
}
638+
if length <= 1 {
639+
return fmt.Sprintf("%d", beginning)
640+
}
641+
return fmt.Sprintf("%d,%d", beginning, beginning+length-1)
642+
}
643+
644+
type ContextDiff UnifiedDiff
645+
646+
// Compare two sequences of lines; generate the delta as a context diff.
647+
//
648+
// Context diffs are a compact way of showing line changes and a few
649+
// lines of context. The number of context lines is set by diff.Context
650+
// which defaults to three.
651+
//
652+
// By default, the diff control lines (those with *** or ---) are
653+
// created with a trailing newline.
654+
//
655+
// For inputs that do not have trailing newlines, set the diff.Eol
656+
// argument to "" so that the output will be uniformly newline free.
657+
//
658+
// The context diff format normally has a header for filenames and
659+
// modification times. Any or all of these may be specified using
660+
// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate.
661+
// The modification times are normally expressed in the ISO 8601 format.
662+
// If not specified, the strings default to blanks.
663+
func WriteContextDiff(writer io.Writer, diff ContextDiff) error {
664+
buf := bufio.NewWriter(writer)
665+
defer buf.Flush()
666+
var diffErr error
667+
w := func(format string, args ...interface{}) {
668+
_, err := buf.WriteString(fmt.Sprintf(format, args...))
669+
if diffErr == nil && err != nil {
670+
diffErr = err
671+
}
672+
}
673+
674+
if len(diff.Eol) == 0 {
675+
diff.Eol = "\n"
676+
}
677+
678+
prefix := map[byte]string{
679+
'i': "+ ",
680+
'd': "- ",
681+
'r': "! ",
682+
'e': " ",
683+
}
684+
685+
started := false
686+
m := NewMatcher(diff.A, diff.B)
687+
for _, g := range m.GetGroupedOpCodes(diff.Context) {
688+
if !started {
689+
started = true
690+
fromDate := ""
691+
if len(diff.FromDate) > 0 {
692+
fromDate = "\t" + diff.FromDate
693+
}
694+
toDate := ""
695+
if len(diff.ToDate) > 0 {
696+
toDate = "\t" + diff.ToDate
697+
}
698+
w("*** %s%s%s", diff.FromFile, fromDate, diff.Eol)
699+
w("--- %s%s%s", diff.ToFile, toDate, diff.Eol)
700+
}
701+
702+
first, last := g[0], g[len(g)-1]
703+
w("***************" + diff.Eol)
704+
705+
range1 := formatRangeContext(first.I1, last.I2)
706+
w("*** %s ****%s", range1, diff.Eol)
707+
for _, c := range g {
708+
if c.Tag == 'r' || c.Tag == 'd' {
709+
for _, cc := range g {
710+
if cc.Tag == 'i' {
711+
continue
712+
}
713+
for _, line := range diff.A[cc.I1:cc.I2] {
714+
w(prefix[cc.Tag] + line)
715+
}
716+
}
717+
break
718+
}
719+
}
720+
721+
range2 := formatRangeContext(first.J1, last.J2)
722+
w("--- %s ----%s", range2, diff.Eol)
723+
for _, c := range g {
724+
if c.Tag == 'r' || c.Tag == 'i' {
725+
for _, cc := range g {
726+
if cc.Tag == 'd' {
727+
continue
728+
}
729+
for _, line := range diff.B[cc.J1:cc.J2] {
730+
w(prefix[cc.Tag] + line)
731+
}
732+
}
733+
break
734+
}
735+
}
736+
}
737+
return diffErr
738+
}
739+
740+
// Like WriteContextDiff but returns the diff a string.
741+
func GetContextDiffString(diff ContextDiff) (string, error) {
742+
w := &bytes.Buffer{}
743+
err := WriteContextDiff(w, diff)
744+
return string(w.Bytes()), err
745+
}

difflib/difflib_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,46 @@ four`
148148
}
149149
}
150150

151+
func TestContextDiff(t *testing.T) {
152+
a := `one
153+
two
154+
three
155+
four`
156+
b := `zero
157+
one
158+
tree
159+
four`
160+
diff := ContextDiff{
161+
A: splitLines(a),
162+
B: splitLines(b),
163+
FromFile: "Original",
164+
ToFile: "Current",
165+
Context: 3,
166+
Eol: "\n",
167+
}
168+
result, err := GetContextDiffString(diff)
169+
assertEqual(t, err, nil)
170+
expected := `*** Original
171+
--- Current
172+
***************
173+
*** 1,4 ****
174+
one
175+
! two
176+
! three
177+
four
178+
--- 1,4 ----
179+
+ zero
180+
one
181+
! tree
182+
four
183+
`
184+
// TABs are a pain to preserve through editors
185+
expected = strings.Replace(expected, "\\t", "\t", -1)
186+
if expected != result {
187+
t.Errorf("unexpected diff result:\n%s", result)
188+
}
189+
}
190+
151191
func rep(s string, count int) string {
152192
return strings.Repeat(s, count)
153193
}
@@ -232,3 +272,68 @@ func TestOutputFormatRangeFormatUnified(t *testing.T) {
232272
assertEqual(t, fm(3, 6), "4,3")
233273
assertEqual(t, fm(0, 0), "0,0")
234274
}
275+
276+
func TestOutputFormatRangeFormatContext(t *testing.T) {
277+
// Per the diff spec at http://www.unix.org/single_unix_specification/
278+
//
279+
// The range of lines in file1 shall be written in the following format
280+
// if the range contains two or more lines:
281+
// "*** %d,%d ****\n", <beginning line number>, <ending line number>
282+
// and the following format otherwise:
283+
// "*** %d ****\n", <ending line number>
284+
// The ending line number of an empty range shall be the number of the preceding line,
285+
// or 0 if the range is at the start of the file.
286+
//
287+
// Next, the range of lines in file2 shall be written in the following format
288+
// if the range contains two or more lines:
289+
// "--- %d,%d ----\n", <beginning line number>, <ending line number>
290+
// and the following format otherwise:
291+
// "--- %d ----\n", <ending line number>
292+
fm := formatRangeContext
293+
assertEqual(t, fm(3, 3), "3")
294+
assertEqual(t, fm(3, 4), "4")
295+
assertEqual(t, fm(3, 5), "4,5")
296+
assertEqual(t, fm(3, 6), "4,6")
297+
assertEqual(t, fm(0, 0), "0")
298+
}
299+
300+
func TestOutputFormatTabDelimiter(t *testing.T) {
301+
diff := UnifiedDiff{
302+
A: splitChars("one"),
303+
B: splitChars("two"),
304+
FromFile: "Original",
305+
FromDate: "2005-01-26 23:30:50",
306+
ToFile: "Current",
307+
ToDate: "2010-04-12 10:20:52",
308+
Eol: "\n",
309+
}
310+
ud, err := GetUnifiedDiffString(diff)
311+
assertEqual(t, err, nil)
312+
assertEqual(t, splitLines(ud)[:2], []string{
313+
"--- Original\t2005-01-26 23:30:50\n",
314+
"+++ Current\t2010-04-12 10:20:52\n",
315+
})
316+
cd, err := GetContextDiffString(ContextDiff(diff))
317+
assertEqual(t, err, nil)
318+
assertEqual(t, splitLines(cd)[:2], []string{
319+
"*** Original\t2005-01-26 23:30:50\n",
320+
"--- Current\t2010-04-12 10:20:52\n",
321+
})
322+
}
323+
324+
func TestOutputFormatNoTrailingTabOnEmptyFiledate(t *testing.T) {
325+
diff := UnifiedDiff{
326+
A: splitChars("one"),
327+
B: splitChars("two"),
328+
FromFile: "Original",
329+
ToFile: "Current",
330+
Eol: "\n",
331+
}
332+
ud, err := GetUnifiedDiffString(diff)
333+
assertEqual(t, err, nil)
334+
assertEqual(t, splitLines(ud)[:2], []string{"--- Original\n", "+++ Current\n"})
335+
336+
cd, err := GetContextDiffString(ContextDiff(diff))
337+
assertEqual(t, err, nil)
338+
assertEqual(t, splitLines(cd)[:2], []string{"*** Original\n", "--- Current\n"})
339+
}

0 commit comments

Comments
 (0)