Applying Patches

How a hunk is placed

A hunk records the line it expects to start at and the surrounding context. patchapply builds two blocks from the hunk — the lines it expects to find (context + deletions) and the lines it writes (context + additions) — then locates the old block in the file.

  • Offset is tolerated. The search starts at the recorded line and expands outward, so a patch still applies when earlier edits moved the target up or down.
  • Context is exact. The surrounding lines must match byte-for-byte. There is no fuzz factor that drops context to force a match — if the context changed, you get ErrConflict, not a misapplied hunk.
  • Hunks apply in order, each after the previous one's position, so a file with several hunks can't have them cross.

Change types

The action comes from FileChange.Type (set by go-mailpatch):

TypeWhat patchapply doesStatus
Addedwrite NewPath (errors if it exists)Created
Modifiedread, apply hunks, write backUpdated
Deletedremove OldPath (errors if absent)Removed
Renamedapply hunks, write NewPath, remove OldPathRenamed
Copiedapply hunks, write NewPath, keep OldPathCreated

Transactional application

Phase 1 reads each file and computes its new contents entirely in memory. Only if every file and every hunk succeeds does phase 2 write — writes first, then removals, so a rename's destination exists before its source is deleted. A conflict in the last file leaves the first untouched.

Filesystem-free: ApplyToBytes

When you already hold the content, skip the FS entirely:

newContent, err := patchapply.ApplyToBytes(oldContent, fileChange)
if errors.Is(err, patchapply.ErrConflict) {
	// this file's hunks don't fit oldContent
}

ApplyToBytes is the core every other entry point is built on.