Files
2026-09-27 16:04:02 -06:00

227 lines
4.9 KiB
Go

package patch
import (
"errors"
"fmt"
"strings"
)
// Límites de la generación de diffs: por encima, usar "diff -u" del sistema.
const (
maxDiffLines = 50000
maxEditDist = 5000
)
// ErrTooDifferent indica que los archivos difieren demasiado para calcular
// el diff con memoria acotada.
var ErrTooDifferent = errors.New("files are too different to diff efficiently; use exec diff -u")
type opKind byte
const (
opEqual opKind = ' '
opDelete opKind = '-'
opInsert opKind = '+'
)
type edit struct {
kind opKind
text string
a, b int // índices en old/new (0-based) para calcular cabeceras
}
// Unified genera un diff unificado entre old y new con context líneas de
// contexto. Devuelve "" si son iguales. Sirve para documentar exactamente
// los cambios realizados (§40, §88) y para crear parches propios.
func Unified(oldName, newName, old, new string, context int) (string, error) {
if old == new {
return "", nil
}
// Las líneas conservan su "\n": así una última línea sin salto final se
// distingue de la misma línea con salto, y el diff lo refleja.
a, b := splitKeep(old), splitKeep(new)
if len(a) > maxDiffLines || len(b) > maxDiffLines {
return "", ErrTooDifferent
}
edits, err := myers(a, b)
if err != nil {
return "", err
}
var sb strings.Builder
fmt.Fprintf(&sb, "--- %s\n+++ %s\n", oldName, newName)
// Agrupar cambios en hunks con contexto.
for i := 0; i < len(edits); {
// Buscar el siguiente cambio.
for i < len(edits) && edits[i].kind == opEqual {
i++
}
if i >= len(edits) {
break
}
start := max(i-context, 0)
end := i
// Extender mientras los cambios estén separados por <= 2*context iguales.
for end < len(edits) {
if edits[end].kind != opEqual {
end++
continue
}
run := end
for run < len(edits) && edits[run].kind == opEqual {
run++
}
if run >= len(edits) || run-end > 2*context {
end = min(end+context, len(edits))
break
}
end = run
}
writeHunk(&sb, edits[start:end])
i = end
}
return sb.String(), nil
}
func writeHunk(sb *strings.Builder, es []edit) {
oldStart, newStart := -1, -1
oldN, newN := 0, 0
for _, e := range es {
if e.kind != opInsert {
if oldStart < 0 {
oldStart = e.a
}
oldN++
}
if e.kind != opDelete {
if newStart < 0 {
newStart = e.b
}
newN++
}
}
// Para lados vacíos, la convención es la línea anterior (o 0).
if oldStart < 0 {
oldStart = es[0].a - 1
}
if newStart < 0 {
newStart = es[0].b - 1
}
fmt.Fprintf(sb, "@@ -%s +%s @@\n", rangeStr(oldStart, oldN), rangeStr(newStart, newN))
for _, e := range es {
sb.WriteByte(byte(e.kind))
sb.WriteString(strings.TrimSuffix(e.text, "\n"))
sb.WriteByte('\n')
if !strings.HasSuffix(e.text, "\n") {
sb.WriteString("\\ No newline at end of file\n")
}
}
}
// splitKeep divide en líneas conservando el "\n" de cada una.
func splitKeep(s string) []string {
if s == "" {
return nil
}
lines := strings.SplitAfter(s, "\n")
if lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
return lines
}
func rangeStr(start, n int) string {
if n == 1 {
return fmt.Sprint(start + 1)
}
if n == 0 {
return fmt.Sprintf("%d,0", start+1)
}
return fmt.Sprintf("%d,%d", start+1, n)
}
// myers calcula el script de edición mínimo (algoritmo de Myers, O((N+M)D)).
// Guarda solo la franja [-d, d] de cada iteración, así la memoria es O(D²).
func myers(a, b []string) ([]edit, error) {
n, m := len(a), len(b)
var trace [][]int
v := map[int]int{1: 0}
var dFinal int
found := false
for d := 0; d <= n+m; d++ {
if d > maxEditDist {
return nil, ErrTooDifferent
}
snap := make([]int, 2*d+3)
for k := -d; k <= d; k += 2 {
var x int
if k == -d || (k != d && v[k-1] < v[k+1]) {
x = v[k+1]
} else {
x = v[k-1] + 1
}
y := x - k
for x < n && y < m && a[x] == b[y] {
x++
y++
}
v[k] = x
if x >= n && y >= m {
found = true
}
}
for k := -d - 1; k <= d+1; k++ {
snap[k+d+1] = v[k]
}
trace = append(trace, snap)
if found {
dFinal = d
break
}
}
get := func(d, k int) int {
s := trace[d]
idx := k + d + 1
if idx < 0 || idx >= len(s) {
return 0
}
return s[idx]
}
var rev []edit
x, y := n, m
for d := dFinal; d > 0; d-- {
k := x - y
var prevK int
if k == -d || (k != d && get(d-1, k-1) < get(d-1, k+1)) {
prevK = k + 1
} else {
prevK = k - 1
}
prevX := get(d-1, prevK)
prevY := prevX - prevK
for x > prevX && y > prevY {
x--
y--
rev = append(rev, edit{kind: opEqual, text: a[x], a: x, b: y})
}
if x == prevX {
y--
rev = append(rev, edit{kind: opInsert, text: b[y], a: x, b: y})
} else {
x--
rev = append(rev, edit{kind: opDelete, text: a[x], a: x, b: y})
}
}
for x > 0 && y > 0 {
x--
y--
rev = append(rev, edit{kind: opEqual, text: a[x], a: x, b: y})
}
out := make([]edit, len(rev))
for i := range rev {
out[i] = rev[len(rev)-1-i]
}
return out, nil
}