diff options
| author | mo khan <mo@mokhan.ca> | 2026-01-30 18:16:31 -0700 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2026-01-30 18:16:31 -0700 |
| commit | feee7d43ef63ae607c6fd4cca88a356a93553ebe (patch) | |
| tree | 2969055a894dc4e72d8d79a9ac74cc30d78aff64 /internal/gitdiff/apply.go | |
| parent | e0db8f82e96acadf6968e0cf9c805a7b22d835db (diff) | |
refactor: move packages to internal/
Diffstat (limited to 'internal/gitdiff/apply.go')
| -rw-r--r-- | internal/gitdiff/apply.go | 147 |
1 files changed, 147 insertions, 0 deletions
diff --git a/internal/gitdiff/apply.go b/internal/gitdiff/apply.go new file mode 100644 index 0000000..44bbcca --- /dev/null +++ b/internal/gitdiff/apply.go @@ -0,0 +1,147 @@ +package gitdiff + +import ( + "errors" + "fmt" + "io" + "sort" +) + +// Conflict indicates an apply failed due to a conflict between the patch and +// the source content. +// +// Users can test if an error was caused by a conflict by using errors.Is with +// an empty Conflict: +// +// if errors.Is(err, &Conflict{}) { +// // handle conflict +// } +type Conflict struct { + msg string +} + +func (c *Conflict) Error() string { + return "conflict: " + c.msg +} + +// Is implements error matching for Conflict. Passing an empty instance of +// Conflict always returns true. +func (c *Conflict) Is(other error) bool { + if other, ok := other.(*Conflict); ok { + return other.msg == "" || other.msg == c.msg + } + return false +} + +// ApplyError wraps an error that occurs during patch application with +// additional location information, if it is available. +type ApplyError struct { + // Line is the one-indexed line number in the source data + Line int64 + // Fragment is the one-indexed fragment number in the file + Fragment int + // FragmentLine is the one-indexed line number in the fragment + FragmentLine int + + err error +} + +// Unwrap returns the wrapped error. +func (e *ApplyError) Unwrap() error { + return e.err +} + +func (e *ApplyError) Error() string { + return fmt.Sprintf("%v", e.err) +} + +type lineNum int +type fragNum int +type fragLineNum int + +// applyError creates a new *ApplyError wrapping err or augments the information +// in err with args if it is already an *ApplyError. Returns nil if err is nil. +func applyError(err error, args ...interface{}) error { + if err == nil { + return nil + } + + e, ok := err.(*ApplyError) + if !ok { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + e = &ApplyError{err: err} + } + for _, arg := range args { + switch v := arg.(type) { + case lineNum: + e.Line = int64(v) + 1 + case fragNum: + e.Fragment = int(v) + 1 + case fragLineNum: + e.FragmentLine = int(v) + 1 + } + } + return e +} + +var ( + errApplyInProgress = errors.New("gitdiff: incompatible apply in progress") + errApplierClosed = errors.New("gitdiff: applier is closed") +) + +// Apply applies the changes in f to src, writing the result to dst. It can +// apply both text and binary changes. +// +// If an error occurs while applying, Apply returns an *ApplyError that +// annotates the error with additional information. If the error is because of +// a conflict with the source, the wrapped error will be a *Conflict. +func Apply(dst io.Writer, src io.ReaderAt, f *File) error { + if f.IsBinary { + if len(f.TextFragments) > 0 { + return applyError(errors.New("binary file contains text fragments")) + } + if f.BinaryFragment == nil { + return applyError(errors.New("binary file does not contain a binary fragment")) + } + } else { + if f.BinaryFragment != nil { + return applyError(errors.New("text file contains a binary fragment")) + } + } + + switch { + case f.BinaryFragment != nil: + applier := NewBinaryApplier(dst, src) + if err := applier.ApplyFragment(f.BinaryFragment); err != nil { + return err + } + return applier.Close() + + case len(f.TextFragments) > 0: + frags := make([]*TextFragment, len(f.TextFragments)) + copy(frags, f.TextFragments) + + sort.Slice(frags, func(i, j int) bool { + return frags[i].OldPosition < frags[j].OldPosition + }) + + // TODO(bkeyes): consider merging overlapping fragments + // right now, the application fails if fragments overlap, but it should be + // possible to precompute the result of applying them in order + + applier := NewTextApplier(dst, src) + for i, frag := range frags { + if err := applier.ApplyFragment(frag); err != nil { + return applyError(err, fragNum(i)) + } + } + return applier.Close() + + default: + // nothing to apply, just copy all the data + _, err := copyFrom(dst, src, 0) + return err + } +} |
