master
  1package gitdiff
  2
  3import (
  4	"bytes"
  5	"compress/zlib"
  6	"fmt"
  7	"io"
  8	"strconv"
  9)
 10
 11type formatter struct {
 12	w   io.Writer
 13	err error
 14}
 15
 16func newFormatter(w io.Writer) *formatter {
 17	return &formatter{w: w}
 18}
 19
 20func (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
 30func (fm *formatter) WriteString(s string) (int, error) {
 31	fm.Write([]byte(s))
 32	return len(s), nil
 33}
 34
 35func (fm *formatter) WriteByte(c byte) error {
 36	fm.Write([]byte{c})
 37	return nil
 38}
 39
 40func (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
 59var 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
 71func 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
 86func (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 ")
173			fm.WriteQuotedName("a/" + aName)
174			fm.WriteString(" and ")
175			fm.WriteQuotedName("b/" + bName)
176			fm.WriteString(" differ\n")
177		} else {
178			fm.WriteString("GIT binary patch\n")
179			fm.FormatBinaryFragment(f.BinaryFragment)
180			if f.ReverseBinaryFragment != nil {
181				fm.FormatBinaryFragment(f.ReverseBinaryFragment)
182			}
183		}
184	}
185
186	// The "---" and "+++" lines only appear for text patches with fragments
187	if len(f.TextFragments) > 0 {
188		fm.WriteString("--- ")
189		if f.OldName == "" {
190			fm.WriteString("/dev/null")
191		} else {
192			fm.WriteQuotedName("a/" + f.OldName)
193		}
194		fm.WriteByte('\n')
195
196		fm.WriteString("+++ ")
197		if f.NewName == "" {
198			fm.WriteString("/dev/null")
199		} else {
200			fm.WriteQuotedName("b/" + f.NewName)
201		}
202		fm.WriteByte('\n')
203
204		for _, frag := range f.TextFragments {
205			fm.FormatTextFragment(frag)
206		}
207	}
208}
209
210func (fm *formatter) FormatTextFragment(f *TextFragment) {
211	fm.FormatTextFragmentHeader(f)
212	fm.WriteByte('\n')
213
214	for _, line := range f.Lines {
215		fm.WriteString(line.Op.String())
216		fm.WriteString(line.Line)
217		if line.NoEOL() {
218			fm.WriteString("\n\\ No newline at end of file\n")
219		}
220	}
221}
222
223func (fm *formatter) FormatTextFragmentHeader(f *TextFragment) {
224	fmt.Fprintf(fm, "@@ -%d,%d +%d,%d @@", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines)
225	if f.Comment != "" {
226		fm.WriteByte(' ')
227		fm.WriteString(f.Comment)
228	}
229}
230
231func (fm *formatter) FormatBinaryFragment(f *BinaryFragment) {
232	const (
233		maxBytesPerLine = 52
234	)
235
236	switch f.Method {
237	case BinaryPatchDelta:
238		fm.WriteString("delta ")
239	case BinaryPatchLiteral:
240		fm.WriteString("literal ")
241	}
242	fm.Write(strconv.AppendInt(nil, f.Size, 10))
243	fm.WriteByte('\n')
244
245	data := deflateBinaryChunk(f.Data)
246	n := (len(data) / maxBytesPerLine) * maxBytesPerLine
247
248	buf := make([]byte, base85Len(maxBytesPerLine))
249	for i := 0; i < n; i += maxBytesPerLine {
250		base85Encode(buf, data[i:i+maxBytesPerLine])
251		fm.WriteByte('z')
252		fm.Write(buf)
253		fm.WriteByte('\n')
254	}
255	if remainder := len(data) - n; remainder > 0 {
256		buf = buf[0:base85Len(remainder)]
257
258		sizeChar := byte(remainder)
259		if remainder <= 26 {
260			sizeChar = 'A' + sizeChar - 1
261		} else {
262			sizeChar = 'a' + sizeChar - 27
263		}
264
265		base85Encode(buf, data[n:])
266		fm.WriteByte(sizeChar)
267		fm.Write(buf)
268		fm.WriteByte('\n')
269	}
270	fm.WriteByte('\n')
271}
272
273func deflateBinaryChunk(data []byte) []byte {
274	var b bytes.Buffer
275
276	zw := zlib.NewWriter(&b)
277	_, _ = zw.Write(data)
278	_ = zw.Close()
279
280	return b.Bytes()
281}