mirror of
https://github.com/bluekeyes/go-gitdiff
synced 2026-06-08 13:18:30 +00:00
Return ApplyError when ApplyString fails
This wraps the underlying error with optional position information and provides a way to test if the error was due to a conflict. At the moment, details about the conflict are not exposed outside of the message string.
This commit is contained in:
+95
-35
@@ -1,22 +1,86 @@
|
||||
package gitdiff
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type conflictError string
|
||||
|
||||
func (e conflictError) Error() string {
|
||||
return "conflict: " + string(e)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Conflict returns true if the error is due to a conflict between the fragment
|
||||
// and the source data.
|
||||
func (e *ApplyError) Conflict() bool {
|
||||
_, ok := e.err.(conflictError)
|
||||
return ok
|
||||
}
|
||||
|
||||
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 {
|
||||
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 file contains no fragments, ApplyStrict is equivalent to io.Copy.
|
||||
// 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 err
|
||||
return applyError(err)
|
||||
}
|
||||
|
||||
lr, ok := src.(LineReader)
|
||||
@@ -24,27 +88,28 @@ func (f *File) ApplyStrict(dst io.Writer, src io.Reader) error {
|
||||
lr = NewLineReader(src, 0)
|
||||
}
|
||||
|
||||
for _, frag := range f.TextFragments {
|
||||
for i, frag := range f.TextFragments {
|
||||
if err := frag.ApplyStrict(dst, lr); err != nil {
|
||||
return err
|
||||
return applyError(err, fragNum(i))
|
||||
}
|
||||
}
|
||||
|
||||
_, err := io.Copy(dst, unwrapLineReader(lr))
|
||||
return err
|
||||
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 there is no error, the next read from src returns the line immediately
|
||||
// after the last line of the fragment.
|
||||
// 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 {
|
||||
// TODO(bkeyes): wrap with additional context
|
||||
return err
|
||||
return applyError(err)
|
||||
}
|
||||
|
||||
// line numbers are zero-indexed, positions are one-indexed
|
||||
@@ -52,27 +117,24 @@ func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReader) error {
|
||||
|
||||
// an EOF is allowed here: the fragment applies to the last line of the
|
||||
// source but it does not have a newline character
|
||||
nextLine, err := copyLines(dst, src, limit)
|
||||
nextLine, n, err := copyLines(dst, src, limit)
|
||||
if err != nil && err != io.EOF {
|
||||
// TODO(bkeyes): wrap with additional context
|
||||
return err
|
||||
return applyError(err, lineNum(n))
|
||||
}
|
||||
|
||||
for i, line := range f.Lines {
|
||||
fromSrc, err := applyTextLine(dst, nextLine, line)
|
||||
if err != nil {
|
||||
// TODO(bkeyes): wrap with additional context
|
||||
return err
|
||||
return applyError(err, lineNum(n), fragLineNum(i))
|
||||
}
|
||||
|
||||
if fromSrc && i < len(f.Lines)-1 {
|
||||
nextLine, _, err = src.ReadLine()
|
||||
nextLine, n, err = src.ReadLine()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
// TODO(bkeyes): wrap with additional context
|
||||
return err
|
||||
return applyError(err, lineNum(n), fragLineNum(i+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,50 +142,48 @@ func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyTextLine(dst io.Writer, srcLine string, line Line) (fromSrc bool, err error) {
|
||||
func applyTextLine(dst io.Writer, src string, line Line) (fromSrc bool, err error) {
|
||||
switch line.Op {
|
||||
case OpContext, OpDelete:
|
||||
fromSrc = true
|
||||
if srcLine != line.Line {
|
||||
// TODO(bkeyes): use special error type here
|
||||
// TODO(bkeyes): include line number information, etc.
|
||||
return fromSrc, errors.New("apply: fragment match failed: line does not match")
|
||||
if src != line.Line {
|
||||
return fromSrc, conflictError("fragment line does not match src line")
|
||||
}
|
||||
}
|
||||
|
||||
switch line.Op {
|
||||
case OpContext, OpAdd:
|
||||
// TODO(bkeyes): wrap with additional context
|
||||
_, err = io.WriteString(dst, line.Line)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// copyLines copies from src to dst until the line at limit, exclusive. The
|
||||
// line at limit is returned. A negative limit means the first read should
|
||||
// return io.EOF and no data.
|
||||
func copyLines(dst io.Writer, src LineReader, limit int64) (string, error) {
|
||||
// copyLines copies from src to dst until the line at limit, exclusive. Returns
|
||||
// the line at limit and the line number. The line number may not equal the
|
||||
// limit if and only if a non-EOF error occurs. A negative limit means the
|
||||
// first read should return io.EOF and no data.
|
||||
func copyLines(dst io.Writer, src LineReader, limit int64) (string, int, error) {
|
||||
// TODO(bkeyes): fix int vs int64 for limit and return value
|
||||
for {
|
||||
line, n, err := src.ReadLine()
|
||||
switch {
|
||||
case limit < 0 && err == io.EOF && line == "":
|
||||
return "", nil
|
||||
return "", int(limit), nil
|
||||
case int64(n) == limit:
|
||||
return line, err
|
||||
return line, n, err
|
||||
case int64(n) > limit:
|
||||
if limit < 0 {
|
||||
return "", errors.New("src is not empty")
|
||||
return "", n, conflictError("cannot create new file from non-empty src")
|
||||
}
|
||||
return "", errors.New("overlapping fragments")
|
||||
return "", n, conflictError("fragment overlaps with an applied fragment")
|
||||
case err != nil:
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return line, err
|
||||
return line, n, err
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(dst, line); err != nil {
|
||||
return "", err
|
||||
return "", n, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user