Skip to content

Commit 3eb61e6

Browse files
committed
Move formatting functions to formatter
This cleans up the code a bit and allows writing everything to a single buffer when formatting a full file.
1 parent 7cec3fe commit 3eb61e6

File tree

5 files changed

+284
-248
lines changed

5 files changed

+284
-248
lines changed

gitdiff/format.go

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package gitdiff
2+
3+
import (
4+
"bytes"
5+
"compress/zlib"
6+
"fmt"
7+
"io"
8+
"strconv"
9+
)
10+
11+
type formatter struct {
12+
w io.Writer
13+
err error
14+
}
15+
16+
func newFormatter(w io.Writer) *formatter {
17+
return &formatter{w: w}
18+
}
19+
20+
func (fm *formatter) Write(p []byte) (int, error) {
21+
if fm.err != nil {
22+
return len(p), nil
23+
}
24+
if _, err := fm.w.Write(p); err != nil {
25+
fm.err = err
26+
}
27+
return len(p), nil
28+
}
29+
30+
func (fm *formatter) WriteString(s string) (int, error) {
31+
fm.Write([]byte(s))
32+
return len(s), nil
33+
}
34+
35+
func (fm *formatter) WriteByte(c byte) error {
36+
fm.Write([]byte{c})
37+
return nil
38+
}
39+
40+
func (fm *formatter) WriteQuotedName(s string) {
41+
qpos := 0
42+
for i := 0; i < len(s); i++ {
43+
ch := s[i]
44+
if q, quoted := quoteByte(ch); quoted {
45+
if qpos == 0 {
46+
fm.WriteByte('"')
47+
}
48+
fm.WriteString(s[qpos:i])
49+
fm.Write(q)
50+
qpos = i + 1
51+
}
52+
}
53+
fm.WriteString(s[qpos:])
54+
if qpos > 0 {
55+
fm.WriteByte('"')
56+
}
57+
}
58+
59+
var quoteEscapeTable = map[byte]byte{
60+
'\a': 'a',
61+
'\b': 'b',
62+
'\t': 't',
63+
'\n': 'n',
64+
'\v': 'v',
65+
'\f': 'f',
66+
'\r': 'r',
67+
'"': '"',
68+
'\\': '\\',
69+
}
70+
71+
func quoteByte(b byte) ([]byte, bool) {
72+
if q, ok := quoteEscapeTable[b]; ok {
73+
return []byte{'\\', q}, true
74+
}
75+
if b < 0x20 || b >= 0x7F {
76+
return []byte{
77+
'\\',
78+
'0' + (b>>6)&0o3,
79+
'0' + (b>>3)&0o7,
80+
'0' + (b>>0)&0o7,
81+
}, true
82+
}
83+
return nil, false
84+
}
85+
86+
func (fm *formatter) FormatFile(f *File) {
87+
fm.WriteString("diff --git ")
88+
89+
var aName, bName string
90+
switch {
91+
case f.OldName == "":
92+
aName = f.NewName
93+
bName = f.NewName
94+
95+
case f.NewName == "":
96+
aName = f.OldName
97+
bName = f.OldName
98+
99+
default:
100+
aName = f.OldName
101+
bName = f.NewName
102+
}
103+
104+
fm.WriteQuotedName("a/" + aName)
105+
fm.WriteByte(' ')
106+
fm.WriteQuotedName("b/" + bName)
107+
fm.WriteByte('\n')
108+
109+
if f.OldMode != 0 {
110+
if f.IsDelete {
111+
fmt.Fprintf(fm, "deleted file mode %o\n", f.OldMode)
112+
} else if f.NewMode != 0 {
113+
fmt.Fprintf(fm, "old mode %o\n", f.OldMode)
114+
}
115+
}
116+
117+
if f.NewMode != 0 {
118+
if f.IsNew {
119+
fmt.Fprintf(fm, "new file mode %o\n", f.NewMode)
120+
} else if f.OldMode != 0 {
121+
fmt.Fprintf(fm, "new mode %o\n", f.NewMode)
122+
}
123+
}
124+
125+
if f.Score > 0 {
126+
if f.IsCopy || f.IsRename {
127+
fmt.Fprintf(fm, "similarity index %d%%\n", f.Score)
128+
} else {
129+
fmt.Fprintf(fm, "dissimilarity index %d%%\n", f.Score)
130+
}
131+
}
132+
133+
if f.IsCopy {
134+
if f.OldName != "" {
135+
fm.WriteString("copy from ")
136+
fm.WriteQuotedName(f.OldName)
137+
fm.WriteByte('\n')
138+
}
139+
if f.NewName != "" {
140+
fm.WriteString("copy to ")
141+
fm.WriteQuotedName(f.NewName)
142+
fm.WriteByte('\n')
143+
}
144+
}
145+
146+
if f.IsRename {
147+
if f.OldName != "" {
148+
fm.WriteString("rename from ")
149+
fm.WriteQuotedName(f.OldName)
150+
fm.WriteByte('\n')
151+
}
152+
if f.NewName != "" {
153+
fm.WriteString("rename to ")
154+
fm.WriteQuotedName(f.NewName)
155+
fm.WriteByte('\n')
156+
}
157+
}
158+
159+
if f.OldOIDPrefix != "" && f.NewOIDPrefix != "" {
160+
fmt.Fprintf(fm, "index %s..%s", f.OldOIDPrefix, f.NewOIDPrefix)
161+
162+
// Mode is only included on the index line when it is not changing
163+
if f.OldMode != 0 && ((f.NewMode == 0 && !f.IsDelete) || f.OldMode == f.NewMode) {
164+
fmt.Fprintf(fm, " %o", f.OldMode)
165+
}
166+
167+
fm.WriteByte('\n')
168+
}
169+
170+
if f.IsBinary {
171+
if f.BinaryFragment == nil {
172+
fm.WriteString("Binary files fmer\n")
173+
} else {
174+
fm.WriteString("GIT binary patch\n")
175+
fm.FormatBinaryFragment(f.BinaryFragment)
176+
if f.ReverseBinaryFragment != nil {
177+
fm.FormatBinaryFragment(f.ReverseBinaryFragment)
178+
}
179+
}
180+
}
181+
182+
// The "---" and "+++" lines only appear for text patches with fragments
183+
if len(f.TextFragments) > 0 {
184+
fm.WriteString("--- ")
185+
if f.OldName == "" {
186+
fm.WriteString("/dev/null")
187+
} else {
188+
fm.WriteQuotedName("a/" + f.OldName)
189+
}
190+
fm.WriteByte('\n')
191+
192+
fm.WriteString("+++ ")
193+
if f.NewName == "" {
194+
fm.WriteString("/dev/null")
195+
} else {
196+
fm.WriteQuotedName("b/" + f.NewName)
197+
}
198+
fm.WriteByte('\n')
199+
200+
for _, frag := range f.TextFragments {
201+
fm.FormatTextFragment(frag)
202+
}
203+
}
204+
}
205+
206+
func (fm *formatter) FormatTextFragment(f *TextFragment) {
207+
fm.FormatTextFragmentHeader(f)
208+
fm.WriteByte('\n')
209+
210+
for _, line := range f.Lines {
211+
fm.WriteString(line.Op.String())
212+
fm.WriteString(line.Line)
213+
if line.NoEOL() {
214+
fm.WriteString("\n\\ No newline at end of file\n")
215+
}
216+
}
217+
}
218+
219+
func (fm *formatter) FormatTextFragmentHeader(f *TextFragment) {
220+
fmt.Fprintf(fm, "@@ -%d,%d +%d,%d @@", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines)
221+
if f.Comment != "" {
222+
fm.WriteByte(' ')
223+
fm.WriteString(f.Comment)
224+
}
225+
}
226+
227+
func (fm *formatter) FormatBinaryFragment(f *BinaryFragment) {
228+
const (
229+
maxBytesPerLine = 52
230+
)
231+
232+
switch f.Method {
233+
case BinaryPatchDelta:
234+
fm.WriteString("delta ")
235+
case BinaryPatchLiteral:
236+
fm.WriteString("literal ")
237+
}
238+
fm.Write(strconv.AppendInt(nil, f.Size, 10))
239+
fm.WriteByte('\n')
240+
241+
data := deflateBinaryChunk(f.Data)
242+
n := (len(data) / maxBytesPerLine) * maxBytesPerLine
243+
244+
buf := make([]byte, base85Len(maxBytesPerLine))
245+
for i := 0; i < n; i += maxBytesPerLine {
246+
base85Encode(buf, data[i:i+maxBytesPerLine])
247+
fm.WriteByte('z')
248+
fm.Write(buf)
249+
fm.WriteByte('\n')
250+
}
251+
if remainder := len(data) - n; remainder > 0 {
252+
buf = buf[0:base85Len(remainder)]
253+
254+
sizeChar := byte(remainder)
255+
if remainder <= 26 {
256+
sizeChar = 'A' + sizeChar - 1
257+
} else {
258+
sizeChar = 'a' + sizeChar - 27
259+
}
260+
261+
base85Encode(buf, data[n:])
262+
fm.WriteByte(sizeChar)
263+
fm.Write(buf)
264+
fm.WriteByte('\n')
265+
}
266+
fm.WriteByte('\n')
267+
}
268+
269+
func deflateBinaryChunk(data []byte) []byte {
270+
var b bytes.Buffer
271+
272+
zw := zlib.NewWriter(&b)
273+
_, _ = zw.Write(data)
274+
_ = zw.Close()
275+
276+
return b.Bytes()
277+
}

gitdiff/gitdiff_string_test.go renamed to gitdiff/format_roundtrip_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"testing"
1010
)
1111

12-
func TestParseRoundtrip(t *testing.T) {
12+
func TestFormatRoundtrip(t *testing.T) {
1313
patches := []struct {
1414
File string
1515
SkipTextCompare bool

gitdiff/quote_test.go renamed to gitdiff/format_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"testing"
66
)
77

8-
func TestWriteQuotedName(t *testing.T) {
8+
func TestFormatter_WriteQuotedName(t *testing.T) {
99
tests := []struct {
1010
Input string
1111
Expected string
@@ -20,7 +20,7 @@ func TestWriteQuotedName(t *testing.T) {
2020

2121
for _, test := range tests {
2222
var b strings.Builder
23-
writeQuotedName(&b, test.Input)
23+
newFormatter(&b).WriteQuotedName(test.Input)
2424
if b.String() != test.Expected {
2525
t.Errorf("expected %q, got %q", test.Expected, b.String())
2626
}

0 commit comments

Comments
 (0)