Files
zero/internal/tools/structured_patch.go
PierrunoYTandClaude Opus 5.5 13e2596b2e chore: move repository references to Twigpine/zero (#1105)
Update the Go module path, imports, install scripts, docs, workflows,
packaging and action defaults from Gitlawb/zero to Twigpine/zero. The npm
scope (already @twigpine/zero on main, with its @gitlawb/zero migration
notes), LICENSE, action author, the OpenGateway provider and historical
CHANGELOG links are unchanged.

The X-AIMLAPI-Integration-Repo header keeps Gitlawb/zero: it is what
AIMLAPI sees for attribution, so changing it needs a maintainer's yes.
docs/INSTALL.md keeps `go install github.com/Gitlawb/zero/...`, because
every tagged release so far declares that module path. The Homebrew
formula comment names the Twigpine/homebrew-tap tap.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-17 07:13:21 +08:00

1160 lines
41 KiB
Go

package tools
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/Twigpine/zero/internal/pathjail"
"github.com/Twigpine/zero/internal/sandbox"
)
const (
structuredPatchBegin = "*** Begin Patch"
structuredPatchEnd = "*** End Patch"
structuredAddFile = "*** Add File: "
structuredDeleteFile = "*** Delete File: "
structuredUpdateFile = "*** Update File: "
structuredMoveTo = "*** Move to: "
structuredEndOfFile = "*** End of File"
structuredHunkContext = "@@ "
)
type structuredPatchKind uint8
const (
structuredPatchAdd structuredPatchKind = iota
structuredPatchDelete
structuredPatchUpdate
// structuredPatchCopy creates movePath from path's content (plus any
// hunks) and keeps path; produced by a unified diff's "copy from/to".
structuredPatchCopy
)
type structuredPatchOperation struct {
kind structuredPatchKind
path string
movePath string
contents string
chunks []structuredPatchChunk
line int
// eofNewline lets a unified diff's "\ No newline at end of file" marker
// force the trailing-newline state of the result; structured patches keep
// the file's existing state.
eofNewline eofNewlineMode
// verifyDelete marks a deletion that carries the expected old content
// (a unified diff's "+++ /dev/null" hunks, or git's header-only form for
// an empty file): the removed lines must equal the current file byte for
// byte, otherwise the deletion is stale and refused. A structured
// "*** Delete File" means "delete this path" and has no chunks.
verifyDelete bool
// oldNoNewline records a unified diff's "\ No newline at end of file"
// on the removed side, i.e. the old content did not end with a newline.
oldNoNewline bool
}
type eofNewlineMode uint8
const (
eofNewlineKeep eofNewlineMode = iota
eofNewlinePresent
eofNewlineAbsent
)
type structuredPatchChunk struct {
context string
hasContext bool
old []string
new []string
newSourceOffsets []int
endOfFile bool
// hint is the 0-based line the hunk is expected at (from a unified diff's
// "@@ -a,b" range). When the expected lines match there it is used as-is;
// otherwise the hunk is located by content like a structured hunk.
hint int
hasHint bool
}
type structuredPatchTarget struct {
requested string
absolute string
relative string
}
type structuredPatchChange struct {
kind structuredPatchKind
from structuredPatchTarget
to structuredPatchTarget
before string
after string
mode os.FileMode
beforeInfo os.FileInfo
}
// unifiedHunkRangePattern recognises a unified-diff range header ("-12,4 +12,6",
// optionally followed by "@@ heading") written inside a structured hunk marker.
// The line numbers carry no information for the context-anchored grammar, so
// the range is dropped and only an explicit heading is kept as the anchor.
var unifiedHunkRangePattern = regexp.MustCompile(`^-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?\s*(?:@@\s*(.*))?$`)
const structuredPatchFormatHint = ` (format: "*** Begin Patch", then "*** Update File: path" / "*** Add File: path" / "*** Delete File: path" sections whose hunks start with "@@" and use " " context, "-" removed and "+" added lines, then "*** End Patch")`
// structuredPatchMarker classifies a line as the "begin" or "end" marker of a
// structured patch, or "" when it is neither. The classifier is owned by the
// sandbox package so the boundary check and the tool accept exactly the same
// spellings; a strict byte-equal match here once made every patch from some
// models fail on line 1 and pushed them into whole-file rewrites.
func structuredPatchMarker(line string) string {
return sandbox.StructuredPatchMarker(line)
}
// structuredHunkAnchor normalises the text after a hunk's "@@ " marker. A
// unified-diff range is dropped (its optional heading survives as the anchor);
// anything else is used verbatim.
func structuredHunkAnchor(context string) string {
context = strings.TrimSpace(context)
if match := unifiedHunkRangePattern.FindStringSubmatch(context); match != nil {
return strings.TrimSpace(match[1])
}
return context
}
func isStructuredPatch(patch string) bool {
return sandbox.IsStructuredPatch(patch)
}
func (tool applyPatchTool) runStructuredPatch(applyRoot, relativeRoot, patch string, options RunOptions) Result {
operations, err := parseStructuredPatch(patch)
if err != nil {
return errorResult("Error applying patch: " + err.Error())
}
return applyPatchOperations(applyRoot, relativeRoot, operations, options)
}
// applyPatchOperations applies parsed operations (from either patch format)
// through an opened workspace root: every stat, read, create and write is
// descriptor-relative and refuses to follow a link out of the root, so there
// is no pathname check-to-use window between validation and write.
func applyPatchOperations(applyRoot, relativeRoot string, operations []structuredPatchOperation, options RunOptions) Result {
workspace, err := os.OpenRoot(applyRoot)
if err != nil {
return errorResult("Error applying patch: " + err.Error())
}
defer workspace.Close()
changes, err := planStructuredPatch(workspace, operations, options.FileTracker)
if err != nil {
return errorResult("Error applying patch: " + err.Error())
}
wholeBefore := make(map[string]bool, len(changes))
if options.FileTracker != nil {
for _, change := range changes {
if change.kind == structuredPatchUpdate || change.kind == structuredPatchCopy {
wholeBefore[change.from.absolute] = options.FileTracker.SeenWhole(change.from.absolute)
}
}
}
applyOutcome, err := applyStructuredPatchChanges(workspace, relativeRoot, changes, options.FileTracker)
if err != nil {
result := errorResult("Error applying patch: " + err.Error())
result.ChangedFiles = changedFilesFromStructuredPatch(relativeRoot, applyOutcome.committed)
result.FileDiffs = fileDiffsFromStructuredPatch(relativeRoot, applyOutcome.committed)
result.Redacted = structuredPatchContainsObfuscatedSecret(applyOutcome.committed)
result.Display = Display{Summary: result.Output, Kind: "diff", Preview: structuredPatchPreview(applyOutcome.committed)}
return result
}
for _, change := range changes {
switch change.kind {
case structuredPatchDelete:
options.FileTracker.Forget(change.from.absolute)
case structuredPatchAdd:
recordStructuredPatchFile(options.FileTracker, change.to.absolute, true, true)
case structuredPatchUpdate:
wasWhole := wholeBefore[change.from.absolute]
options.FileTracker.Forget(change.from.absolute)
recordStructuredPatchFile(options.FileTracker, change.to.absolute, false, wasWhole)
case structuredPatchCopy:
// The source is untouched; the destination inherits only what the
// model had actually seen of the source.
recordStructuredPatchFile(options.FileTracker, change.to.absolute, true, wholeBefore[change.from.absolute])
}
}
summary := "Patch applied successfully."
if relativeRoot != "." {
summary = "Patch applied successfully in " + relativeRoot + "."
}
result := okResult(summary)
result.ChangedFiles = changedFilesFromStructuredPatch(relativeRoot, changes)
result.FileDiffs = fileDiffsFromStructuredPatch(relativeRoot, changes)
result.Redacted = structuredPatchContainsObfuscatedSecret(changes)
result.Display = Display{Summary: summary, Kind: "diff", Preview: structuredPatchPreview(changes)}
return result
}
func structuredPatchContainsObfuscatedSecret(changes []structuredPatchChange) bool {
for _, change := range changes {
if diffTextRevealsObfuscatedSecret(change.before) || diffTextRevealsObfuscatedSecret(change.after) {
return true
}
}
return false
}
func fileDiffsFromStructuredPatch(_ string, changes []structuredPatchChange) []FileDiff {
const maxToolResultFileDiffs = 64
diffs := make([]FileDiff, 0, len(changes)*2)
usedBytes := 0
appendGroup := func(group ...FileDiff) bool {
if len(group) == 0 || len(diffs)+len(group) > maxToolResultFileDiffs {
return false
}
groupBytes := 0
for _, diff := range group {
groupBytes += len(diff.OldText) + len(diff.NewText)
}
if usedBytes+groupBytes > maxToolResultFileDiffBytes {
return false
}
diffs = append(diffs, group...)
usedBytes += groupBytes
return true
}
makeDiff := func(path, before, after string, oldExists, newExists bool) (FileDiff, bool) {
return boundedFileDiff(path, before, after, oldExists, newExists)
}
for _, change := range changes {
var group []FileDiff
switch {
case change.kind == structuredPatchDelete:
if diff, ok := makeDiff(change.from.absolute, change.before, "", true, false); ok {
group = append(group, diff)
}
case change.kind == structuredPatchAdd:
if diff, ok := makeDiff(change.to.absolute, "", change.after, false, true); ok {
group = append(group, diff)
}
case change.kind == structuredPatchCopy && change.from.absolute != change.to.absolute:
// A copy leaves its source unchanged; the destination is a create.
if diff, ok := makeDiff(change.to.absolute, "", change.after, false, true); ok {
group = append(group, diff)
}
case change.kind == structuredPatchUpdate && change.from.absolute != change.to.absolute:
// A move is two filesystem changes, not a destination overwrite.
from, fromOK := makeDiff(change.from.absolute, change.before, "", true, false)
to, toOK := makeDiff(change.to.absolute, "", change.after, false, true)
if fromOK && toOK {
group = append(group, from, to)
}
default:
if diff, ok := makeDiff(change.to.absolute, change.before, change.after, true, true); ok {
group = append(group, diff)
}
}
if len(group) > 0 && !appendGroup(group...) {
break
}
}
return diffs
}
func parseStructuredPatch(patch string) ([]structuredPatchOperation, error) {
normalized := strings.TrimSpace(strings.TrimPrefix(strings.ReplaceAll(patch, "\r\n", "\n"), "\ufeff"))
if normalized == "" {
return nil, fmt.Errorf("structured patch is empty")
}
lines := strings.Split(normalized, "\n")
if structuredPatchMarker(lines[0]) != "begin" {
return nil, fmt.Errorf("the first line of a structured patch must be %q%s", structuredPatchBegin, structuredPatchFormatHint)
}
if structuredPatchMarker(lines[len(lines)-1]) != "end" {
return nil, fmt.Errorf("the last line of a structured patch must be %q%s", structuredPatchEnd, structuredPatchFormatHint)
}
var operations []structuredPatchOperation
for index := 1; index < len(lines)-1; {
header := strings.TrimSpace(lines[index])
lineNumber := index + 1
if header == "" {
index++
continue
}
switch {
case strings.HasPrefix(header, structuredAddFile):
path, err := structuredPatchPath(header, structuredAddFile, lineNumber)
if err != nil {
return nil, err
}
op := structuredPatchOperation{kind: structuredPatchAdd, path: path, line: lineNumber}
index++
var content []string
for index < len(lines)-1 && !isStructuredPatchHeader(lines[index]) {
if !strings.HasPrefix(lines[index], "+") {
return nil, structuredPatchLineError(index+1, "added-file contents must start with '+'")
}
content = append(content, strings.TrimPrefix(lines[index], "+"))
index++
}
if len(content) > 0 {
op.contents = strings.Join(content, "\n") + "\n"
}
operations = append(operations, op)
case strings.HasPrefix(header, structuredDeleteFile):
path, err := structuredPatchPath(header, structuredDeleteFile, lineNumber)
if err != nil {
return nil, err
}
operations = append(operations, structuredPatchOperation{kind: structuredPatchDelete, path: path, line: lineNumber})
index++
case strings.HasPrefix(header, structuredUpdateFile):
path, err := structuredPatchPath(header, structuredUpdateFile, lineNumber)
if err != nil {
return nil, err
}
op := structuredPatchOperation{kind: structuredPatchUpdate, path: path, line: lineNumber}
index++
if index < len(lines)-1 && strings.HasPrefix(strings.TrimSpace(lines[index]), structuredMoveTo) {
op.movePath, err = structuredPatchPath(strings.TrimSpace(lines[index]), structuredMoveTo, index+1)
if err != nil {
return nil, err
}
index++
}
for index < len(lines)-1 && !isStructuredPatchHeader(lines[index]) {
line := lines[index]
trimmed := strings.TrimSpace(line)
if trimmed == structuredEndOfFile {
if len(op.chunks) == 0 || (!hasStructuredPatchContent(op.chunks[len(op.chunks)-1])) {
return nil, structuredPatchLineError(index+1, "*** End of File must follow an update hunk")
}
op.chunks[len(op.chunks)-1].endOfFile = true
index++
continue
}
if trimmed == "@@" || strings.HasPrefix(trimmed, structuredHunkContext) {
chunk := structuredPatchChunk{}
rest := ""
if trimmed != "@@" {
rest = strings.TrimPrefix(trimmed, structuredHunkContext)
}
if anchor := structuredHunkAnchor(rest); anchor != "" {
chunk.context = anchor
chunk.hasContext = true
}
op.chunks = append(op.chunks, chunk)
index++
continue
}
if len(op.chunks) == 0 {
op.chunks = append(op.chunks, structuredPatchChunk{})
}
chunk := &op.chunks[len(op.chunks)-1]
switch {
case line == "":
sourceOffset := len(chunk.old)
chunk.old = append(chunk.old, "")
chunk.new = append(chunk.new, "")
chunk.newSourceOffsets = append(chunk.newSourceOffsets, sourceOffset)
case strings.HasPrefix(line, " "):
content := strings.TrimPrefix(line, " ")
sourceOffset := len(chunk.old)
chunk.old = append(chunk.old, content)
chunk.new = append(chunk.new, content)
chunk.newSourceOffsets = append(chunk.newSourceOffsets, sourceOffset)
case strings.HasPrefix(line, "+"):
chunk.new = append(chunk.new, strings.TrimPrefix(line, "+"))
chunk.newSourceOffsets = append(chunk.newSourceOffsets, -1)
case strings.HasPrefix(line, "-"):
chunk.old = append(chunk.old, strings.TrimPrefix(line, "-"))
default:
return nil, structuredPatchLineError(index+1, "update lines must start with ' ', '+', or '-'")
}
index++
}
if len(op.chunks) == 0 || !allStructuredPatchChunksHaveContent(op.chunks) {
return nil, structuredPatchLineError(lineNumber, "update file hunk is empty")
}
operations = append(operations, op)
default:
return nil, structuredPatchLineError(lineNumber, "expected an Add File, Delete File, or Update File header")
}
}
if len(operations) == 0 {
return nil, fmt.Errorf("structured patch contains no file operations")
}
return operations, nil
}
func structuredPatchPath(header, prefix string, line int) (string, error) {
path := strings.TrimSpace(strings.TrimPrefix(header, prefix))
if path == "" {
return "", structuredPatchLineError(line, "file path is required")
}
return path, nil
}
func isStructuredPatchHeader(line string) bool {
return structuredPatchMarker(line) == "end" || strings.HasPrefix(line, structuredAddFile) || strings.HasPrefix(line, structuredDeleteFile) || strings.HasPrefix(line, structuredUpdateFile)
}
func structuredPatchLineError(line int, message string) error {
return fmt.Errorf("invalid structured patch at line %d: %s", line, message)
}
func hasStructuredPatchContent(chunk structuredPatchChunk) bool {
return len(chunk.old) > 0 || len(chunk.new) > 0
}
func allStructuredPatchChunksHaveContent(chunks []structuredPatchChunk) bool {
for _, chunk := range chunks {
if !hasStructuredPatchContent(chunk) {
return false
}
}
return true
}
func structuredPatchOperationPaths(operations []structuredPatchOperation) []string {
seen := make(map[string]bool)
var paths []string
for _, operation := range operations {
for _, path := range []string{operation.path, operation.movePath} {
if path == "" || seen[path] {
continue
}
seen[path] = true
paths = append(paths, path)
}
}
return paths
}
func planStructuredPatch(root *os.Root, operations []structuredPatchOperation, tracker *FileTracker) ([]structuredPatchChange, error) {
seen := make(map[string]struct{})
var changes []structuredPatchChange
for _, operation := range operations {
from, err := resolveStructuredPatchTarget(root.Name(), operation.path)
if err != nil {
return nil, err
}
to := from
if operation.movePath != "" {
to, err = resolveStructuredPatchTarget(root.Name(), operation.movePath)
if err != nil {
return nil, err
}
}
targets := []structuredPatchTarget{from}
if to.absolute != from.absolute {
targets = append(targets, to)
}
for _, target := range targets {
if _, exists := seen[target.absolute]; exists {
return nil, fmt.Errorf("structured patch changes %q more than once", target.relative)
}
seen[target.absolute] = struct{}{}
}
change := structuredPatchChange{kind: operation.kind, from: from, to: to, mode: 0o644}
switch operation.kind {
case structuredPatchAdd:
if _, err := root.Lstat(to.relative); err == nil {
return nil, fmt.Errorf("cannot add %s because it already exists", to.relative)
} else if !os.IsNotExist(err) {
return nil, err
}
change.after = operation.contents
case structuredPatchDelete, structuredPatchUpdate, structuredPatchCopy:
content, info, err := readRootedFile(root, from.relative)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", from.relative, err)
}
change.mode = info.Mode()
change.beforeInfo = info
if err := tracker.CheckConflict(from.absolute, content); err != nil {
return nil, fmt.Errorf("%s", fileConflictMessage(from.relative))
}
change.before = string(content)
if operation.kind == structuredPatchDelete && operation.verifyDelete {
if err := verifyUnifiedDeletion(change.before, from.relative, operation); err != nil {
return nil, err
}
}
if operation.kind == structuredPatchUpdate || operation.kind == structuredPatchCopy {
updated, err := applyStructuredPatchUpdate(change.before, from.relative, operation.chunks)
if err != nil {
return nil, err
}
change.after = applyEOFNewline(updated, operation.eofNewline)
if operation.kind == structuredPatchCopy && from.absolute == to.absolute {
return nil, fmt.Errorf("cannot copy %s onto itself", from.relative)
}
if from.absolute != to.absolute {
if _, err := root.Lstat(to.relative); err == nil {
return nil, fmt.Errorf("cannot move %s to %s because the destination already exists", from.relative, to.relative)
} else if !os.IsNotExist(err) {
return nil, err
}
}
}
}
changes = append(changes, change)
}
return changes, nil
}
func resolveStructuredPatchTarget(root, path string) (structuredPatchTarget, error) {
// Relative traversal is rejected outright. An absolute path is allowed when
// it resolves inside the apply root (models routinely echo the absolute path
// they were shown by read_file); resolveWorkspaceTargetPath rejects anything
// that lands outside.
if path == ".." || strings.HasPrefix(filepath.ToSlash(path), "../") {
return structuredPatchTarget{}, fmt.Errorf("patch path %q must stay inside the workspace", path)
}
absolute, relative, err := resolveWorkspaceTargetPath(root, normalizePatchPathForRoot(root, path))
if err != nil {
return structuredPatchTarget{}, err
}
return structuredPatchTarget{requested: path, absolute: absolute, relative: relative}, nil
}
func applyStructuredPatchUpdate(content, path string, chunks []structuredPatchChunk) (string, error) {
lineEnding := structuredPatchLineEnding(content)
if lineEnding == "\r\n" {
content = strings.ReplaceAll(content, "\r\n", "\n")
}
lines := strings.Split(content, "\n")
trailingNewline := content != "" && len(lines) > 0 && lines[len(lines)-1] == ""
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
replacements := make([]structuredReplacement, 0, len(chunks))
lineIndex := 0
for _, chunk := range chunks {
if chunk.hasContext {
index, ambiguous := findStructuredPatchSequence(lines, []string{chunk.context}, lineIndex, false)
if ambiguous {
return "", fmt.Errorf("context %q is ambiguous in %s; provide a more specific hunk header", chunk.context, path)
}
if index < 0 {
return "", fmt.Errorf("could not find context %q in %s", chunk.context, path)
}
lineIndex = index + 1
}
if len(chunk.old) == 0 {
// A context anchor identifies the line immediately before a pure
// insertion. Only an explicit end-of-file hunk (or an unanchored
// insertion) belongs at the file end. A unified diff names the
// insertion point in its range instead; a zero-context hunk (as
// from `git diff -U0`) has nothing to verify the position against,
// so it is position-trusting by design — the same semantics git
// apply uses without fuzz. Hunks that carry context are verified.
start := lineIndex
if chunk.hasHint {
start = max(chunk.hint, lineIndex)
} else if chunk.endOfFile || !chunk.hasContext {
start = len(lines)
}
if start > len(lines) {
start = len(lines)
}
replacement, err := structuredPatchReplacement(chunk, lines, start)
if err != nil {
return "", fmt.Errorf("building replacement for %s: %w", path, err)
}
replacements = append(replacements, structuredReplacement{start: start, replacement: replacement})
lineIndex = start
continue
}
var index int
ambiguous := false
if chunk.hasHint && chunk.hint >= lineIndex && sequenceMatchesAt(lines, chunk.old, chunk.hint) {
index = chunk.hint
} else {
index, ambiguous = findStructuredPatchSequence(lines, chunk.old, lineIndex, chunk.endOfFile)
}
if ambiguous {
return "", fmt.Errorf("expected lines are ambiguous in %s; provide more surrounding context:\n%s", path, strings.Join(chunk.old, "\n"))
}
if index < 0 {
return "", fmt.Errorf("could not find expected lines in %s:\n%s", path, strings.Join(chunk.old, "\n"))
}
replacement, err := structuredPatchReplacement(chunk, lines, index)
if err != nil {
return "", fmt.Errorf("building replacement for %s: %w", path, err)
}
replacements = append(replacements, structuredReplacement{start: index, length: len(chunk.old), replacement: replacement})
lineIndex = index + len(chunk.old)
}
for index := 1; index < len(replacements); index++ {
previous := replacements[index-1]
current := replacements[index]
if current.start < previous.start+previous.length {
return "", fmt.Errorf("overlapping or out-of-order hunks in %s; provide non-overlapping context", path)
}
}
for index := len(replacements) - 1; index >= 0; index-- {
replacement := replacements[index]
lines = append(lines[:replacement.start], append(replacement.replacement, lines[replacement.start+replacement.length:]...)...)
}
if len(lines) == 0 {
return "", nil
}
updated := strings.Join(lines, lineEnding)
if trailingNewline {
updated += lineEnding
}
return updated, nil
}
// verifyUnifiedDeletion checks, byte for byte, that the lines a unified
// deletion removes are exactly the file's current content. A deletion is not
// recoverable, so none of the whitespace tolerance used to locate update
// hunks applies here; only the file's own line-ending style is normalised.
// With no hunks (git's header-only form) the file must be empty.
func verifyUnifiedDeletion(current, path string, operation structuredPatchOperation) error {
var removed []string
for _, chunk := range operation.chunks {
if len(chunk.new) > 0 {
return fmt.Errorf("deletion of %s must not add lines", path)
}
removed = append(removed, chunk.old...)
}
expected := strings.Join(removed, "\n")
if len(removed) > 0 && !operation.oldNoNewline {
expected += "\n"
}
actual := current
if structuredPatchLineEnding(current) == "\r\n" {
actual = strings.ReplaceAll(current, "\r\n", "\n")
}
if actual != expected {
if len(removed) == 0 {
return fmt.Errorf("deletion of %s expects an empty file but it has content; include the removed lines in the patch", path)
}
return fmt.Errorf("deletion of %s does not match its current content; the removed lines must equal the whole file", path)
}
return nil
}
// sequenceMatchesAt reports whether wanted appears verbatim at lines[start:].
func sequenceMatchesAt(lines, wanted []string, start int) bool {
if start < 0 || start+len(wanted) > len(lines) {
return false
}
for offset, line := range wanted {
if lines[start+offset] != line {
return false
}
}
return true
}
// applyEOFNewline forces or strips a single trailing line ending when a
// unified diff's end-of-file marker asked for it.
func applyEOFNewline(content string, mode eofNewlineMode) string {
switch mode {
case eofNewlinePresent:
if content != "" && !strings.HasSuffix(content, "\n") {
return content + structuredPatchLineEnding(content)
}
case eofNewlineAbsent:
content = strings.TrimSuffix(content, "\n")
content = strings.TrimSuffix(content, "\r")
}
return content
}
func structuredPatchReplacement(chunk structuredPatchChunk, source []string, start int) ([]string, error) {
if len(chunk.newSourceOffsets) != len(chunk.new) {
return nil, fmt.Errorf("context mapping has %d entries for %d output lines", len(chunk.newSourceOffsets), len(chunk.new))
}
replacement := append([]string(nil), chunk.new...)
for outputIndex, sourceOffset := range chunk.newSourceOffsets {
if sourceOffset < 0 {
continue
}
sourceIndex := start + sourceOffset
if sourceIndex < 0 || sourceIndex >= len(source) {
return nil, fmt.Errorf("context source index %d is outside the matched file", sourceIndex)
}
replacement[outputIndex] = source[sourceIndex]
}
return replacement, nil
}
func structuredPatchLineEnding(content string) string {
if strings.Contains(content, "\r\n") && !strings.Contains(strings.ReplaceAll(content, "\r\n", ""), "\n") {
return "\r\n"
}
return "\n"
}
type structuredReplacement struct {
start int
length int
replacement []string
}
func findStructuredPatchSequence(lines, wanted []string, start int, endOfFile bool) (int, bool) {
if len(wanted) == 0 {
return start, false
}
if len(wanted) > len(lines) {
return -1, false
}
searchStart := start
if endOfFile {
searchStart = len(lines) - len(wanted)
if searchStart < start {
searchStart = start
}
}
for _, equal := range []func(string, string) bool{
func(left, right string) bool { return left == right },
func(left, right string) bool {
return strings.TrimRight(left, " \t") == strings.TrimRight(right, " \t")
},
func(left, right string) bool { return strings.TrimSpace(left) == strings.TrimSpace(right) },
} {
match := -1
for index := searchStart; index+len(wanted) <= len(lines); index++ {
matched := true
for offset, line := range wanted {
if !equal(lines[index+offset], line) {
matched = false
break
}
}
if matched {
if match >= 0 {
return -1, true
}
match = index
}
}
if match >= 0 {
return match, false
}
}
return -1, false
}
type structuredPatchApplyOutcome struct {
committed []structuredPatchChange
incompletePaths []string
}
type structuredPatchChangeOutcome struct {
completed []structuredPatchChange
incompletePaths []string
}
func applyStructuredPatchChanges(root *os.Root, relativeRoot string, changes []structuredPatchChange, tracker *FileTracker) (structuredPatchApplyOutcome, error) {
// committed lists, in order, the paths whose change reached disk before a
// later change failed, so the caller (and the model) knows exactly which
// files now hold the patched content and which were never touched.
var outcome structuredPatchApplyOutcome
for _, change := range changes {
changeOutcome, err := applyStructuredPatchChange(root, change)
outcome.committed = append(outcome.committed, changeOutcome.completed...)
outcome.incompletePaths = append(outcome.incompletePaths, changeOutcome.incompletePaths...)
if err != nil {
forgetStructuredPatchFiles(tracker, changes)
committedPaths := changedFilesFromStructuredPatch(relativeRoot, outcome.committed)
unverifiedPaths := appendUniqueStructuredPatchPaths(nil, relativeRoot, outcome.incompletePaths)
if len(committedPaths) > 0 || len(unverifiedPaths) > 0 {
parts := []string{"patch was partially applied"}
if len(committedPaths) > 0 {
parts = append(parts, "already committed: "+strings.Join(committedPaths, ", "))
}
if len(unverifiedPaths) > 0 {
parts = append(parts, "published but unverified: "+strings.Join(unverifiedPaths, ", "))
}
parts = append(parts, "the remaining files are unchanged")
if len(unverifiedPaths) > 0 {
parts = append(parts, "re-read the listed files before retrying")
} else {
parts = append(parts, "re-read the committed files before retrying")
}
return outcome, fmt.Errorf("%w; %s", err, strings.Join(parts, "; "))
}
return outcome, err
}
}
return outcome, nil
}
// completedStructuredPatchEffect turns a partially completed compound change
// into the exact filesystem sub-effect that is still verifiable after the
// error. In particular, a failed move may have published its destination while
// leaving the source in place; that is a destination creation, not a move.
func completedStructuredPatchEffect(root *os.Root, change structuredPatchChange) (structuredPatchChange, bool) {
if change.to.relative == "" {
return structuredPatchChange{}, false
}
content, _, err := readRootedFile(root, change.to.relative)
if err != nil || string(content) != change.after {
return structuredPatchChange{}, false
}
switch change.kind {
case structuredPatchAdd, structuredPatchCopy:
return structuredPatchChange{
kind: structuredPatchAdd,
to: change.to,
after: change.after,
mode: change.mode,
}, true
case structuredPatchUpdate:
if change.from.absolute != change.to.absolute {
return structuredPatchChange{
kind: structuredPatchAdd,
to: change.to,
after: change.after,
mode: change.mode,
}, true
}
}
return structuredPatchChange{}, false
}
func incompleteStructuredPatchWrite(root *os.Root, change structuredPatchChange, published bool) structuredPatchChangeOutcome {
if !published {
return structuredPatchChangeOutcome{}
}
if completed, ok := completedStructuredPatchEffect(root, change); ok {
return structuredPatchChangeOutcome{completed: []structuredPatchChange{completed}}
}
if change.to.relative != "" {
return structuredPatchChangeOutcome{incompletePaths: []string{change.to.relative}}
}
return structuredPatchChangeOutcome{}
}
func appendUniqueStructuredPatchPaths(existing []string, relativeRoot string, paths []string) []string {
seen := make(map[string]bool, len(existing)+len(paths))
for _, path := range existing {
seen[path] = true
}
for _, path := range paths {
if relativeRoot != "" && relativeRoot != "." {
path = filepath.ToSlash(filepath.Join(relativeRoot, path))
}
if path != "" && !seen[path] {
seen[path] = true
existing = append(existing, path)
}
}
return existing
}
func forgetStructuredPatchFiles(tracker *FileTracker, changes []structuredPatchChange) {
if tracker == nil {
return
}
for _, change := range changes {
tracker.Forget(change.from.absolute)
if change.to.absolute != change.from.absolute {
tracker.Forget(change.to.absolute)
}
}
}
// structuredPatchBeforeCommit, when set, runs before each change's pre-commit
// recheck. Tests use it to alter the filesystem between planning and commit
// deterministically; it is nil in production.
var structuredPatchBeforeCommit func(change structuredPatchChange)
// structuredPatchBeforeRename runs after a same-path update has staged and
// closed its replacement but before the final preimage recheck and rename.
// Tests use it to reproduce a competing writer deterministically.
var structuredPatchBeforeRename func(change structuredPatchChange)
// structuredPatchRemove is a deterministic test seam for failures after a
// move destination has been published. Production removes through the opened
// workspace root.
var structuredPatchRemove = func(root *os.Root, name string) error { return root.Remove(name) }
func applyStructuredPatchChange(root *os.Root, change structuredPatchChange) (structuredPatchChangeOutcome, error) {
if structuredPatchBeforeCommit != nil {
structuredPatchBeforeCommit(change)
}
// The planner validated the source against the bytes it read; re-read
// through the same root immediately before committing so a change made
// by another process in between is refused rather than overwritten or
// removed.
if change.kind != structuredPatchAdd {
if err := recheckStructuredPatchPreimage(root, change); err != nil {
return structuredPatchChangeOutcome{}, err
}
}
switch change.kind {
case structuredPatchDelete:
if err := structuredPatchRemove(root, change.from.relative); err != nil {
return structuredPatchChangeOutcome{}, fmt.Errorf("deleting %s: %w", change.from.relative, err)
}
return structuredPatchChangeOutcome{completed: []structuredPatchChange{change}}, nil
case structuredPatchAdd, structuredPatchCopy:
var beforePublish func() error
if change.kind == structuredPatchCopy {
beforePublish = structuredPatchPrePublishGuard(root, change)
}
published, err := writeStructuredPatchFile(root, change.to, change.after, change.mode, true, beforePublish)
if err != nil {
return incompleteStructuredPatchWrite(root, change, published), err
}
return structuredPatchChangeOutcome{completed: []structuredPatchChange{change}}, nil
case structuredPatchUpdate:
moving := change.from.absolute != change.to.absolute
published, err := writeStructuredPatchFile(root, change.to, change.after, change.mode, moving, structuredPatchPrePublishGuard(root, change))
if err != nil {
return incompleteStructuredPatchWrite(root, change, published), err
}
if moving {
if err := structuredPatchRemove(root, change.from.relative); err != nil {
return incompleteStructuredPatchWrite(root, change, true), fmt.Errorf("removing moved source %s: %w", change.from.relative, err)
}
}
return structuredPatchChangeOutcome{completed: []structuredPatchChange{change}}, nil
}
return structuredPatchChangeOutcome{}, fmt.Errorf("unsupported structured patch operation")
}
func recheckStructuredPatchPreimage(root *os.Root, change structuredPatchChange) error {
current, info, err := readRootedFile(root, change.from.relative)
if err != nil {
return fmt.Errorf("re-reading %s before commit: %w", change.from.relative, err)
}
if (change.beforeInfo != nil && !os.SameFile(change.beforeInfo, info)) || string(current) != change.before {
return fmt.Errorf("%s changed on disk between planning and commit; re-read it and retry", change.from.relative)
}
return nil
}
func structuredPatchPrePublishGuard(root *os.Root, change structuredPatchChange) func() error {
return func() error {
if change.kind == structuredPatchUpdate && change.from.absolute == change.to.absolute && structuredPatchBeforeRename != nil {
structuredPatchBeforeRename(change)
}
return recheckStructuredPatchPreimage(root, change)
}
}
func writeStructuredPatchFile(root *os.Root, target structuredPatchTarget, content string, mode os.FileMode, createOnly bool, beforePublish func() error) (bool, error) {
parent := filepath.Dir(target.relative)
if err := root.MkdirAll(parent, 0o755); err != nil {
return false, fmt.Errorf("creating parent directory for %s: %w", target.relative, err)
}
temp, tempName, err := pathjail.CreateTemp(root, parent, "zero-patch", ".tmp")
if err != nil {
return false, fmt.Errorf("writing %s: %w", target.relative, err)
}
defer func() { _ = removeStructuredPatchTemp(root, tempName) }()
if _, err := temp.WriteString(content); err != nil {
_ = temp.Close()
return false, fmt.Errorf("writing %s: %w", target.relative, err)
}
if err := temp.Chmod(mode.Perm()); err != nil {
_ = temp.Close()
return false, fmt.Errorf("writing %s: %w", target.relative, err)
}
if err := temp.Close(); err != nil {
return false, fmt.Errorf("writing %s: %w", target.relative, err)
}
if beforePublish != nil {
if err := beforePublish(); err != nil {
return false, err
}
}
if createOnly {
return structuredPatchPublishNoReplace(root, tempName, target.relative, mode)
}
if err := root.Rename(tempName, target.relative); err != nil {
return false, fmt.Errorf("writing %s: %w", target.relative, err)
}
return true, nil
}
func publishStructuredPatchNoReplace(root *os.Root, source, target string, mode os.FileMode) (bool, error) {
return publishStructuredPatchNoReplaceWith(root, source, target, mode, root.Link)
}
// structuredPatchPublishNoReplace is a deterministic test seam for failures
// after a create-only target has become visible. Production uses the atomic
// no-replace publisher above.
var structuredPatchPublishNoReplace = publishStructuredPatchNoReplace
func publishStructuredPatchNoReplaceWith(root *os.Root, source, target string, mode os.FileMode, link func(string, string) error) (bool, error) {
if linkErr := link(source, target); linkErr == nil {
cleanupErr := removeStructuredPatchTemp(root, source)
chmodErr := root.Chmod(target, mode.Perm())
if cleanupErr != nil || chmodErr != nil {
return true, fmt.Errorf("publishing %s: %w", target, errors.Join(cleanupErr, chmodErr))
}
return true, nil
} else if errors.Is(linkErr, os.ErrExist) {
return false, fmt.Errorf("creating %s: %w", target, linkErr)
} else {
committed, copyErr := copyStructuredPatchNoReplace(root, source, target, mode)
if copyErr != nil {
return committed, fmt.Errorf("creating %s without overwrite: hard link failed: %w; exclusive-copy fallback failed: %w", target, linkErr, copyErr)
}
return committed, nil
}
}
func copyStructuredPatchNoReplace(root *os.Root, source, target string, mode os.FileMode) (bool, error) {
return copyStructuredPatchNoReplaceWith(root, source, target, mode, io.Copy, removeStructuredPatchTemp)
}
func copyStructuredPatchNoReplaceWith(
root *os.Root,
source, target string,
mode os.FileMode,
copyFile func(io.Writer, io.Reader) (int64, error),
cleanup func(*os.Root, string) error,
) (bool, error) {
sourceFile, err := root.Open(source)
if err != nil {
return false, err
}
sourceClosed := false
defer func() {
if !sourceClosed {
_ = sourceFile.Close()
}
}()
targetFile, err := root.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return false, err
}
committed := true
cleanupTarget := func(cause error) (bool, error) {
_ = targetFile.Close()
if cleanupErr := cleanup(root, target); cleanupErr == nil {
return false, cause
} else {
return committed, errors.Join(cause, fmt.Errorf("cleaning partial target: %w", cleanupErr))
}
}
if _, err := copyFile(targetFile, sourceFile); err != nil {
return cleanupTarget(err)
}
if err := sourceFile.Close(); err != nil {
sourceClosed = true
return cleanupTarget(err)
}
sourceClosed = true
if err := targetFile.Chmod(mode.Perm()); err != nil {
return cleanupTarget(err)
}
if err := targetFile.Close(); err != nil {
if cleanupErr := cleanup(root, target); cleanupErr == nil {
return false, err
} else {
return true, errors.Join(err, fmt.Errorf("cleaning target after close failure: %w", cleanupErr))
}
}
if err := cleanup(root, source); err != nil {
return true, fmt.Errorf("removing published source: %w", err)
}
return true, nil
}
func removeStructuredPatchTemp(root *os.Root, name string) error {
chmodErr := root.Chmod(name, 0o600)
if os.IsNotExist(chmodErr) {
return nil
}
removeErr := root.Remove(name)
if os.IsNotExist(removeErr) {
removeErr = nil
}
if removeErr == nil {
return nil
}
return errors.Join(chmodErr, removeErr)
}
func recordStructuredPatchFile(tracker *FileTracker, absolute string, created, seenWhole bool) {
if tracker == nil {
return
}
content, err := os.ReadFile(absolute)
if err != nil {
tracker.Forget(absolute)
return
}
info, _ := os.Stat(absolute)
tracker.Record(absolute, content, info)
if seenWhole {
lines := trackedLineTotal(string(content))
tracker.RecordSeenRange(absolute, 1, lines, lines)
}
if created {
tracker.RecordCreated(absolute)
}
}
func changedFilesFromStructuredPatch(relativeRoot string, changes []structuredPatchChange) []string {
seen := make(map[string]bool)
var paths []string
for _, change := range changes {
var targets []structuredPatchTarget
switch change.kind {
case structuredPatchDelete:
targets = []structuredPatchTarget{change.from}
case structuredPatchUpdate:
targets = []structuredPatchTarget{change.to}
if change.from.absolute != change.to.absolute {
targets = append([]structuredPatchTarget{change.from}, targets...)
}
default:
// Adds and copies mutate only their destination; the copy source is
// evidence for the operation, not a changed file.
targets = []structuredPatchTarget{change.to}
}
for _, target := range targets {
path := target.relative
if relativeRoot != "" && relativeRoot != "." {
path = filepath.ToSlash(filepath.Join(relativeRoot, path))
}
if !seen[path] {
seen[path] = true
paths = append(paths, path)
}
}
}
return paths
}
func structuredPatchPreview(changes []structuredPatchChange) string {
var preview strings.Builder
for _, change := range changes {
path := change.to.relative
if change.kind == structuredPatchDelete {
path = change.from.relative
}
before, after := change.before, change.after
if change.kind == structuredPatchDelete {
after = ""
}
diff := boundedUnifiedDiff(path, before, after)
if diff == "" {
continue
}
if preview.Len()+len(diff)+1 > maxToolPreviewBytes {
break
}
if preview.Len() > 0 {
preview.WriteByte('\n')
}
preview.WriteString(diff)
}
return capPreviewDiff(preview.String())
}