summaryrefslogtreecommitdiff
path: root/internal/gitdiff/format.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 /internal/gitdiff/format.go
parente0db8f82e96acadf6968e0cf9c805a7b22d835db (diff)
refactor: move packages to internal/
Diffstat (limited to 'internal/gitdiff/format.go')
-rw-r--r--internal/gitdiff/format.go281
1 files changed, 281 insertions, 0 deletions
diff --git a/internal/gitdiff/format.go b/internal/gitdiff/format.go
new file mode 100644
index 0000000..d97aba9
--- /dev/null
+++ b/internal/gitdiff/format.go
@@ -0,0 +1,281 @@
+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()
+}