Myers-Zeilendiff (DiffText) und Metadaten-Vorher-Nachher (DiffMetadata) im versioning-Paket, DiffRevisions als fertige Schnittstelle für VER-04. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
201 lines
5.3 KiB
Go
201 lines
5.3 KiB
Go
package versioning
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
dmscrypto "gitea.perlbach24.de/scripte/nexarch/dms/internal/crypto"
|
|
"gitea.perlbach24.de/scripte/nexarch/dms/internal/storage"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// DiffOp beschreibt eine einzelne Zeilen-Operation eines Textvergleichs.
|
|
type DiffOp string
|
|
|
|
const (
|
|
DiffEqual DiffOp = "equal"
|
|
DiffInsert DiffOp = "insert"
|
|
DiffDelete DiffOp = "delete"
|
|
)
|
|
|
|
// TextDiffLine ist eine Zeile des Ergebnisses von DiffText.
|
|
type TextDiffLine struct {
|
|
Op DiffOp
|
|
Text string
|
|
}
|
|
|
|
// DiffText vergleicht zwei Texte zeilenweise (Akzeptanzkriterium 1). Nutzt
|
|
// den Myers-Diff-Algorithmus (O(N*D), D = Anzahl Unterschiede) statt einer
|
|
// vollen O(N*M)-LCS-Matrix, damit auch große, weitgehend gleiche Dokumente
|
|
// (Pflichtprüfung 3) ohne quadratischen Speicherbedarf vergleichbar bleiben.
|
|
func DiffText(oldText, newText string) []TextDiffLine {
|
|
oldLines := splitLines(oldText)
|
|
newLines := splitLines(newText)
|
|
return myersDiff(oldLines, newLines)
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
if s == "" {
|
|
return []string{}
|
|
}
|
|
return strings.Split(s, "\n")
|
|
}
|
|
|
|
// myersDiff implementiert Eugene Myers' "An O(ND) Difference Algorithm"
|
|
// über das Trace der Kürzeste-Editierskript-Suche und läuft es danach
|
|
// rückwärts ab, um die eigentliche Operationenfolge zu rekonstruieren.
|
|
func myersDiff(a, b []string) []TextDiffLine {
|
|
n, m := len(a), len(b)
|
|
max := n + m
|
|
if max == 0 {
|
|
return []TextDiffLine{}
|
|
}
|
|
offset := max
|
|
size := 2*max + 1
|
|
trace := make([][]int, 0, max+1)
|
|
|
|
v := make([]int, size)
|
|
found := false
|
|
var foundD int
|
|
for d := 0; d <= max && !found; d++ {
|
|
snapshot := make([]int, size)
|
|
copy(snapshot, v)
|
|
for k := -d; k <= d; k += 2 {
|
|
var x int
|
|
if k == -d || (k != d && v[offset+k-1] < v[offset+k+1]) {
|
|
x = v[offset+k+1]
|
|
} else {
|
|
x = v[offset+k-1] + 1
|
|
}
|
|
y := x - k
|
|
for x < n && y < m && a[x] == b[y] {
|
|
x++
|
|
y++
|
|
}
|
|
v[offset+k] = x
|
|
if x >= n && y >= m {
|
|
foundD = d
|
|
found = true
|
|
}
|
|
}
|
|
trace = append(trace, snapshot)
|
|
}
|
|
|
|
x, y := n, m
|
|
var ops []TextDiffLine
|
|
for d := foundD; d > 0; d-- {
|
|
v := trace[d]
|
|
k := x - y
|
|
var prevK int
|
|
if k == -d || (k != d && v[offset+k-1] < v[offset+k+1]) {
|
|
prevK = k + 1
|
|
} else {
|
|
prevK = k - 1
|
|
}
|
|
prevX := v[offset+prevK]
|
|
prevY := prevX - prevK
|
|
|
|
for x > prevX && y > prevY {
|
|
ops = append(ops, TextDiffLine{Op: DiffEqual, Text: a[x-1]})
|
|
x--
|
|
y--
|
|
}
|
|
if x == prevX {
|
|
ops = append(ops, TextDiffLine{Op: DiffInsert, Text: b[y-1]})
|
|
y--
|
|
} else {
|
|
ops = append(ops, TextDiffLine{Op: DiffDelete, Text: a[x-1]})
|
|
x--
|
|
}
|
|
}
|
|
for x > 0 && y > 0 {
|
|
ops = append(ops, TextDiffLine{Op: DiffEqual, Text: a[x-1]})
|
|
x--
|
|
y--
|
|
}
|
|
|
|
for i, j := 0, len(ops)-1; i < j; i, j = i+1, j-1 {
|
|
ops[i], ops[j] = ops[j], ops[i]
|
|
}
|
|
return ops
|
|
}
|
|
|
|
// MetadataDiff ist eine Vorher-Nachher-Änderung eines Revisionsfeldes
|
|
// (Akzeptanzkriterium 2).
|
|
type MetadataDiff struct {
|
|
Field string
|
|
Before string
|
|
After string
|
|
}
|
|
|
|
// DiffMetadata vergleicht die Revisions-Metadaten zweier Revisionen und
|
|
// liefert nur tatsächlich geänderte Felder. Verglichen werden die Felder,
|
|
// die je Revision individuell gesetzt werden (MIME-Typ, Größe, Kommentar,
|
|
// Ersteller) — Prüfsumme und Zeitstempel sind für JEDE Revision naturgemäß
|
|
// unterschiedlich und daher als "Änderung" nicht aussagekräftig, sie werden
|
|
// bewusst nicht mit aufgeführt.
|
|
func DiffMetadata(before, after Revision) []MetadataDiff {
|
|
var diffs []MetadataDiff
|
|
add := func(field, b, a string) {
|
|
if b != a {
|
|
diffs = append(diffs, MetadataDiff{Field: field, Before: b, After: a})
|
|
}
|
|
}
|
|
add("mime_type", before.MimeType, after.MimeType)
|
|
add("size_bytes", strconv.FormatInt(before.SizeBytes, 10), strconv.FormatInt(after.SizeBytes, 10))
|
|
add("comment", derefOrEmpty(before.Comment), derefOrEmpty(after.Comment))
|
|
add("created_by", before.CreatedBy, after.CreatedBy)
|
|
if diffs == nil {
|
|
diffs = []MetadataDiff{}
|
|
}
|
|
return diffs
|
|
}
|
|
|
|
func derefOrEmpty(s *string) string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return *s
|
|
}
|
|
|
|
// DiffRevisions lädt zwei Revisionen, entschlüsselt beide Inhalte und
|
|
// liefert sowohl den Zeilen-Textvergleich als auch den Metadaten-Vergleich
|
|
// (Akzeptanzkriterien 1+2 in einem Aufruf, für VER-04 als fertige
|
|
// Schnittstelle vorbereitet).
|
|
func DiffRevisions(
|
|
ctx context.Context, pool *pgxpool.Pool, storageSvc *storage.Service, cryptoSvc *dmscrypto.Service, tenantSlug string,
|
|
documentID, revisionIDA, revisionIDB string,
|
|
) ([]TextDiffLine, []MetadataDiff, error) {
|
|
revisions, err := ListRevisions(ctx, pool, documentID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
revByID := make(map[string]Revision, len(revisions))
|
|
for _, r := range revisions {
|
|
revByID[r.ID] = r
|
|
}
|
|
revA, ok := revByID[revisionIDA]
|
|
if !ok {
|
|
return nil, nil, ErrRevisionNotFound
|
|
}
|
|
revB, ok := revByID[revisionIDB]
|
|
if !ok {
|
|
return nil, nil, ErrRevisionNotFound
|
|
}
|
|
|
|
contentA, _, err := GetRevisionContent(ctx, pool, storageSvc, cryptoSvc, tenantSlug, documentID, revisionIDA)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("versioning: revision a laden: %w", err)
|
|
}
|
|
contentB, _, err := GetRevisionContent(ctx, pool, storageSvc, cryptoSvc, tenantSlug, documentID, revisionIDB)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("versioning: revision b laden: %w", err)
|
|
}
|
|
|
|
textDiff := DiffText(string(contentA), string(contentB))
|
|
metadataDiff := DiffMetadata(revA, revB)
|
|
return textDiff, metadataDiff, nil
|
|
}
|