mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
0cc033edd8
POST /workspaces/{ws}/items/{ref}/patch {edits:[{old,new,line_hint?}],
expected_seq?} replaces exact text in the body. It runs through updateItem,
so it takes every PATCH guard; the body is computed inside the store
transaction from the row it replaces. Each old must occur exactly once;
edits apply in order, all or nothing; every patch writes a version. The
answer carries a unified diff of what changed.
expected_seq does not refuse: a moved seq is applied over when the
anchors still match, and answered rebased with the intervening writes,
read from a seq chain stamped into item activity entries ({prev_seq, seq,
version_id}). complete is decided at runtime, so any write that left no
stamp reads incomplete; a guard test classifies every seq-moving store
statement. Refusals write nothing: patch_anchor_missing,
patch_anchor_ambiguous, patch_room_open (an open editor; step 2).
CLI: pad item patch, pad item show --lines. MCP 0.87 (additive):
pad_item.patch, get lines, three allow-listed codes. The timeline treats
the chain and patch keys as bookkeeping, not a change.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
128 lines
3.0 KiB
Go
128 lines
3.0 KiB
Go
package diff
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
difflib "github.com/sergi/go-diff/diffmatchpatch"
|
|
)
|
|
|
|
// Unified renders a line-based unified diff of oldText → newText with the
|
|
// given lines of context (PLAN-3578: what a patch write hands back so the
|
|
// caller can verify exactly what changed). Empty when the texts are equal.
|
|
// Headers are fixed ("--- before" / "+++ after"); hunk ranges follow the
|
|
// unified format (1-based start, count; a zero-count range starts at the
|
|
// line before it).
|
|
func Unified(oldText, newText string, context int) string {
|
|
if oldText == newText {
|
|
return ""
|
|
}
|
|
a, b, lines := dmp.DiffLinesToChars(oldText, newText)
|
|
diffs := dmp.DiffCharsToLines(dmp.DiffMain(a, b, false), lines)
|
|
|
|
type line struct {
|
|
op difflib.Operation
|
|
text string
|
|
}
|
|
var all []line
|
|
for _, d := range diffs {
|
|
for _, l := range splitKeepEnd(d.Text) {
|
|
all = append(all, line{d.Type, l})
|
|
}
|
|
}
|
|
|
|
var out strings.Builder
|
|
out.WriteString("--- before\n+++ after\n")
|
|
oldNo, newNo := 1, 1
|
|
for i := 0; i < len(all); {
|
|
if all[i].op == difflib.DiffEqual {
|
|
oldNo++
|
|
newNo++
|
|
i++
|
|
continue
|
|
}
|
|
// A change starts at i: back up over up to `context` equal lines.
|
|
start := i
|
|
for k := 0; k < context && start > 0 && all[start-1].op == difflib.DiffEqual; k++ {
|
|
start--
|
|
}
|
|
hOld, hNew := oldNo-(i-start), newNo-(i-start)
|
|
// Extend through changes separated by at most 2*context equal lines.
|
|
end := i
|
|
for end < len(all) {
|
|
if all[end].op != difflib.DiffEqual {
|
|
end++
|
|
continue
|
|
}
|
|
run := end
|
|
for run < len(all) && all[run].op == difflib.DiffEqual {
|
|
run++
|
|
}
|
|
if run < len(all) && run-end <= 2*context {
|
|
end = run
|
|
continue
|
|
}
|
|
end = min(end+context, run)
|
|
break
|
|
}
|
|
var body strings.Builder
|
|
oc, nc := 0, 0
|
|
for _, l := range all[start:end] {
|
|
switch l.op {
|
|
case difflib.DiffEqual:
|
|
body.WriteString(" " + l.text)
|
|
oc++
|
|
nc++
|
|
case difflib.DiffDelete:
|
|
body.WriteString("-" + l.text)
|
|
oc++
|
|
case difflib.DiffInsert:
|
|
body.WriteString("+" + l.text)
|
|
nc++
|
|
}
|
|
if !strings.HasSuffix(l.text, "\n") {
|
|
body.WriteString("\n\\ No newline at end of file\n")
|
|
}
|
|
}
|
|
fmt.Fprintf(&out, "@@ -%s +%s @@\n", hunkRange(hOld, oc), hunkRange(hNew, nc))
|
|
out.WriteString(body.String())
|
|
// Advance the counters over the lines of this hunk that lie past i.
|
|
for _, l := range all[i:end] {
|
|
if l.op != difflib.DiffInsert {
|
|
oldNo++
|
|
}
|
|
if l.op != difflib.DiffDelete {
|
|
newNo++
|
|
}
|
|
}
|
|
i = end
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
func hunkRange(start, count int) string {
|
|
if count == 0 {
|
|
return fmt.Sprintf("%d,0", start-1)
|
|
}
|
|
if count == 1 {
|
|
return fmt.Sprintf("%d", start)
|
|
}
|
|
return fmt.Sprintf("%d,%d", start, count)
|
|
}
|
|
|
|
// splitKeepEnd splits s into lines, each keeping its "\n"; a last line
|
|
// without one is kept as is.
|
|
func splitKeepEnd(s string) []string {
|
|
var out []string
|
|
for s != "" {
|
|
k := strings.IndexByte(s, '\n')
|
|
if k < 0 {
|
|
out = append(out, s)
|
|
break
|
|
}
|
|
out = append(out, s[:k+1])
|
|
s = s[k+1:]
|
|
}
|
|
return out
|
|
}
|