diff options
| author | Anton Medvedev <anton@medv.io> | 2025-11-30 12:46:34 +0100 |
|---|---|---|
| committer | Anton Medvedev <anton@medv.io> | 2025-11-30 12:46:34 +0100 |
| commit | f6b0f38af648d028422a7494378b5dabdc90573f (patch) | |
| tree | 3c26cfc269c021300a2d1e4e02623dd440c20226 /pkg/gitdiff/apply.go | |
First commit
Diffstat (limited to 'pkg/gitdiff/apply.go')
| -rw-r--r-- | pkg/gitdiff/apply.go | 147 |
1 files changed, 147 insertions, 0 deletions
diff --git a/pkg/gitdiff/apply.go b/pkg/gitdiff/apply.go new file mode 100644 index 0000000..44bbcca --- /dev/null +++ b/pkg/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 + } +} |
