Files
bluekeyes-go-gitdiff/gitdiff/apply.go
T
Billy Keyes eb5f3de78c Improve conflict error detection
Conflict errors are now represented by an exported type that is
compatible with errors.Is instead of using the method defined on
ApplyError. This makes the tests slightly cleaner and should be more
idiomatic for clients.
2020-01-12 22:09:08 -08:00

217 lines
5.7 KiB
Go

package gitdiff
import (
"fmt"
"io"
)
// 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 int
// 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 = int(v) + 1
case fragNum:
e.Fragment = int(v) + 1
case fragLineNum:
e.FragmentLine = int(v) + 1
}
}
return e
}
// ApplyStrict writes data from src to dst, modifying it as described by the
// fragments in the file. For text files, each fragment, including all context
// lines, must exactly match src at the expected line number.
//
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
// Partial data may be written to dst in this case.
func (f *File) ApplyStrict(dst io.Writer, src io.Reader) error {
if f.IsBinary {
if f.BinaryFragment != nil {
return f.BinaryFragment.Apply(dst, src)
}
_, err := io.Copy(dst, src)
return applyError(err)
}
lr, ok := src.(LineReader)
if !ok {
lr = NewLineReader(src, 0)
}
for i, frag := range f.TextFragments {
if err := frag.ApplyStrict(dst, lr); err != nil {
return applyError(err, fragNum(i))
}
}
_, err := io.Copy(dst, unwrapLineReader(lr))
return applyError(err)
}
// ApplyStrict writes data from src to dst, modifying it as described by the
// fragment. The fragment, including all context lines, must exactly match src
// at the expected line number.
//
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
// Partial data may be written to dst in this case. If there is no error, the
// next read from src returns the line immediately after the last line of the
// fragment.
func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReader) error {
// application code assumes fragment fields are consistent
if err := f.Validate(); err != nil {
return applyError(err)
}
// line numbers are zero-indexed, positions are one-indexed
limit := f.OldPosition - 1
// io.EOF is acceptable here: the first line of the patch is the last of
// the source and it has no newline character
nextLine, n, err := copyLines(dst, src, limit)
if err != nil && err != io.EOF {
return applyError(err, lineNum(n))
}
used := int64(0)
for i, line := range f.Lines {
if err := applyTextLine(dst, nextLine, line); err != nil {
return applyError(err, lineNum(n), fragLineNum(i))
}
if line.Old() {
used++
}
// advance reader if the next fragment line appears in src and we're behind
if i < len(f.Lines)-1 && f.Lines[i+1].Old() && int64(n)-limit < used {
nextLine, n, err = src.ReadLine()
switch {
case err == io.EOF && f.Lines[i+1].NoEOL():
continue
case err != nil:
return applyError(err, lineNum(n), fragLineNum(i+1)) // report for _next_ line in fragment
}
}
}
return nil
}
func applyTextLine(dst io.Writer, src string, line Line) (err error) {
switch line.Op {
case OpContext, OpDelete:
if src != line.Line {
return &Conflict{"fragment line does not match src line"}
}
}
switch line.Op {
case OpContext, OpAdd:
_, err = io.WriteString(dst, line.Line)
}
return
}
// copyLines copies from src to dst until the line at limit, exclusive. Returns
// the line at limit and the line number. If the error is nil or io.EOF, the
// line number equals limit. A negative limit checks that the source has no
// more lines to read.
func copyLines(dst io.Writer, src LineReader, limit int64) (string, int64, error) {
for {
line, n, err := src.ReadLine()
switch {
case limit < 0 && err == io.EOF && line == "":
return "", limit, nil
case n == limit:
return line, n, err
case n > limit:
if limit < 0 {
return "", n, &Conflict{"cannot create new file from non-empty src"}
}
return "", n, &Conflict{"fragment overlaps with an applied fragment"}
case err != nil:
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return line, n, err
}
if _, err := io.WriteString(dst, line); err != nil {
return "", n, err
}
}
}
// Apply writes data from src to dst, modifying it as described by the
// fragment.
//
// Unlike text fragments, binary fragments do not distinguish between strict
// and non-strict application.
func (f *BinaryFragment) Apply(dst io.Writer, src io.Reader) error {
panic("TODO(bkeyes): unimplemented")
}