summaryrefslogtreecommitdiff
path: root/pkg/gitdiff/gitdiff.go
diff options
context:
space:
mode:
authorAnton Medvedev <anton@medv.io>2025-11-30 12:46:34 +0100
committerAnton Medvedev <anton@medv.io>2025-11-30 12:46:34 +0100
commitf6b0f38af648d028422a7494378b5dabdc90573f (patch)
tree3c26cfc269c021300a2d1e4e02623dd440c20226 /pkg/gitdiff/gitdiff.go
First commit
Diffstat (limited to 'pkg/gitdiff/gitdiff.go')
-rw-r--r--pkg/gitdiff/gitdiff.go230
1 files changed, 230 insertions, 0 deletions
diff --git a/pkg/gitdiff/gitdiff.go b/pkg/gitdiff/gitdiff.go
new file mode 100644
index 0000000..5365645
--- /dev/null
+++ b/pkg/gitdiff/gitdiff.go
@@ -0,0 +1,230 @@
+package gitdiff
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+)
+
+// File describes changes to a single file. It can be either a text file or a
+// binary file.
+type File struct {
+ OldName string
+ NewName string
+
+ IsNew bool
+ IsDelete bool
+ IsCopy bool
+ IsRename bool
+
+ OldMode os.FileMode
+ NewMode os.FileMode
+
+ OldOIDPrefix string
+ NewOIDPrefix string
+ Score int
+
+ // TextFragments contains the fragments describing changes to a text file. It
+ // may be empty if the file is empty or if only the mode changes.
+ TextFragments []*TextFragment
+
+ // IsBinary is true if the file is a binary file. If the patch includes
+ // binary data, BinaryFragment will be non-nil and describe the changes to
+ // the data. If the patch is reversible, ReverseBinaryFragment will also be
+ // non-nil and describe the changes needed to restore the original file
+ // after applying the changes in BinaryFragment.
+ IsBinary bool
+ BinaryFragment *BinaryFragment
+ ReverseBinaryFragment *BinaryFragment
+}
+
+// String returns a git diff representation of this file. The value can be
+// parsed by this library to obtain the same File, but may not be the same as
+// the original input.
+func (f *File) String() string {
+ var diff strings.Builder
+ newFormatter(&diff).FormatFile(f)
+ return diff.String()
+}
+
+// TextFragment describes changed lines starting at a specific line in a text file.
+type TextFragment struct {
+ Comment string
+
+ OldPosition int64
+ OldLines int64
+
+ NewPosition int64
+ NewLines int64
+
+ LinesAdded int64
+ LinesDeleted int64
+
+ LeadingContext int64
+ TrailingContext int64
+
+ Lines []Line
+}
+
+// String returns a git diff format of this fragment. See [File.String] for
+// more details on this format.
+func (f *TextFragment) String() string {
+ var diff strings.Builder
+ newFormatter(&diff).FormatTextFragment(f)
+ return diff.String()
+}
+
+// Header returns a git diff header of this fragment. See [File.String] for
+// more details on this format.
+func (f *TextFragment) Header() string {
+ var hdr strings.Builder
+ newFormatter(&hdr).FormatTextFragmentHeader(f)
+ return hdr.String()
+}
+
+// Validate checks that the fragment is self-consistent and appliable. Validate
+// returns an error if and only if the fragment is invalid.
+func (f *TextFragment) Validate() error {
+ if f == nil {
+ return errors.New("nil fragment")
+ }
+
+ var (
+ oldLines, newLines int64
+ leadingContext, trailingContext int64
+ contextLines, addedLines, deletedLines int64
+ )
+
+ // count the types of lines in the fragment content
+ for i, line := range f.Lines {
+ switch line.Op {
+ case OpContext:
+ oldLines++
+ newLines++
+ contextLines++
+ if addedLines == 0 && deletedLines == 0 {
+ leadingContext++
+ } else {
+ trailingContext++
+ }
+ case OpAdd:
+ newLines++
+ addedLines++
+ trailingContext = 0
+ case OpDelete:
+ oldLines++
+ deletedLines++
+ trailingContext = 0
+ default:
+ return fmt.Errorf("unknown operator %q on line %d", line.Op, i+1)
+ }
+ }
+
+ // check the actual counts against the reported counts
+ if oldLines != f.OldLines {
+ return lineCountErr("old", oldLines, f.OldLines)
+ }
+ if newLines != f.NewLines {
+ return lineCountErr("new", newLines, f.NewLines)
+ }
+ if leadingContext != f.LeadingContext {
+ return lineCountErr("leading context", leadingContext, f.LeadingContext)
+ }
+ if trailingContext != f.TrailingContext {
+ return lineCountErr("trailing context", trailingContext, f.TrailingContext)
+ }
+ if addedLines != f.LinesAdded {
+ return lineCountErr("added", addedLines, f.LinesAdded)
+ }
+ if deletedLines != f.LinesDeleted {
+ return lineCountErr("deleted", deletedLines, f.LinesDeleted)
+ }
+
+ // if a file is being created, it can only contain additions
+ if f.OldPosition == 0 && f.OldLines != 0 {
+ return errors.New("file creation fragment contains context or deletion lines")
+ }
+
+ return nil
+}
+
+func lineCountErr(kind string, actual, reported int64) error {
+ return fmt.Errorf("fragment contains %d %s lines but reports %d", actual, kind, reported)
+}
+
+// Line is a line in a text fragment.
+type Line struct {
+ Op LineOp
+ Line string
+}
+
+func (fl Line) String() string {
+ return fl.Op.String() + fl.Line
+}
+
+// Old returns true if the line appears in the old content of the fragment.
+func (fl Line) Old() bool {
+ return fl.Op == OpContext || fl.Op == OpDelete
+}
+
+// New returns true if the line appears in the new content of the fragment.
+func (fl Line) New() bool {
+ return fl.Op == OpContext || fl.Op == OpAdd
+}
+
+// NoEOL returns true if the line is missing a trailing newline character.
+func (fl Line) NoEOL() bool {
+ return len(fl.Line) == 0 || fl.Line[len(fl.Line)-1] != '\n'
+}
+
+// LineOp describes the type of a text fragment line: context, added, or removed.
+type LineOp int
+
+const (
+ // OpContext indicates a context line
+ OpContext LineOp = iota
+ // OpDelete indicates a deleted line
+ OpDelete
+ // OpAdd indicates an added line
+ OpAdd
+)
+
+func (op LineOp) String() string {
+ switch op {
+ case OpContext:
+ return " "
+ case OpDelete:
+ return "-"
+ case OpAdd:
+ return "+"
+ }
+ return "?"
+}
+
+// BinaryFragment describes changes to a binary file.
+type BinaryFragment struct {
+ Method BinaryPatchMethod
+ Size int64
+ Data []byte
+}
+
+// BinaryPatchMethod is the method used to create and apply the binary patch.
+type BinaryPatchMethod int
+
+const (
+ // BinaryPatchDelta indicates the data uses Git's packfile encoding
+ BinaryPatchDelta BinaryPatchMethod = iota
+ // BinaryPatchLiteral indicates the data is the exact file content
+ BinaryPatchLiteral
+)
+
+// String returns a git diff format of this fragment. Due to differences in
+// zlib implementation between Go and Git, encoded binary data in the result
+// will likely differ from what Git produces for the same input. See
+// [File.String] for more details on this format.
+func (f *BinaryFragment) String() string {
+ var diff strings.Builder
+ newFormatter(&diff).FormatBinaryFragment(f)
+ return diff.String()
+}