summaryrefslogtreecommitdiff
path: root/pkg/gitdiff/apply_text.go
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2026-01-30 18:16:31 -0700
committermo khan <mo@mokhan.ca>2026-01-30 18:16:31 -0700
commitfeee7d43ef63ae607c6fd4cca88a356a93553ebe (patch)
tree2969055a894dc4e72d8d79a9ac74cc30d78aff64 /pkg/gitdiff/apply_text.go
parente0db8f82e96acadf6968e0cf9c805a7b22d835db (diff)
refactor: move packages to internal/
Diffstat (limited to 'pkg/gitdiff/apply_text.go')
-rw-r--r--pkg/gitdiff/apply_text.go158
1 files changed, 0 insertions, 158 deletions
diff --git a/pkg/gitdiff/apply_text.go b/pkg/gitdiff/apply_text.go
deleted file mode 100644
index 8d0accb..0000000
--- a/pkg/gitdiff/apply_text.go
+++ /dev/null
@@ -1,158 +0,0 @@
-package gitdiff
-
-import (
- "errors"
- "io"
-)
-
-// TextApplier applies changes described in text fragments to source data. If
-// changes are described in multiple fragments, those fragments must be applied
-// in order. The applier must be closed after use.
-//
-// By default, TextApplier operates in "strict" mode, where fragment content
-// and positions must exactly match those of the source.
-type TextApplier struct {
- dst io.Writer
- src io.ReaderAt
- lineSrc LineReaderAt
- nextLine int64
-
- closed bool
- dirty bool
-}
-
-// NewTextApplier creates a TextApplier that reads data from src and writes
-// modified data to dst. If src implements LineReaderAt, it is used directly.
-func NewTextApplier(dst io.Writer, src io.ReaderAt) *TextApplier {
- a := TextApplier{
- dst: dst,
- src: src,
- }
-
- if lineSrc, ok := src.(LineReaderAt); ok {
- a.lineSrc = lineSrc
- } else {
- a.lineSrc = &lineReaderAt{r: src}
- }
-
- return &a
-}
-
-// ApplyFragment applies the changes in the fragment f, writing unwritten data
-// before the start of the fragment and any changes from the fragment. If
-// multiple text fragments apply to the same content, ApplyFragment must be
-// called in order of increasing start position. As a result, each fragment can
-// be applied at most once.
-//
-// If an error occurs while applying, ApplyFragment returns an *ApplyError that
-// annotates the error with additional information. If the error is because of
-// a conflict between the fragment and the source, the wrapped error will be a
-// *Conflict.
-func (a *TextApplier) ApplyFragment(f *TextFragment) error {
- if a.closed {
- return applyError(errApplierClosed)
- }
-
- // mark an apply as in progress, even if it fails before making changes
- a.dirty = true
-
- // application code assumes fragment fields are consistent
- if err := f.Validate(); err != nil {
- return applyError(err)
- }
-
- // lines are 0-indexed, positions are 1-indexed (but new files have position = 0)
- fragStart := f.OldPosition - 1
- if fragStart < 0 {
- fragStart = 0
- }
- fragEnd := fragStart + f.OldLines
-
- start := a.nextLine
- if fragStart < start {
- return applyError(&Conflict{"fragment overlaps with an applied fragment"})
- }
-
- if f.OldPosition == 0 {
- ok, err := isLen(a.src, 0)
- if err != nil {
- return applyError(err)
- }
- if !ok {
- return applyError(&Conflict{"cannot create new file from non-empty src"})
- }
- }
-
- preimage := make([][]byte, fragEnd-start)
- n, err := a.lineSrc.ReadLinesAt(preimage, start)
- if err != nil {
- // an EOF indicates that source file is shorter than the patch expects,
- // which should be reported as a conflict rather than a generic error
- if errors.Is(err, io.EOF) {
- err = &Conflict{"src has fewer lines than required by fragment"}
- }
- return applyError(err, lineNum(start+int64(n)))
- }
-
- // copy leading data before the fragment starts
- for i, line := range preimage[:fragStart-start] {
- if _, err := a.dst.Write(line); err != nil {
- a.nextLine = start + int64(i)
- return applyError(err, lineNum(a.nextLine))
- }
- }
- preimage = preimage[fragStart-start:]
-
- // apply the changes in the fragment
- used := int64(0)
- for i, line := range f.Lines {
- if err := applyTextLine(a.dst, line, preimage, used); err != nil {
- a.nextLine = fragStart + used
- return applyError(err, lineNum(a.nextLine), fragLineNum(i))
- }
- if line.Old() {
- used++
- }
- }
- a.nextLine = fragStart + used
-
- // new position of +0,0 mean a full delete, so check for leftovers
- if f.NewPosition == 0 && f.NewLines == 0 {
- var b [1][]byte
- n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine)
- if err != nil && err != io.EOF {
- return applyError(err, lineNum(a.nextLine))
- }
- if n > 0 {
- return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine))
- }
- }
-
- return nil
-}
-
-func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) {
- if line.Old() && string(preimage[i]) != line.Line {
- return &Conflict{"fragment line does not match src line"}
- }
- if line.New() {
- _, err = io.WriteString(dst, line.Line)
- }
- return err
-}
-
-// Close writes any data following the last applied fragment and prevents
-// future calls to ApplyFragment.
-func (a *TextApplier) Close() (err error) {
- if a.closed {
- return nil
- }
-
- a.closed = true
- if !a.dirty {
- _, err = copyFrom(a.dst, a.src, 0)
- } else {
- _, err = copyLinesFrom(a.dst, a.lineSrc, a.nextLine)
- }
- return err
-}