···2233A [birdie](https://github.com/giacomocavalieri/birdie) and [insta](https://github.com/mitsuhiko/insta) inspired snapshot testing library for Go.
4455-
55+
66+77+")
6879## Installation
810···3032go run github.com/ptdewey/freeze/cmd/freeze review
3133```
32343535+<!-- TODO: add example of `freeze.Review()` in go code -->
3636+3737+Freeze also includes (in a separate Go module) with a [Bubbletea](https://github.com/charmbracelet/bubbletea) TUI in [cmd/tui/main.go](./cmd/tui/main.go). (The TUI is shipped in a separate module to make the added dependencies optional)
3838+3939+```sh
4040+# TODO: tui usage
4141+```
4242+3343## Disclaimer
34443545- This package was largely vibe coded, your mileage may vary (but this library provides more of what I want than the ones below).
···3747## Other Libraries
38483949- [go-snaps](https://github.com/gkampitakis/go-snaps)
5050+ - Freeze uses the diff implementation from `go-snaps`.
4051- [cupaloy](https://github.com/bradleyjkemp/cupaloy)
···11package diff
2233-import (
44- "strings"
55-)
33+/*
44+This file was sourced from github.com/gkampitakis/go-snaps, available with the following License
55+66+MIT License
77+88+Copyright (c) 2021 Georgios Kampitakis
99+1010+Permission is hereby granted, free of charge, to any person obtaining a copy
1111+of this software and associated documentation files (the "Software"), to deal
1212+in the Software without restriction, including without limitation the rights
1313+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1414+copies of the Software, and to permit persons to whom the Software is
1515+furnished to do so, subject to the following conditions:
1616+1717+The above copyright notice and this permission notice shall be included in all
1818+copies or substantial portions of the Software.
1919+2020+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2121+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2222+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2323+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2424+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2525+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2626+SOFTWARE.
2727+2828+=======================
2929+3030+This package is a partial port of Python difflib.
3131+3232+This library is vendored and modified to address the `go-snaps` needs for a more
3333+readable difference report.
3434+3535+3636+Original source: https://github.com/pmezard/go-difflib
3737+3838+Copyright (c) 2013, Patrick Mezard
3939+All rights reserved.
4040+4141+Redistribution and use in source and binary forms, with or without
4242+modification, are permitted provided that the following conditions are
4343+met:
4444+4545+ Redistributions of source code must retain the above copyright
4646+notice, this list of conditions and the following disclaimer.
4747+ Redistributions in binary form must reproduce the above copyright
4848+notice, this list of conditions and the following disclaimer in the
4949+documentation and/or other materials provided with the distribution.
5050+ The names of its contributors may not be used to endorse or promote
5151+products derived from this software without specific prior written
5252+permission.
5353+5454+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
5555+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
5656+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
5757+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
5858+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
5959+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
6060+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
6161+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
6262+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
6363+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
6464+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6565+*/
6666+6767+import "strconv"
668769type DiffKind int
870···1981 Kind DiffKind
2082}
21832222-func Histogram(old, new string) []DiffLine {
2323- oldLines := strings.Split(old, "\n")
2424- newLines := strings.Split(new, "\n")
8484+const (
8585+ // Tag Codes for internal opcodes
8686+ opEqual int8 = iota
8787+ opInsert
8888+ opDelete
8989+ opReplace
9090+)
9191+9292+type match struct {
9393+ A int
9494+ B int
9595+ Size int
9696+}
9797+9898+type opCode struct {
9999+ Tag int8
100100+ I1 int
101101+ I2 int
102102+ J1 int
103103+ J2 int
104104+}
105105+106106+func min(a, b int) int {
107107+ if a < b {
108108+ return a
109109+ }
110110+ return b
111111+}
112112+113113+func max(a, b int) int {
114114+ if a > b {
115115+ return a
116116+ }
117117+ return b
118118+}
119119+120120+// sequenceMatcher compares sequence of strings. The basic
121121+// algorithm predates, and is a little fancier than, an algorithm
122122+// published in the late 1980's by Ratcliff and Obershelp under the
123123+// hyperbolic name "gestalt pattern matching". The basic idea is to find
124124+// the longest contiguous matching subsequence that contains no "junk"
125125+// elements (R-O doesn't address junk). The same idea is then applied
126126+// recursively to the pieces of the sequences to the left and to the right
127127+// of the matching subsequence. This does not yield minimal edit
128128+// sequences, but does tend to yield matches that "look right" to people.
129129+//
130130+// sequenceMatcher tries to compute a "human-friendly diff" between two
131131+// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the
132132+// longest *contiguous* & junk-free matching subsequence. That's what
133133+// catches peoples' eyes. The Windows(tm) windiff has another interesting
134134+// notion, pairing up elements that appear uniquely in each sequence.
135135+// That, and the method here, appear to yield more intuitive difference
136136+// reports than does diff. This method appears to be the least vulnerable
137137+// to synching up on blocks of "junk lines", though (like blank lines in
138138+// ordinary text files, or maybe "<P>" lines in HTML files). That may be
139139+// because this is the only method of the 3 that has a *concept* of
140140+// "junk" <wink>.
141141+//
142142+// Timing: Basic R-O is cubic time worst case and quadratic time expected
143143+// case. sequenceMatcher is quadratic time for the worst case and has
144144+// expected-case behavior dependent in a complicated way on how many
145145+// elements the sequences have in common; best case time is linear.
146146+type sequenceMatcher struct {
147147+ a []string
148148+ b []string
149149+ b2j map[string][]int
150150+ IsJunk func(string) bool
151151+ autoJunk bool
152152+ bJunk map[string]struct{}
153153+ matchingBlocks []match
154154+ fullBCount map[string]int
155155+ bPopular map[string]struct{}
156156+ opCodes []opCode
157157+}
158158+159159+func newMatcher(a, b []string) *sequenceMatcher {
160160+ m := sequenceMatcher{autoJunk: true}
161161+ m.setSeqs(a, b)
162162+ return &m
163163+}
164164+165165+// Set two sequences to be compared.
166166+func (m *sequenceMatcher) setSeqs(a, b []string) {
167167+ m.setSeq1(a)
168168+ m.setSeq2(b)
169169+}
251702626- if len(oldLines) == 1 && oldLines[0] == "" {
2727- oldLines = []string{}
171171+// Set the first sequence to be compared.
172172+func (m *sequenceMatcher) setSeq1(a []string) {
173173+ if &a == &m.a {
174174+ return
28175 }
2929- if len(newLines) == 1 && newLines[0] == "" {
3030- newLines = []string{}
176176+ m.a = a
177177+ m.matchingBlocks, m.opCodes = nil, nil
178178+}
179179+180180+// Set the second sequence to be compared.
181181+func (m *sequenceMatcher) setSeq2(b []string) {
182182+ if &b == &m.b {
183183+ return
31184 }
185185+ m.b = b
186186+ m.matchingBlocks, m.opCodes, m.fullBCount = nil, nil, nil
187187+ m.chainB()
188188+}
321893333- matrix := computeEditDistance(oldLines, newLines)
3434- return traceback(oldLines, newLines, matrix)
190190+func (m *sequenceMatcher) chainB() {
191191+ // Populate line -> index mapping
192192+ b2j := map[string][]int{}
193193+ for i, elt := range m.b {
194194+ indices := b2j[elt]
195195+ indices = append(indices, i)
196196+ b2j[elt] = indices
197197+ }
198198+199199+ // Purge junk elements
200200+ m.bJunk = map[string]struct{}{}
201201+ if m.IsJunk != nil {
202202+ junk := m.bJunk
203203+ for elt := range b2j {
204204+ if m.IsJunk(elt) {
205205+ junk[elt] = struct{}{}
206206+ }
207207+ }
208208+ for elt := range junk { // separate loop avoids separate list of keys
209209+ delete(b2j, elt)
210210+ }
211211+ }
212212+213213+ // Purge popular elements that are not junk
214214+ popular := map[string]struct{}{}
215215+ n := len(m.b)
216216+ if m.autoJunk && n >= 200 {
217217+ ntest := n/100 + 1
218218+ for s, indices := range b2j {
219219+ if len(indices) > ntest {
220220+ popular[s] = struct{}{}
221221+ }
222222+ }
223223+ for s := range popular {
224224+ delete(b2j, s)
225225+ }
226226+ }
227227+ m.bPopular = popular
228228+ m.b2j = b2j
229229+}
230230+231231+func (m *sequenceMatcher) isBJunk(s string) bool {
232232+ _, ok := m.bJunk[s]
233233+ return ok
35234}
362353737-func computeEditDistance(old, new []string) [][]int {
3838- m, n := len(old), len(new)
3939- matrix := make([][]int, m+1)
4040- for i := range matrix {
4141- matrix[i] = make([]int, n+1)
236236+// Find longest matching block in a[alo:ahi] and b[blo:bhi].
237237+func (m *sequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) match {
238238+ besti, bestj, bestsize := alo, blo, 0
239239+240240+ // find longest junk-free match
241241+ j2len := map[int]int{}
242242+ for i := alo; i != ahi; i++ {
243243+ newj2len := map[int]int{}
244244+ for _, j := range m.b2j[m.a[i]] {
245245+ if j < blo {
246246+ continue
247247+ }
248248+ if j >= bhi {
249249+ break
250250+ }
251251+ k := j2len[j-1] + 1
252252+ newj2len[j] = k
253253+ if k > bestsize {
254254+ besti, bestj, bestsize = i-k+1, j-k+1, k
255255+ }
256256+ }
257257+ j2len = newj2len
42258 }
432594444- for i := 0; i <= m; i++ {
4545- matrix[i][0] = i
260260+ // Extend the best by non-junk elements on each end
261261+ for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) &&
262262+ m.a[besti-1] == m.b[bestj-1] {
263263+ besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
264264+ }
265265+ for besti+bestsize < ahi && bestj+bestsize < bhi &&
266266+ !m.isBJunk(m.b[bestj+bestsize]) &&
267267+ m.a[besti+bestsize] == m.b[bestj+bestsize] {
268268+ bestsize++
269269+ }
270270+271271+ // Suck up junk on each side
272272+ for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) &&
273273+ m.a[besti-1] == m.b[bestj-1] {
274274+ besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
275275+ }
276276+ for besti+bestsize < ahi && bestj+bestsize < bhi &&
277277+ m.isBJunk(m.b[bestj+bestsize]) &&
278278+ m.a[besti+bestsize] == m.b[bestj+bestsize] {
279279+ bestsize++
46280 }
4747- for j := 0; j <= n; j++ {
4848- matrix[0][j] = j
281281+282282+ return match{A: besti, B: bestj, Size: bestsize}
283283+}
284284+285285+// Return list of triples describing matching subsequences.
286286+func (m *sequenceMatcher) getMatchingBlocks() []match {
287287+ if m.matchingBlocks != nil {
288288+ return m.matchingBlocks
49289 }
502905151- for i := 1; i <= m; i++ {
5252- for j := 1; j <= n; j++ {
5353- if old[i-1] == new[j-1] {
5454- matrix[i][j] = matrix[i-1][j-1]
5555- } else {
5656- matrix[i][j] = 1 + minThree(matrix[i-1][j], matrix[i][j-1], matrix[i-1][j-1])
291291+ var matchBlocks func(alo, ahi, blo, bhi int, matched []match) []match
292292+ matchBlocks = func(alo, ahi, blo, bhi int, matched []match) []match {
293293+ match := m.findLongestMatch(alo, ahi, blo, bhi)
294294+ i, j, k := match.A, match.B, match.Size
295295+ if match.Size > 0 {
296296+ if alo < i && blo < j {
297297+ matched = matchBlocks(alo, i, blo, j, matched)
298298+ }
299299+ matched = append(matched, match)
300300+ if i+k < ahi && j+k < bhi {
301301+ matched = matchBlocks(i+k, ahi, j+k, bhi, matched)
57302 }
58303 }
304304+ return matched
59305 }
306306+ matched := matchBlocks(0, len(m.a), 0, len(m.b), nil)
603076161- return matrix
308308+ // Collapse adjacent equal blocks
309309+ nonAdjacent := []match{}
310310+ i1, j1, k1 := 0, 0, 0
311311+ for _, b := range matched {
312312+ i2, j2, k2 := b.A, b.B, b.Size
313313+ if i1+k1 == i2 && j1+k1 == j2 {
314314+ k1 += k2
315315+ } else {
316316+ if k1 > 0 {
317317+ nonAdjacent = append(nonAdjacent, match{i1, j1, k1})
318318+ }
319319+ i1, j1, k1 = i2, j2, k2
320320+ }
321321+ }
322322+ if k1 > 0 {
323323+ nonAdjacent = append(nonAdjacent, match{i1, j1, k1})
324324+ }
325325+326326+ nonAdjacent = append(nonAdjacent, match{len(m.a), len(m.b), 0})
327327+ m.matchingBlocks = nonAdjacent
328328+ return m.matchingBlocks
329329+}
330330+331331+// Return list of opcodes describing how to turn a into b.
332332+func (m *sequenceMatcher) getOpCodes() []opCode {
333333+ if m.opCodes != nil {
334334+ return m.opCodes
335335+ }
336336+ i, j := 0, 0
337337+ matching := m.getMatchingBlocks()
338338+ opCodes := make([]opCode, 0, len(matching))
339339+ for _, m := range matching {
340340+ ai, bj, size := m.A, m.B, m.Size
341341+ var tag int8 = 0
342342+ if i < ai && j < bj {
343343+ tag = opReplace
344344+ } else if i < ai {
345345+ tag = opDelete
346346+ } else if j < bj {
347347+ tag = opInsert
348348+ }
349349+ if tag > 0 {
350350+ opCodes = append(opCodes, opCode{tag, i, ai, j, bj})
351351+ }
352352+ i, j = ai+size, bj+size
353353+ if size > 0 {
354354+ opCodes = append(opCodes, opCode{opEqual, ai, i, bj, j})
355355+ }
356356+ }
357357+ m.opCodes = opCodes
358358+ return m.opCodes
62359}
633606464-func traceback(old, new []string, matrix [][]int) []DiffLine {
361361+// Histogram computes a diff between two strings using the Ratcliff-Obershelp algorithm
362362+func Histogram(old, new string) []DiffLine {
363363+ oldLines := splitLines(old)
364364+ newLines := splitLines(new)
365365+366366+ matcher := newMatcher(oldLines, newLines)
367367+ opcodes := matcher.getOpCodes()
368368+65369 var result []DiffLine
6666- i, j := len(old), len(new)
673706868- for i > 0 || j > 0 {
6969- if i > 0 && j > 0 && old[i-1] == new[j-1] {
7070- // Lines match - shared
7171- result = append([]DiffLine{{Line: old[i-1], Kind: DiffShared, OldNumber: i, NewNumber: j}}, result...)
7272- i--
7373- j--
7474- } else if i > 0 && j > 0 && matrix[i-1][j] == matrix[i][j-1] {
7575- // Equal cost - prefer showing deletions first, then additions
7676- // This groups changes better
7777- result = append([]DiffLine{{Line: old[i-1], Kind: DiffOld, OldNumber: i}}, result...)
7878- i--
7979- } else if j > 0 && (i == 0 || matrix[i][j-1] <= matrix[i-1][j]) {
8080- // Addition
8181- result = append([]DiffLine{{Line: new[j-1], Kind: DiffNew, NewNumber: j}}, result...)
8282- j--
8383- } else if i > 0 {
8484- // Deletion
8585- result = append([]DiffLine{{Line: old[i-1], Kind: DiffOld, OldNumber: i}}, result...)
8686- i--
371371+ for _, op := range opcodes {
372372+ switch op.Tag {
373373+ case opEqual:
374374+ for i := op.I1; i < op.I2; i++ {
375375+ newIdx := i + (op.J1 - op.I1)
376376+ result = append(result, DiffLine{
377377+ Line: oldLines[i],
378378+ Kind: DiffShared,
379379+ OldNumber: i + 1,
380380+ NewNumber: newIdx + 1,
381381+ })
382382+ }
383383+ case opDelete:
384384+ for i := op.I1; i < op.I2; i++ {
385385+ result = append(result, DiffLine{
386386+ Line: oldLines[i],
387387+ Kind: DiffOld,
388388+ OldNumber: i + 1,
389389+ })
390390+ }
391391+ case opInsert:
392392+ for j := op.J1; j < op.J2; j++ {
393393+ result = append(result, DiffLine{
394394+ Line: newLines[j],
395395+ Kind: DiffNew,
396396+ NewNumber: j + 1,
397397+ })
398398+ }
399399+ case opReplace:
400400+ for i := op.I1; i < op.I2; i++ {
401401+ result = append(result, DiffLine{
402402+ Line: oldLines[i],
403403+ Kind: DiffOld,
404404+ OldNumber: i + 1,
405405+ })
406406+ }
407407+ for j := op.J1; j < op.J2; j++ {
408408+ result = append(result, DiffLine{
409409+ Line: newLines[j],
410410+ Kind: DiffNew,
411411+ NewNumber: j + 1,
412412+ })
413413+ }
87414 }
88415 }
8941690417 return result
91418}
924199393-func minThree(a, b, c int) int {
9494- if a < b {
9595- if a < c {
9696- return a
420420+// splitLines splits a string by newlines, handling empty strings
421421+func splitLines(s string) []string {
422422+ if s == "" {
423423+ return []string{}
424424+ }
425425+ lines := splitLinesKeepNewline(s)
426426+ return lines
427427+}
428428+429429+// splitLinesKeepNewline splits on newlines but removes the newlines themselves
430430+func splitLinesKeepNewline(s string) []string {
431431+ var lines []string
432432+ var current string
433433+434434+ for _, char := range s {
435435+ if char == '\n' {
436436+ lines = append(lines, current)
437437+ current = ""
438438+ } else {
439439+ current += string(char)
97440 }
9898- return c
441441+ }
442442+443443+ if current != "" {
444444+ lines = append(lines, current)
445445+ }
446446+447447+ return lines
448448+}
449449+450450+// FormatRangeUnified converts range to the "ed" format for unified diffs
451451+func FormatRangeUnified(start, stop int) string {
452452+ beginning := start + 1 // lines start numbering with one
453453+ length := stop - start
454454+455455+ if length == 1 {
456456+ return strconv.Itoa(beginning)
99457 }
100100- if b < c {
101101- return b
458458+ if length == 0 {
459459+ beginning--
102460 }
103103- return c
461461+462462+ return strconv.Itoa(beginning) + "," + strconv.Itoa(length)
104463}