mirror of
https://github.com/bluekeyes/go-gitdiff
synced 2026-06-08 13:18:30 +00:00
ae704236bb
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.
199 lines
5.2 KiB
Go
199 lines
5.2 KiB
Go
package gitdiff
|
|
|
|
import (
|
|
"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 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
|
|
|
|
// an EOF is allowed here: the fragment applies to the last line of the
|
|
// source but it does not have a newline character
|
|
nextLine, n, err := copyLines(dst, src, limit)
|
|
if err != nil && err != io.EOF {
|
|
return applyError(err, lineNum(n))
|
|
}
|
|
|
|
for i, line := range f.Lines {
|
|
fromSrc, err := applyTextLine(dst, nextLine, line)
|
|
if err != nil {
|
|
return applyError(err, lineNum(n), fragLineNum(i))
|
|
}
|
|
|
|
if fromSrc && i < len(f.Lines)-1 {
|
|
nextLine, n, err = src.ReadLine()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
err = io.ErrUnexpectedEOF
|
|
}
|
|
return applyError(err, lineNum(n), fragLineNum(i+1))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyTextLine(dst io.Writer, src string, line Line) (fromSrc bool, err error) {
|
|
switch line.Op {
|
|
case OpContext, OpDelete:
|
|
fromSrc = true
|
|
if src != line.Line {
|
|
return fromSrc, conflictError("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. 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 "", int(limit), nil
|
|
case int64(n) == limit:
|
|
return line, n, err
|
|
case int64(n) > limit:
|
|
if limit < 0 {
|
|
return "", n, conflictError("cannot create new file from non-empty src")
|
|
}
|
|
return "", n, conflictError("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")
|
|
}
|