diff options
Diffstat (limited to 'pkg/gitdiff/gitdiff.go')
| -rw-r--r-- | pkg/gitdiff/gitdiff.go | 230 |
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() +} |
