patchapply

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 9 Imported by: 0

README

go-patchapply

Apply parsed patches to files, in Go. Add / modify / delete / rename, reverse, dry-run — confined to a directory, transactional, never runs git.

Go Version Go Reference GitHub release (latest by date) CI

go-patchapply is the apply half of go-mailpatch. mailpatch turns a git format-patch email into structured FileChanges; patchapply takes those — or a bare unified diff — and writes the changes to a filesystem: creating, modifying, deleting, renaming, and copying files. It can reverse a patch and dry-run one. It never executes git.

It grew out of matcha's git-mail feature, where applying a mailed patch must be safe against hostile paths and must not half-apply.

Features

  • Every change type. Added, deleted, modified, renamed, copied — driven straight off mailpatch.FileChange.
  • Offset-tolerant, exact-context hunks. A hunk still applies when earlier edits shifted the file; context lines must match (no silent fuzz), and a hunk that can't be placed fails with ErrConflict.
  • Transactional. Every file is read and every hunk placed in memory first. If any hunk fails, nothing is written — no half-applied trees.
  • Confined by construction. DirFS pins every path inside a root; a patch that tries to escape via ../ or an absolute path fails with ErrUnsafePath before touching disk.
  • Reverse & dry-run. Options{Reverse: true} unapplies; Options{DryRun: true} validates the whole patch and writes nothing.
  • Apply anywhere. Run against the OS (DirFS), entirely in memory (MemFS), or your own FS (a git object store, a virtual tree…).
  • Zero third-party deps beyond go-mailpatch. Standard library otherwise.

Install

go get github.com/floatpane/go-patchapply

Requires Go 1.26+.

Usage

Apply a patch email to a directory
package main

import (
	"log"
	"os"

	"github.com/floatpane/go-mailpatch"
	"github.com/floatpane/go-patchapply"
)

func main() {
	raw, _ := os.ReadFile("fix.patch")

	p, err := mailpatch.ParseBytes(raw) // parse the format-patch email
	if err != nil {
		log.Fatal(err)
	}

	fsys := patchapply.NewDirFS("/path/to/repo") // confined to this root
	res, err := patchapply.ApplyPatch(fsys, p, nil)
	if err != nil {
		log.Fatal(err) // ErrConflict, ErrUnsafePath, ErrMissing, ErrExists
	}

	for _, f := range res.Files {
		log.Printf("%s %s", f.Status, f.Path) // updated/created/removed/renamed
	}
}
Apply a bare diff in memory
fsys := patchapply.NewMemFS(map[string][]byte{
	"greet.txt": []byte("hello\nworld\nbye\n"),
})

_, err := patchapply.ApplyDiff(fsys, diffText, nil)

out, _ := fsys.ReadFile("greet.txt")
Dry-run, then reverse
// Validate without writing — nil error means a real apply would succeed.
if _, err := patchapply.Apply(fsys, files, &patchapply.Options{DryRun: true}); err != nil {
	log.Fatal("patch will not apply cleanly:", err)
}

// Apply, then later undo.
patchapply.Apply(fsys, files, nil)
patchapply.Apply(fsys, files, &patchapply.Options{Reverse: true})
Just transform bytes
// No filesystem at all: apply one file's hunks to content you hold.
newContent, err := patchapply.ApplyToBytes(oldContent, fileChange)

API at a glance

Function Does
Apply(fs, []FileChange, *Options) apply already-parsed changes
ApplyDiff(fs, diff, *Options) parse a bare diff (via go-mailpatch) and apply
ApplyPatch(fs, *mailpatch.Patch, *Options) apply a parsed patch email
ApplyToBytes(orig, FileChange) pure, filesystem-free single-file apply
NewDirFS(root) / NewMemFS(seed) the two built-in FS implementations

Options{Reverse, DryRun}. Errors: ErrConflict, ErrUnsafePath, ErrMissing, ErrExists (compare with errors.Is).

What this is not

  • Not a merge tool. No 3-way merge, no conflict markers. A hunk that doesn't apply is an error, not a <<<<<<<.
  • Not git am. It writes files; it does not create commits, move HEAD, or touch the index.
  • Not a fuzzy patcher. It tolerates line offset, not changed context. That's deliberate — applying a security-relevant patch to the wrong place silently is worse than failing.

[!NOTE] End-of-file newline state (\ No newline at end of file) is not tracked; a file produced from an addition ends with a newline.

Documentation

Full API reference: pkg.go.dev/github.com/floatpane/go-patchapply

Guides and diagrams: see docs/.

Sister projects

Project Role
floatpane/go-mailpatch The parser half — turns format-patch email into the FileChanges this library applies.
floatpane/matcha Reference consumer — git-mail patch apply.
floatpane/go-secretbox Sibling extraction — password-based encryption for data at rest.

Contributing

PRs welcome. See CONTRIBUTING.md.

Security

It writes files from untrusted patches. Path-confinement matters — report vulnerabilities privately via SECURITY.md.

License

MIT. See LICENSE.

Documentation

Overview

Package patchapply applies parsed patches to files.

It is the apply half of go-mailpatch: mailpatch turns a format-patch email into structured FileChanges; patchapply takes those (or a bare unified diff) and writes the changes to a filesystem — creating, modifying, deleting, renaming, and copying files. It can also reverse a patch and dry-run one without writing.

Apply runs against an FS, not directly against the OS. Use DirFS to confine every path to a directory (patches that try to escape via "../" fail with ErrUnsafePath), or MemFS to apply entirely in memory.

