mirror of
https://github.com/bluekeyes/go-gitdiff
synced 2026-06-08 13:18:30 +00:00
fffa3cca9b
Include file names in the header (now that we can actually parse them) and fix a bad find-and-replace that changed "differ" to "fmer". Add a new test to verify that binary files without data format correctly.
282 lines
5.5 KiB
Go
282 lines
5.5 KiB
Go
package gitdiff
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
)
|
|
|
|
type formatter struct {
|
|
w io.Writer
|
|
err error
|
|
}
|
|
|
|
func newFormatter(w io.Writer) *formatter {
|
|
return &formatter{w: w}
|
|
}
|
|
|
|
func (fm *formatter) Write(p []byte) (int, error) {
|
|
if fm.err != nil {
|
|
return len(p), nil
|
|
}
|
|
if _, err := fm.w.Write(p); err != nil {
|
|
fm.err = err
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (fm *formatter) WriteString(s string) (int, error) {
|
|
fm.Write([]byte(s))
|
|
return len(s), nil
|
|
}
|
|
|
|
func (fm *formatter) WriteByte(c byte) error {
|
|
fm.Write([]byte{c})
|
|
return nil
|
|
}
|
|
|
|
func (fm *formatter) WriteQuotedName(s string) {
|
|
qpos := 0
|
|
for i := 0; i < len(s); i++ {
|
|
ch := s[i]
|
|
if q, quoted := quoteByte(ch); quoted {
|
|
if qpos == 0 {
|
|
fm.WriteByte('"')
|
|
}
|
|
fm.WriteString(s[qpos:i])
|
|
fm.Write(q)
|
|
qpos = i + 1
|
|
}
|
|
}
|
|
fm.WriteString(s[qpos:])
|
|
if qpos > 0 {
|
|
fm.WriteByte('"')
|
|
}
|
|
}
|
|
|
|
var quoteEscapeTable = map[byte]byte{
|
|
'\a': 'a',
|
|
'\b': 'b',
|
|
'\t': 't',
|
|
'\n': 'n',
|
|
'\v': 'v',
|
|
'\f': 'f',
|
|
'\r': 'r',
|
|
'"': '"',
|
|
'\\': '\\',
|
|
}
|
|
|
|
func quoteByte(b byte) ([]byte, bool) {
|
|
if q, ok := quoteEscapeTable[b]; ok {
|
|
return []byte{'\\', q}, true
|
|
}
|
|
if b < 0x20 || b >= 0x7F {
|
|
return []byte{
|
|
'\\',
|
|
'0' + (b>>6)&0o3,
|
|
'0' + (b>>3)&0o7,
|
|
'0' + (b>>0)&0o7,
|
|
}, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (fm *formatter) FormatFile(f *File) {
|
|
fm.WriteString("diff --git ")
|
|
|
|
var aName, bName string
|
|
switch {
|
|
case f.OldName == "":
|
|
aName = f.NewName
|
|
bName = f.NewName
|
|
|
|
case f.NewName == "":
|
|
aName = f.OldName
|
|
bName = f.OldName
|
|
|
|
default:
|
|
aName = f.OldName
|
|
bName = f.NewName
|
|
}
|
|
|
|
fm.WriteQuotedName("a/" + aName)
|
|
fm.WriteByte(' ')
|
|
fm.WriteQuotedName("b/" + bName)
|
|
fm.WriteByte('\n')
|
|
|
|
if f.OldMode != 0 {
|
|
if f.IsDelete {
|
|
fmt.Fprintf(fm, "deleted file mode %o\n", f.OldMode)
|
|
} else if f.NewMode != 0 {
|
|
fmt.Fprintf(fm, "old mode %o\n", f.OldMode)
|
|
}
|
|
}
|
|
|
|
if f.NewMode != 0 {
|
|
if f.IsNew {
|
|
fmt.Fprintf(fm, "new file mode %o\n", f.NewMode)
|
|
} else if f.OldMode != 0 {
|
|
fmt.Fprintf(fm, "new mode %o\n", f.NewMode)
|
|
}
|
|
}
|
|
|
|
if f.Score > 0 {
|
|
if f.IsCopy || f.IsRename {
|
|
fmt.Fprintf(fm, "similarity index %d%%\n", f.Score)
|
|
} else {
|
|
fmt.Fprintf(fm, "dissimilarity index %d%%\n", f.Score)
|
|
}
|
|
}
|
|
|
|
if f.IsCopy {
|
|
if f.OldName != "" {
|
|
fm.WriteString("copy from ")
|
|
fm.WriteQuotedName(f.OldName)
|
|
fm.WriteByte('\n')
|
|
}
|
|
if f.NewName != "" {
|
|
fm.WriteString("copy to ")
|
|
fm.WriteQuotedName(f.NewName)
|
|
fm.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
if f.IsRename {
|
|
if f.OldName != "" {
|
|
fm.WriteString("rename from ")
|
|
fm.WriteQuotedName(f.OldName)
|
|
fm.WriteByte('\n')
|
|
}
|
|
if f.NewName != "" {
|
|
fm.WriteString("rename to ")
|
|
fm.WriteQuotedName(f.NewName)
|
|
fm.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
if f.OldOIDPrefix != "" && f.NewOIDPrefix != "" {
|
|
fmt.Fprintf(fm, "index %s..%s", f.OldOIDPrefix, f.NewOIDPrefix)
|
|
|
|
// Mode is only included on the index line when it is not changing
|
|
if f.OldMode != 0 && ((f.NewMode == 0 && !f.IsDelete) || f.OldMode == f.NewMode) {
|
|
fmt.Fprintf(fm, " %o", f.OldMode)
|
|
}
|
|
|
|
fm.WriteByte('\n')
|
|
}
|
|
|
|
if f.IsBinary {
|
|
if f.BinaryFragment == nil {
|
|
fm.WriteString("Binary files ")
|
|
fm.WriteQuotedName("a/" + aName)
|
|
fm.WriteString(" and ")
|
|
fm.WriteQuotedName("b/" + bName)
|
|
fm.WriteString(" differ\n")
|
|
} else {
|
|
fm.WriteString("GIT binary patch\n")
|
|
fm.FormatBinaryFragment(f.BinaryFragment)
|
|
if f.ReverseBinaryFragment != nil {
|
|
fm.FormatBinaryFragment(f.ReverseBinaryFragment)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The "---" and "+++" lines only appear for text patches with fragments
|
|
if len(f.TextFragments) > 0 {
|
|
fm.WriteString("--- ")
|
|
if f.OldName == "" {
|
|
fm.WriteString("/dev/null")
|
|
} else {
|
|
fm.WriteQuotedName("a/" + f.OldName)
|
|
}
|
|
fm.WriteByte('\n')
|
|
|
|
fm.WriteString("+++ ")
|
|
if f.NewName == "" {
|
|
fm.WriteString("/dev/null")
|
|
} else {
|
|
fm.WriteQuotedName("b/" + f.NewName)
|
|
}
|
|
fm.WriteByte('\n')
|
|
|
|
for _, frag := range f.TextFragments {
|
|
fm.FormatTextFragment(frag)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (fm *formatter) FormatTextFragment(f *TextFragment) {
|
|
fm.FormatTextFragmentHeader(f)
|
|
fm.WriteByte('\n')
|
|
|
|
for _, line := range f.Lines {
|
|
fm.WriteString(line.Op.String())
|
|
fm.WriteString(line.Line)
|
|
if line.NoEOL() {
|
|
fm.WriteString("\n\\ No newline at end of file\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (fm *formatter) FormatTextFragmentHeader(f *TextFragment) {
|
|
fmt.Fprintf(fm, "@@ -%d,%d +%d,%d @@", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines)
|
|
if f.Comment != "" {
|
|
fm.WriteByte(' ')
|
|
fm.WriteString(f.Comment)
|
|
}
|
|
}
|
|
|
|
func (fm *formatter) FormatBinaryFragment(f *BinaryFragment) {
|
|
const (
|
|
maxBytesPerLine = 52
|
|
)
|
|
|
|
switch f.Method {
|
|
case BinaryPatchDelta:
|
|
fm.WriteString("delta ")
|
|
case BinaryPatchLiteral:
|
|
fm.WriteString("literal ")
|
|
}
|
|
fm.Write(strconv.AppendInt(nil, f.Size, 10))
|
|
fm.WriteByte('\n')
|
|
|
|
data := deflateBinaryChunk(f.Data)
|
|
n := (len(data) / maxBytesPerLine) * maxBytesPerLine
|
|
|
|
buf := make([]byte, base85Len(maxBytesPerLine))
|
|
for i := 0; i < n; i += maxBytesPerLine {
|
|
base85Encode(buf, data[i:i+maxBytesPerLine])
|
|
fm.WriteByte('z')
|
|
fm.Write(buf)
|
|
fm.WriteByte('\n')
|
|
}
|
|
if remainder := len(data) - n; remainder > 0 {
|
|
buf = buf[0:base85Len(remainder)]
|
|
|
|
sizeChar := byte(remainder)
|
|
if remainder <= 26 {
|
|
sizeChar = 'A' + sizeChar - 1
|
|
} else {
|
|
sizeChar = 'a' + sizeChar - 27
|
|
}
|
|
|
|
base85Encode(buf, data[n:])
|
|
fm.WriteByte(sizeChar)
|
|
fm.Write(buf)
|
|
fm.WriteByte('\n')
|
|
}
|
|
fm.WriteByte('\n')
|
|
}
|
|
|
|
func deflateBinaryChunk(data []byte) []byte {
|
|
var b bytes.Buffer
|
|
|
|
zw := zlib.NewWriter(&b)
|
|
_, _ = zw.Write(data)
|
|
_ = zw.Close()
|
|
|
|
return b.Bytes()
|
|
}
|