Files
bluekeyes-go-gitdiff/gitdiff/gitdiff.go
T
Billy Keyes 8584cd59af Add String() methods to parsed types (#48)
This enables clients to move back and forth between parsed objects and
text patches. The generated patches are semantically equal to the parsed
object and should re-parse to the same object, but may not be
byte-for-byte identical to the original input.

In my testing, formatted text patches are usually identical to the
input, but there may be cases where this is not true. Binary patches
always differ. This is because Go's 'compress/flate' package ends
streams with an empty block instead of adding the end-of-stream flag to
the last non-empty block, like Git's C implementation. Since the streams
will always be different for this reason, I chose to also enable default
compression (the test patches I generated with Git used no compression.)

The main tests for this feature involve parsing, formatting, and then
re-parsing a patch to make sure we get equal objects.

Formatting is handled by a new internal formatter type, which allows
writing all data to the same stream. This isn't exposed publicly right
now, but will be useful if there's a need for more flexible formatting
functions in the future, like formatting to a user-provided io.Writer.
2024-08-11 11:57:07 -07:00

231 lines
5.8 KiB
Go

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()
}