Application is transactional: every file is read and every hunk is placed in memory first, so if any hunk fails to apply (ErrConflict) nothing is written.

It never executes git.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrConflict is returned (wrapped, with the file and hunk) when a hunk's
	// context cannot be matched in the target file.
	ErrConflict = errors.New("patchapply: hunk does not apply")
	// ErrUnsafePath is returned by DirFS when a patch path escapes the root.
	ErrUnsafePath = errors.New("patchapply: unsafe path")
	// ErrExists is returned when a patch adds a file that is already present.
	ErrExists = errors.New("patchapply: file already exists")
	// ErrMissing is returned when a patch modifies, deletes, or renames a file
	// that is not present.
	ErrMissing = errors.New("patchapply: target file not found")
)

Sentinel errors. Compare with errors.Is.

Functions

func ApplyToBytes

func ApplyToBytes(orig []byte, f mailpatch.FileChange) ([]byte, error)

ApplyToBytes applies a single file's hunks to orig and returns the new contents. It is the pure, filesystem-free core: read a file yourself, pass its bytes here, write the result yourself.

Hunks are located at the line numbers the diff records, but a whole-hunk offset is tolerated (so a patch still applies when earlier, unrelated edits shifted the file). Context lines must match exactly — there is no fuzz. A hunk that cannot be placed returns an error wrapping ErrConflict.

Types

type DirFS

type DirFS struct {
	// contains filtered or unexported fields
}

DirFS is an FS rooted at a directory on the real filesystem. Every path is confined to the root: a patch whose path escapes it (via "../" or an absolute path) fails with ErrUnsafePath instead of touching anything outside.

func NewDirFS

func NewDirFS(dir string) *DirFS

NewDirFS returns a DirFS rooted at dir.

func (*DirFS) Exists

func (d *DirFS) Exists(name string) bool

func (*DirFS) ReadFile

func (d *DirFS) ReadFile(name string) ([]byte, error)

func (*DirFS) Remove

func (d *DirFS) Remove(name string) error

func (*DirFS) WriteFile

func (d *DirFS) WriteFile(name string, data []byte, perm fs.FileMode) error

type FS

type FS interface {
	// ReadFile returns the current contents of name. It must report a
	// not-exist error (testable with os.IsNotExist / fs.ErrNotExist) when the
	// file is absent.
	ReadFile(name string) ([]byte, error)
	// WriteFile creates or overwrites name with data, creating parent
	// directories as needed.
	WriteFile(name string, data []byte, perm fs.FileMode) error
	// Remove deletes name.
	Remove(name string) error
	// Exists reports whether name currently exists.
	Exists(name string) bool
}

FS is the filesystem an apply runs against: read the current files, write new content, and remove deletions. DirFS (rooted at a directory) and MemFS (in memory) implement it. Implement it yourself to apply patches against, say, a git object store or a virtual tree.

type FileResult

type FileResult struct {
	Path    string // resulting path; for a removal, the path removed
	OldPath string // previous path for a rename, else empty
	Status  Status
	Hunks   int // hunks applied
}

FileResult records what an apply did (or, for a dry run, would do) to one file.

type MemFS

type MemFS struct {
	// contains filtered or unexported fields
}

MemFS is an in-memory FS, handy for tests and for applying a patch to a set of files you already hold in memory. The zero value is not usable; use NewMemFS.

func NewMemFS

func NewMemFS(initial map[string][]byte) *MemFS

NewMemFS returns an empty MemFS. Seed it with WriteFile or by passing initial files.

func (*MemFS) Exists

func (m *MemFS) Exists(name string) bool

func (*MemFS) Files

func (m *MemFS) Files() map[string][]byte

Files returns a snapshot of the current contents, keyed by clean path.

func (*MemFS) ReadFile

func (m *MemFS) ReadFile(name string) ([]byte, error)

func (*MemFS) Remove

func (m *MemFS) Remove(name string) error

func (*MemFS) WriteFile

func (m *MemFS) WriteFile(name string, data []byte, _ fs.FileMode) error

type Options

type Options struct {
	// Reverse undoes the patch instead of applying it.
	Reverse bool
	// DryRun validates the whole patch (reads and places every hunk) but
	// writes nothing. A nil error means a real apply would succeed.
	DryRun bool
}

Options tunes an apply.

type PathError

type PathError struct {
	Path string
	Err  error
}

PathError annotates an error with the offending path.

func (*PathError) Error

func (e *PathError) Error() string

func (*PathError) Unwrap

func (e *PathError) Unwrap() error

type Result

type Result struct {
	Files []FileResult
}

Result is the outcome of an apply.

func Apply

func Apply(fsys FS, files []mailpatch.FileChange, opts *Options) (*Result, error)

Apply applies files to fsys. opts may be nil.

func ApplyDiff

func ApplyDiff(fsys FS, diff string, opts *Options) (*Result, error)

ApplyDiff parses a bare unified diff and applies it to fsys.

func ApplyPatch

func ApplyPatch(fsys FS, p *mailpatch.Patch, opts *Options) (*Result, error)

ApplyPatch applies a parsed format-patch email (its FileChanges) to fsys.

type Status

type Status int

Status is what happened to a file.

const (
	// Created means the file was written and did not exist before.
	Created Status = iota
	// Updated means an existing file's contents changed.
	Updated
	// Removed means the file was deleted.
	Removed
	// Renamed means the file moved (and possibly changed).
	Renamed
)

func (Status) String

func (s Status) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL