Help us shape the future of Go! Take the 2026 Go Developer Survey

diffutils

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: BSD-3-Clause, MIT Imports: 9 Imported by: 0

README

go-diffutils

Canonical types and algorithms for computing and representing text diffs in Go.

The root package compares string slices (DiffLines) and produces the canonical interchange types DiffContent, DiffChange and LineRange, using a port of Python's difflib SequenceMatcher. Two adapter modules bridge major Go diff libraries.

Modules

Module Import path What it is
root github.com/mikeschinkel/go-diffutils Canonical diff types, DiffLines, and the difflib engine (SequenceMatcher, unified and context diffs)
dmp github.com/mikeschinkel/go-diffutils/dmp An InlineHighlighter using diff-match-patch (via sergi/go-diff) for character-level highlighting
sgdiff github.com/mikeschinkel/go-diffutils/sgdiff Parses unified diff text into DiffContent (via sourcegraph/go-diff)

Each is a separate Go module with its own version. Install the ones you use:

go get github.com/mikeschinkel/go-diffutils
go get github.com/mikeschinkel/go-diffutils/dmp
go get github.com/mikeschinkel/go-diffutils/sgdiff

Root is tagged vX.Y.Z; dmp and sgdiff are tagged dmp/vX.Y.Z and sgdiff/vX.Y.Z. All three are provisional as of v0.1.0: the API may change in minor releases.

Local Development

dmp and sgdiff require a published version of the root module and carry no replace directive, because a consumer never sees a replace — only the require — and a replace would let a wrong require go unnoticed.

To build every module from the code on disk, create a Go workspace:

just work

just work writes a go.work listing every directory that holds a go.mod, so it needs no edit when a module is added. Re-run it after adding one.

go.work is not committed (it is in .gitignore). CI builds and tests with GOWORK=off, against the versions each go.mod requires, so a change in dmp or sgdiff that depends on an unpublished root change fails in CI until that root version is tagged and required. To see what CI sees locally, prefix a command with GOWORK=off:

cd dmp && GOWORK=off go test ./...

just tidy runs go mod tidy in every module with GOWORK=off, and CI fails when any go.mod is not tidy. Run just to list the other recipes (test, lint, vet, build, ci, …).

Releasing

Releases are made by the Release GitHub Actions workflow: choose the module (root, dmp or sgdiff) and the version. It refuses a module whose go.mod declares the wrong path or carries a replace, or whose tag already exists, runs that module's tests, then tags and publishes it.

License

MIT; see LICENSE.

difflib.go is derived from pmezard/go-difflib, Copyright (c) 2013 Patrick Mezard, and is used under its BSD 3-Clause license, whose full text is included in LICENSE. Thanks to Patrick Mezard for the port of Python's difflib it is built on.

Documentation

Overview

Package diffutils provides canonical types and algorithms for computing and representing text diffs in Go.

The core diff engine is based on pmezard/go-difflib (BSD 3-Clause licensed; see LICENSE), which implements a port of Python's difflib SequenceMatcher. This package absorbs that code and adds:

Adapter sub-packages bridge major Go diff libraries:

Stability

This package is provisional as of v0.1.0. The public API may change in minor releases until promoted to stable.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrMissingSentinel     = errors.New("missing required sentinel error")
	ErrTrailingKey         = errors.New("trailing key without value")
	ErrMisplacedError      = errors.New("error in wrong position")
	ErrInvalidArgumentType = errors.New("invalid argument type")
	ErrOddKeyValueCount    = errors.New("odd number of key-value arguments")
	ErrCrossPackageError   = errors.New("error from different doterr package")
)

Sentinel errors for validation failures

View Source
var (
	// ErrDiffUtils is the layer sentinel for all diffutils errors.
	ErrDiffUtils = errors.New("diffutils")

	// ErrEmptyInput indicates that both old and new inputs are empty.
	ErrEmptyInput = errors.New("empty input")

	// ErrNilContent indicates a nil DiffContent was passed.
	ErrNilContent = errors.New("nil content")
)

Functions

func AppendErr

func AppendErr(errs []error, err error) []error

func CombineErrs

func CombineErrs(errs []error) error

CombineErrs bundles a slice of errors into a single composite error that unwraps to its members. Order is preserved and nils are skipped. Returns nil for an empty/fully-nil slice, or the sole error when there is exactly one.

func ErrValue

func ErrValue[T any](err error, key string) (T, bool)

ErrValue extracts a single metadata value by key with type safety. Returns the value and true if found and the value is of type T. Returns the zero value of T and false if not found or type mismatch.

Example:

status, ok := ErrValue[int](err, "http_status")
if ok {
    fmt.Printf("Status: %d\n", status)
}

name, ok := ErrValue[string](err, "parameter_name")

func Errors

func Errors(err error) []error

Errors returns the errors stored on a doterr entry. If err is a doterr entry, returns its errors. If err is a joined error (has Unwrap() []error), scans immediate children left-to-right and returns errors from the first doterr entry found. Otherwise returns nil. The returned slice preserves insertion order and is a copy.

Note: These errors may be sentinel errors (e.g., ErrRepo), custom error types (e.g., *rfc9457.Error), or any other error type stored in the entry.

func FindErr

func FindErr[T error](err error) (out T, ok bool)

FindErr walks an error tree (including errors.Join trees) and returns the first match for target (via errors.As).

func GetContextDiffString

func GetContextDiffString(diff ContextDiff) (string, error)

GetContextDiffString returns a context diff as a string.

func GetUnifiedDiffString

func GetUnifiedDiffString(diff UnifiedDiff) (string, error)

GetUnifiedDiffString returns a unified diff as a string.

func MsgErr

func MsgErr(msg any) error

MsgErr creates an ad-hoc error message without requiring a sentinel error. This is a convenience for rapid development - use sentinels for production code.

Accepts two forms:

  • MsgErr("message") - creates error with the given message
  • MsgErr(err) - wraps existing error, preserving error chain for errors.Is()

Use this when you don't want to define a sentinel immediately. Future tooling can detect MsgErr usage and suggest/generate appropriate sentinel errors.

For errors with metadata, use NewErr with a sentinel, or wrap with WithErr:

err := MsgErr("config invalid")
err = WithErr(err, "path", configPath)

func NewErr

func NewErr(parts ...any) error

NewErr builds a standalone structured entry (no primary cause inside). Accepted parts:

  • error — sentinel/tag (required: at least one, must be first)
  • KV{Key,Value} — explicit key/value
  • "key", value — implicit pair (value can be any type, including error)
  • error — optional trailing cause (joined last via errors.Join)

Pattern: one or more sentinels (error), then zero or more key-value pairs, then optional trailing cause (error). After the first string key, all remaining args must form valid pairs, except for an optional final error. Returns nil if no meaningful parts are provided after validation. Returns a validation error joined with the partial entry if validation fails.

func SplitLines

func SplitLines(s string) []string

SplitLines splits a string on "\n" while preserving them. The output can be used as input for UnifiedDiff and ContextDiff.

func WithErr

func WithErr(parts ...any) error

WithErr is a flexible enrichment helper. Typical uses:

// Enrich an existing composite error (err may be an errors.Join tree):
err = WithErr(err, "Foo", 10)

// Build an entry and join a trailing cause in one shot:
err = WithErr("endpoint", ep, ErrTemplate, cause) // 'cause' is last

Behavior:

  1. If the FIRST arg is an error, it is treated as the base error to enrich: • If it is a doterr entry, merge KVs/sentinels into that entry. • If it is a multi-unwrap (errors.Join tree), find the RIGHTMOST doterr entry, merge into it, and rebuild preserving order. • If no doterr entry is found, a new entry will be joined in (see step 3).

  2. After consuming the base (if present), if the LAST remaining arg is an error, it is treated as the CAUSE and joined LAST.

  3. The remaining middle args (if any) are collected into an entry. If we enriched an existing doterr entry in step 1, that merged entry is used; otherwise, a fresh entry is created. If there is a trailing CAUSE from step 2, the result is errors.Join(entry, cause). If there is no cause, the entry is returned.

Note: For inter-function composition, prefer New() with trailing cause:

return NewErr(ErrRepo, "key", val, cause) // cause last

func WriteContextDiff

func WriteContextDiff(writer io.Writer, diff ContextDiff) error

WriteContextDiff writes a context diff to the given writer.

func WriteUnifiedDiff

func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) (err error)

WriteUnifiedDiff writes a unified diff to the given writer.

Types

type ChangeType

type ChangeType int

ChangeType indicates whether lines were added or deleted in a diff.

const (
	// LinesAdded means lines exist in new but not old.
	LinesAdded ChangeType = iota
	// LinesDeleted means lines exist in old but not new.
	LinesDeleted
)

func (ChangeType) String

func (ct ChangeType) String() string

String returns a human-readable representation of the change type.

type ContextDiff

type ContextDiff UnifiedDiff

ContextDiff holds parameters for generating a context diff.

type DiffChange

type DiffChange struct {
	Type     ChangeType
	OldRange LineRange
	NewRange LineRange
}

DiffChange represents a contiguous block of added or deleted lines.

type DiffContent

type DiffContent struct {
	OldLines []string     // Full "before" content, one entry per line
	NewLines []string     // Full "after" content, one entry per line
	Changes  []DiffChange // Change blocks (additions and deletions)
	Label    string       // Display label (filename, path, etc.)
}

DiffContent holds diff data for two versions of text. It is the canonical interchange type for all diff operations in this package.

func DiffLines

func DiffLines(old, new []string, label string) (content *DiffContent, err error)

DiffLines computes a diff between two line slices. Uses the absorbed go-difflib SequenceMatcher engine (Myers-like algorithm). Returns a DiffContent with the original lines and computed change blocks.

type DiffSource

type DiffSource interface {
	DiffContent() (*DiffContent, error)
}

DiffSource produces diff content from some origin. Implementations might read from Git, files, databases, etc.

type ErrKV

type ErrKV interface {
	Key() string
	Value() any
}

ErrKV represents a key/value metadata pair. Keys are preserved in insertion order, and values may be of any type.

func AnyKV

func AnyKV(key string, value any) ErrKV

AnyKV creates a KV with any value type. Use this for custom types or when the specific type constructor doesn't exist.

func AppendKV

func AppendKV(kvs []ErrKV, parts ...any) []ErrKV

AppendKV appends key-value pairs to a slice of KV values. It accepts variadic arguments in three forms:

  1. Individual KV values: AppendKV(kvs, StringKV("foo", "bar"))
  2. String key-value pairs: AppendKV(kvs, "foo", "bar")
  3. Lazy ErrKV functions: AppendKV(kvs, func()ErrKV{ return StringKV("expensive", compute()) })

This function is useful for accumulating metadata throughout a function before creating an error:

var kvs []ErrKV
kvs = AppendKV(kvs, "user_id", userID)
if complexCondition {
    kvs = AppendKV(kvs, "reason", "complex")
}
return NewErr(ErrFailed, kvs, cause)

String key-value pairs must come in even counts (odd counts panic in debug mode). Lazy functions (func()ErrKV) are wrapped and not evaluated until error creation.

func BoolKV

func BoolKV(key string, value bool) ErrKV

BoolKV creates a KV with a bool value.

func ErrMeta

func ErrMeta(err error) []ErrKV

ErrMeta returns the key/value pairs stored on all doterr entries in the error tree. If err is a doterr entry, returns its metadata. If err is a joined error (has Unwrap() []error), scans all children recursively and collects metadata from all doterr entries found, preserving order. Otherwise returns nil. The returned slice preserves insertion order across all entries.

func ErrorKV

func ErrorKV(key string, value error) ErrKV

ErrorKV creates a KV with an error value. Use this when you want to include an error as metadata (not as a cause).

func Float64KV

func Float64KV(key string, value float64) ErrKV

Float64KV creates a KV with a float64 value.

func Int64KV

func Int64KV(key string, value int64) ErrKV

Int64KV creates a KV with an int64 value.

func IntKV

func IntKV(key string, value int) ErrKV

IntKV creates a KV with an int value.

func StringKV

func StringKV(key, value string) ErrKV

StringKV creates a KV with a string value.

type InlineHighlighter

type InlineHighlighter interface {
	HighlightInline(oldLine, newLine string) (oldResult, newResult string)
}

InlineHighlighter provides character-level highlighting within changed lines. Implementations typically use diff-match-patch or similar algorithms to identify which characters changed between two versions of a line.

type IsDotErrEntry

type IsDotErrEntry = interface {
	IsDotErrEntry()
	HasKV(key string, value any) bool
	MatchKV(key string, value any) bool
}

type LineRange

type LineRange struct {
	Start int // 1-based line number where range starts
	Count int // Number of lines in the range
}

LineRange is a 1-based line range.

type Match

type Match struct {
	A    int
	B    int
	Size int
}

Match represents a matching block between two sequences. a[A:A+Size] == b[B:B+Size]

type OpCode

type OpCode struct {
	Tag byte
	I1  int
	I2  int
	J1  int
	J2  int
}

OpCode describes how to turn a[I1:I2] into b[J1:J2]. Tag is one of: 'r' (replace), 'd' (delete), 'i' (insert), 'e' (equal).

type SequenceMatcher

type SequenceMatcher struct {
	IsJunk func(string) bool
	// contains filtered or unexported fields
}

SequenceMatcher compares two sequences of strings using a "gestalt pattern matching" algorithm. It finds the longest contiguous matching subsequence that contains no "junk" elements, then recursively applies the same idea to the pieces on each side.

func NewMatcher

func NewMatcher(a, b []string) *SequenceMatcher

NewMatcher creates a new SequenceMatcher comparing a and b.

func NewMatcherWithJunk

func NewMatcherWithJunk(a, b []string, autoJunk bool,
	isJunk func(string) bool) *SequenceMatcher

NewMatcherWithJunk creates a SequenceMatcher with custom junk detection.

func (*SequenceMatcher) GetGroupedOpCodes

func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode

GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes. Returns groups with up to n lines of context.

func (*SequenceMatcher) GetMatchingBlocks

func (m *SequenceMatcher) GetMatchingBlocks() []Match

GetMatchingBlocks returns a list of triples describing matching subsequences. Each Match means a[A:A+Size] == b[B:B+Size]. The last triple is a dummy (len(a), len(b), 0).

func (*SequenceMatcher) GetOpCodes

func (m *SequenceMatcher) GetOpCodes() []OpCode

GetOpCodes returns a list of OpCodes describing how to turn a into b. Tag meanings: 'r' replace, 'd' delete, 'i' insert, 'e' equal.

func (*SequenceMatcher) QuickRatio

func (m *SequenceMatcher) QuickRatio() float64

QuickRatio returns an upper bound on Ratio() relatively quickly.

func (*SequenceMatcher) Ratio

func (m *SequenceMatcher) Ratio() float64

Ratio returns a measure of the sequences' similarity (float in [0,1]).

func (*SequenceMatcher) RealQuickRatio

func (m *SequenceMatcher) RealQuickRatio() float64

RealQuickRatio returns an upper bound on Ratio() very quickly.

func (*SequenceMatcher) SetSeq1

func (m *SequenceMatcher) SetSeq1(a []string)

SetSeq1 sets the first sequence. The second is not changed.

func (*SequenceMatcher) SetSeq2

func (m *SequenceMatcher) SetSeq2(b []string)

SetSeq2 sets the second sequence. The first is not changed.

func (*SequenceMatcher) SetSeqs

func (m *SequenceMatcher) SetSeqs(a, b []string)

SetSeqs sets both sequences to be compared.

type UnifiedDiff

type UnifiedDiff struct {
	A        []string // First sequence lines
	FromFile string   // First file name
	FromDate string   // First file time
	B        []string // Second sequence lines
	ToFile   string   // Second file name
	ToDate   string   // Second file time
	Eol      string   // Headers end of line, defaults to LF
	Context  int      // Number of context lines
}

UnifiedDiff holds parameters for generating a unified diff.

Directories

Path Synopsis
dmp module
sgdiff module

Jump to

Keyboard shortcuts

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