package tools import ( "errors" "fmt" "io" "os" "path/filepath" "regexp" "strings" "github.com/Gitlawb/zero/internal/pathjail" "github.com/Gitlawb/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 } // 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) } } } if err := applyStructuredPatchChanges(workspace, changes, options.FileTracker); err != nil { return errorResult("Error applying patch: " + err.Error()) } 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.Display = Display{Summary: summary, Kind: "diff", Preview: structuredPatchPreview(changes)} return result } 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: info, err := root.Stat(from.relative) if err != nil { return nil, fmt.Errorf("stating %s: %w", from.relative, err) } change.mode = info.Mode() content, err := root.ReadFile(from.relative) if err != nil { return nil, fmt.Errorf("reading %s: %w", from.relative, err) } 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 } func applyStructuredPatchChanges(root *os.Root, changes []structuredPatchChange, tracker *FileTracker) 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 committed []string for _, change := range changes { done, err := applyStructuredPatchChange(root, change) if done { committed = append(committed, structuredPatchChangePaths(change)...) } if err != nil { forgetStructuredPatchFiles(tracker, changes) if len(committed) > 0 { return fmt.Errorf("%w; patch was partially applied — already committed: %s; the remaining files are unchanged; re-read the committed files before retrying", err, strings.Join(committed, ", ")) } return err } } return nil } // structuredPatchChangePaths names the workspace-relative paths a committed // change touched: the destination, plus the source of a move or copy. func structuredPatchChangePaths(change structuredPatchChange) []string { if change.kind == structuredPatchDelete { return []string{change.from.relative} } paths := []string{change.to.relative} if change.from.absolute != change.to.absolute && change.from.relative != "" { paths = append([]string{change.from.relative}, paths...) } return paths } 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) func applyStructuredPatchChange(root *os.Root, change structuredPatchChange) (bool, 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 { current, err := root.ReadFile(change.from.relative) if err != nil { return false, fmt.Errorf("re-reading %s before commit: %w", change.from.relative, err) } if string(current) != change.before { return false, fmt.Errorf("%s changed on disk between planning and commit; re-read it and retry", change.from.relative) } } switch change.kind { case structuredPatchDelete: if err := root.Remove(change.from.relative); err != nil { return false, fmt.Errorf("deleting %s: %w", change.from.relative, err) } return true, nil case structuredPatchAdd, structuredPatchCopy: return writeStructuredPatchFile(root, change.to, change.after, change.mode, true) case structuredPatchUpdate: moving := change.from.absolute != change.to.absolute committed, err := writeStructuredPatchFile(root, change.to, change.after, change.mode, moving) if err != nil { return committed, err } if moving { if err := root.Remove(change.from.relative); err != nil { return true, fmt.Errorf("removing moved source %s: %w", change.from.relative, err) } } return true, nil } return false, fmt.Errorf("unsupported structured patch operation") } func writeStructuredPatchFile(root *os.Root, target structuredPatchTarget, content string, mode os.FileMode, createOnly bool) (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 createOnly { return publishStructuredPatchNoReplace(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) } 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 { targets := []structuredPatchTarget{change.to} if change.from.absolute != change.to.absolute { targets = append([]structuredPatchTarget{change.from}, targets...) } 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()) }