Files
bluekeyes-go-gitdiff/gitdiff/apply.go
T
Billy Keyes 774281f01b Standardize on int64 for line numbers
This matches the type used for positions in text fragments.
2020-01-09 22:11:25 -08:00

203 lines
5.3 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 {
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 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. 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, 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")
}