2018-11-27 22:52:20 +01:00
|
|
|
package diff
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2020-03-17 17:19:58 +01:00
|
|
|
"regexp"
|
2020-06-18 01:29:38 +02:00
|
|
|
"strconv"
|
2018-11-27 22:52:20 +01:00
|
|
|
"strings"
|
|
|
|
|
2020-03-17 17:19:58 +01:00
|
|
|
"github.com/go-git/go-git/v5/plumbing"
|
2018-11-27 22:52:20 +01:00
|
|
|
)
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
// DefaultContextLines is the default number of context lines.
|
|
|
|
const DefaultContextLines = 3
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
var (
|
|
|
|
splitLinesRegexp = regexp.MustCompile(`[^\n]*(\n|$)`)
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
operationChar = map[Operation]byte{
|
|
|
|
Add: '+',
|
|
|
|
Delete: '-',
|
|
|
|
Equal: ' ',
|
|
|
|
}
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
operationColorKey = map[Operation]ColorKey{
|
|
|
|
Add: New,
|
|
|
|
Delete: Old,
|
|
|
|
Equal: Context,
|
|
|
|
}
|
2018-11-27 22:52:20 +01:00
|
|
|
)
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
// UnifiedEncoder encodes an unified diff into the provided Writer. It does not
|
|
|
|
// support similarity index for renames or sorting hash representations.
|
2018-11-27 22:52:20 +01:00
|
|
|
type UnifiedEncoder struct {
|
|
|
|
io.Writer
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
// contextLines is the count of unchanged lines that will appear surrounding
|
|
|
|
// a change.
|
|
|
|
contextLines int
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2021-04-23 02:08:53 +02:00
|
|
|
// srcPrefix and dstPrefix are prepended to file paths when encoding a diff.
|
|
|
|
srcPrefix string
|
|
|
|
dstPrefix string
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
// colorConfig is the color configuration. The default is no color.
|
|
|
|
color ColorConfig
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
// NewUnifiedEncoder returns a new UnifiedEncoder that writes to w.
|
|
|
|
func NewUnifiedEncoder(w io.Writer, contextLines int) *UnifiedEncoder {
|
|
|
|
return &UnifiedEncoder{
|
|
|
|
Writer: w,
|
2021-04-23 02:08:53 +02:00
|
|
|
srcPrefix: "a/",
|
|
|
|
dstPrefix: "b/",
|
2020-06-18 01:29:38 +02:00
|
|
|
contextLines: contextLines,
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
2020-06-18 01:29:38 +02:00
|
|
|
}
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
// SetColor sets e's color configuration and returns e.
|
|
|
|
func (e *UnifiedEncoder) SetColor(colorConfig ColorConfig) *UnifiedEncoder {
|
|
|
|
e.color = colorConfig
|
|
|
|
return e
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2021-04-23 02:08:53 +02:00
|
|
|
// SetSrcPrefix sets e's srcPrefix and returns e.
|
|
|
|
func (e *UnifiedEncoder) SetSrcPrefix(prefix string) *UnifiedEncoder {
|
|
|
|
e.srcPrefix = prefix
|
|
|
|
return e
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetDstPrefix sets e's dstPrefix and returns e.
|
|
|
|
func (e *UnifiedEncoder) SetDstPrefix(prefix string) *UnifiedEncoder {
|
|
|
|
e.dstPrefix = prefix
|
|
|
|
return e
|
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
// Encode encodes patch.
|
|
|
|
func (e *UnifiedEncoder) Encode(patch Patch) error {
|
|
|
|
sb := &strings.Builder{}
|
|
|
|
|
|
|
|
if message := patch.Message(); message != "" {
|
|
|
|
sb.WriteString(message)
|
|
|
|
if !strings.HasSuffix(message, "\n") {
|
|
|
|
sb.WriteByte('\n')
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
2020-06-18 01:29:38 +02:00
|
|
|
}
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
for _, filePatch := range patch.FilePatches() {
|
|
|
|
e.writeFilePatchHeader(sb, filePatch)
|
|
|
|
g := newHunksGenerator(filePatch.Chunks(), e.contextLines)
|
|
|
|
for _, hunk := range g.Generate() {
|
|
|
|
hunk.writeTo(sb, e.color)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
_, err := e.Write([]byte(sb.String()))
|
|
|
|
return err
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
func (e *UnifiedEncoder) writeFilePatchHeader(sb *strings.Builder, filePatch FilePatch) {
|
|
|
|
from, to := filePatch.Files()
|
|
|
|
if from == nil && to == nil {
|
|
|
|
return
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
2020-06-18 01:29:38 +02:00
|
|
|
isBinary := filePatch.IsBinary()
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
var lines []string
|
2018-11-27 22:52:20 +01:00
|
|
|
switch {
|
|
|
|
case from != nil && to != nil:
|
|
|
|
hashEquals := from.Hash() == to.Hash()
|
2020-06-18 01:29:38 +02:00
|
|
|
lines = append(lines,
|
2021-04-23 02:08:53 +02:00
|
|
|
fmt.Sprintf("diff --git %s%s %s%s",
|
|
|
|
e.srcPrefix, from.Path(), e.dstPrefix, to.Path()),
|
2020-06-18 01:29:38 +02:00
|
|
|
)
|
2018-11-27 22:52:20 +01:00
|
|
|
if from.Mode() != to.Mode() {
|
2020-06-18 01:29:38 +02:00
|
|
|
lines = append(lines,
|
|
|
|
fmt.Sprintf("old mode %o", from.Mode()),
|
|
|
|
fmt.Sprintf("new mode %o", to.Mode()),
|
|
|
|
)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
if from.Path() != to.Path() {
|
2020-06-18 01:29:38 +02:00
|
|
|
lines = append(lines,
|
|
|
|
fmt.Sprintf("rename from %s", from.Path()),
|
|
|
|
fmt.Sprintf("rename to %s", to.Path()),
|
|
|
|
)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
if from.Mode() != to.Mode() && !hashEquals {
|
2020-06-18 01:29:38 +02:00
|
|
|
lines = append(lines,
|
|
|
|
fmt.Sprintf("index %s..%s", from.Hash(), to.Hash()),
|
|
|
|
)
|
2018-11-27 22:52:20 +01:00
|
|
|
} else if !hashEquals {
|
2020-06-18 01:29:38 +02:00
|
|
|
lines = append(lines,
|
|
|
|
fmt.Sprintf("index %s..%s %o", from.Hash(), to.Hash(), from.Mode()),
|
|
|
|
)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
if !hashEquals {
|
2021-04-23 02:08:53 +02:00
|
|
|
lines = e.appendPathLines(lines, e.srcPrefix+from.Path(), e.dstPrefix+to.Path(), isBinary)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
case from == nil:
|
2020-06-18 01:29:38 +02:00
|
|
|
lines = append(lines,
|
2021-04-23 02:08:53 +02:00
|
|
|
fmt.Sprintf("diff --git %s %s", e.srcPrefix+to.Path(), e.dstPrefix+to.Path()),
|
2020-06-18 01:29:38 +02:00
|
|
|
fmt.Sprintf("new file mode %o", to.Mode()),
|
|
|
|
fmt.Sprintf("index %s..%s", plumbing.ZeroHash, to.Hash()),
|
|
|
|
)
|
2021-04-23 02:08:53 +02:00
|
|
|
lines = e.appendPathLines(lines, "/dev/null", e.dstPrefix+to.Path(), isBinary)
|
2018-11-27 22:52:20 +01:00
|
|
|
case to == nil:
|
2020-06-18 01:29:38 +02:00
|
|
|
lines = append(lines,
|
2021-04-23 02:08:53 +02:00
|
|
|
fmt.Sprintf("diff --git %s %s", e.srcPrefix+from.Path(), e.dstPrefix+from.Path()),
|
2020-06-18 01:29:38 +02:00
|
|
|
fmt.Sprintf("deleted file mode %o", from.Mode()),
|
|
|
|
fmt.Sprintf("index %s..%s", from.Hash(), plumbing.ZeroHash),
|
|
|
|
)
|
2021-04-23 02:08:53 +02:00
|
|
|
lines = e.appendPathLines(lines, e.srcPrefix+from.Path(), "/dev/null", isBinary)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
sb.WriteString(e.color[Meta])
|
|
|
|
sb.WriteString(lines[0])
|
|
|
|
for _, line := range lines[1:] {
|
|
|
|
sb.WriteByte('\n')
|
|
|
|
sb.WriteString(line)
|
|
|
|
}
|
|
|
|
sb.WriteString(e.color.Reset(Meta))
|
|
|
|
sb.WriteByte('\n')
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
func (e *UnifiedEncoder) appendPathLines(lines []string, fromPath, toPath string, isBinary bool) []string {
|
2018-11-27 22:52:20 +01:00
|
|
|
if isBinary {
|
2020-06-18 01:29:38 +02:00
|
|
|
return append(lines,
|
|
|
|
fmt.Sprintf("Binary files %s and %s differ", fromPath, toPath),
|
|
|
|
)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
2020-06-18 01:29:38 +02:00
|
|
|
return append(lines,
|
|
|
|
fmt.Sprintf("--- %s", fromPath),
|
|
|
|
fmt.Sprintf("+++ %s", toPath),
|
|
|
|
)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type hunksGenerator struct {
|
|
|
|
fromLine, toLine int
|
|
|
|
ctxLines int
|
|
|
|
chunks []Chunk
|
|
|
|
current *hunk
|
|
|
|
hunks []*hunk
|
|
|
|
beforeContext, afterContext []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func newHunksGenerator(chunks []Chunk, ctxLines int) *hunksGenerator {
|
|
|
|
return &hunksGenerator{
|
|
|
|
chunks: chunks,
|
|
|
|
ctxLines: ctxLines,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
func (g *hunksGenerator) Generate() []*hunk {
|
|
|
|
for i, chunk := range g.chunks {
|
|
|
|
lines := splitLines(chunk.Content())
|
|
|
|
nLines := len(lines)
|
2018-11-27 22:52:20 +01:00
|
|
|
|
|
|
|
switch chunk.Type() {
|
|
|
|
case Equal:
|
2020-06-18 01:29:38 +02:00
|
|
|
g.fromLine += nLines
|
|
|
|
g.toLine += nLines
|
|
|
|
g.processEqualsLines(lines, i)
|
2018-11-27 22:52:20 +01:00
|
|
|
case Delete:
|
2020-06-18 01:29:38 +02:00
|
|
|
if nLines != 0 {
|
|
|
|
g.fromLine++
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
g.processHunk(i, chunk.Type())
|
|
|
|
g.fromLine += nLines - 1
|
|
|
|
g.current.AddOp(chunk.Type(), lines...)
|
2018-11-27 22:52:20 +01:00
|
|
|
case Add:
|
2020-06-18 01:29:38 +02:00
|
|
|
if nLines != 0 {
|
|
|
|
g.toLine++
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
2020-06-18 01:29:38 +02:00
|
|
|
g.processHunk(i, chunk.Type())
|
|
|
|
g.toLine += nLines - 1
|
|
|
|
g.current.AddOp(chunk.Type(), lines...)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
if i == len(g.chunks)-1 && g.current != nil {
|
|
|
|
g.hunks = append(g.hunks, g.current)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
return g.hunks
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
func (g *hunksGenerator) processHunk(i int, op Operation) {
|
|
|
|
if g.current != nil {
|
2018-11-27 22:52:20 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var ctxPrefix string
|
2020-06-18 01:29:38 +02:00
|
|
|
linesBefore := len(g.beforeContext)
|
|
|
|
if linesBefore > g.ctxLines {
|
|
|
|
ctxPrefix = g.beforeContext[linesBefore-g.ctxLines-1]
|
|
|
|
g.beforeContext = g.beforeContext[linesBefore-g.ctxLines:]
|
|
|
|
linesBefore = g.ctxLines
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
g.current = &hunk{ctxPrefix: strings.TrimSuffix(ctxPrefix, "\n")}
|
|
|
|
g.current.AddOp(Equal, g.beforeContext...)
|
2018-11-27 22:52:20 +01:00
|
|
|
|
|
|
|
switch op {
|
|
|
|
case Delete:
|
2020-06-18 01:29:38 +02:00
|
|
|
g.current.fromLine, g.current.toLine =
|
|
|
|
g.addLineNumbers(g.fromLine, g.toLine, linesBefore, i, Add)
|
2018-11-27 22:52:20 +01:00
|
|
|
case Add:
|
2020-06-18 01:29:38 +02:00
|
|
|
g.current.toLine, g.current.fromLine =
|
|
|
|
g.addLineNumbers(g.toLine, g.fromLine, linesBefore, i, Delete)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
g.beforeContext = nil
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
// addLineNumbers obtains the line numbers in a new chunk.
|
|
|
|
func (g *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op Operation) (cla, clb int) {
|
2018-11-27 22:52:20 +01:00
|
|
|
cla = la - linesBefore
|
|
|
|
// we need to search for a reference for the next diff
|
|
|
|
switch {
|
2020-06-18 01:29:38 +02:00
|
|
|
case linesBefore != 0 && g.ctxLines != 0:
|
|
|
|
if lb > g.ctxLines {
|
|
|
|
clb = lb - g.ctxLines + 1
|
2018-11-27 22:52:20 +01:00
|
|
|
} else {
|
|
|
|
clb = 1
|
|
|
|
}
|
2020-06-18 01:29:38 +02:00
|
|
|
case g.ctxLines == 0:
|
2018-11-27 22:52:20 +01:00
|
|
|
clb = lb
|
2020-06-18 01:29:38 +02:00
|
|
|
case i != len(g.chunks)-1:
|
|
|
|
next := g.chunks[i+1]
|
2018-11-27 22:52:20 +01:00
|
|
|
if next.Type() == op || next.Type() == Equal {
|
|
|
|
// this diff will be into this chunk
|
|
|
|
clb = lb + 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
func (g *hunksGenerator) processEqualsLines(ls []string, i int) {
|
|
|
|
if g.current == nil {
|
|
|
|
g.beforeContext = append(g.beforeContext, ls...)
|
2018-11-27 22:52:20 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
g.afterContext = append(g.afterContext, ls...)
|
|
|
|
if len(g.afterContext) <= g.ctxLines*2 && i != len(g.chunks)-1 {
|
|
|
|
g.current.AddOp(Equal, g.afterContext...)
|
|
|
|
g.afterContext = nil
|
2018-11-27 22:52:20 +01:00
|
|
|
} else {
|
2020-06-18 01:29:38 +02:00
|
|
|
ctxLines := g.ctxLines
|
|
|
|
if ctxLines > len(g.afterContext) {
|
|
|
|
ctxLines = len(g.afterContext)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
2020-06-18 01:29:38 +02:00
|
|
|
g.current.AddOp(Equal, g.afterContext[:ctxLines]...)
|
|
|
|
g.hunks = append(g.hunks, g.current)
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
g.current = nil
|
|
|
|
g.beforeContext = g.afterContext[ctxLines:]
|
|
|
|
g.afterContext = nil
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func splitLines(s string) []string {
|
2020-06-18 01:29:38 +02:00
|
|
|
out := splitLinesRegexp.FindAllString(s, -1)
|
2018-11-27 22:52:20 +01:00
|
|
|
if out[len(out)-1] == "" {
|
|
|
|
out = out[:len(out)-1]
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
type hunk struct {
|
|
|
|
fromLine int
|
|
|
|
toLine int
|
|
|
|
|
|
|
|
fromCount int
|
|
|
|
toCount int
|
|
|
|
|
|
|
|
ctxPrefix string
|
|
|
|
ops []*op
|
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
func (h *hunk) writeTo(sb *strings.Builder, color ColorConfig) {
|
|
|
|
sb.WriteString(color[Frag])
|
|
|
|
sb.WriteString("@@ -")
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
if h.fromCount == 1 {
|
|
|
|
sb.WriteString(strconv.Itoa(h.fromLine))
|
2018-11-27 22:52:20 +01:00
|
|
|
} else {
|
2020-06-18 01:29:38 +02:00
|
|
|
sb.WriteString(strconv.Itoa(h.fromLine))
|
|
|
|
sb.WriteByte(',')
|
|
|
|
sb.WriteString(strconv.Itoa(h.fromCount))
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
sb.WriteString(" +")
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
if h.toCount == 1 {
|
|
|
|
sb.WriteString(strconv.Itoa(h.toLine))
|
2018-11-27 22:52:20 +01:00
|
|
|
} else {
|
2020-06-18 01:29:38 +02:00
|
|
|
sb.WriteString(strconv.Itoa(h.toLine))
|
|
|
|
sb.WriteByte(',')
|
|
|
|
sb.WriteString(strconv.Itoa(h.toCount))
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
sb.WriteString(" @@")
|
|
|
|
sb.WriteString(color.Reset(Frag))
|
2018-11-27 22:52:20 +01:00
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
if h.ctxPrefix != "" {
|
|
|
|
sb.WriteByte(' ')
|
|
|
|
sb.WriteString(color[Func])
|
|
|
|
sb.WriteString(h.ctxPrefix)
|
|
|
|
sb.WriteString(color.Reset(Func))
|
|
|
|
}
|
|
|
|
|
|
|
|
sb.WriteByte('\n')
|
|
|
|
|
|
|
|
for _, op := range h.ops {
|
|
|
|
op.writeTo(sb, color)
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
func (h *hunk) AddOp(t Operation, ss ...string) {
|
|
|
|
n := len(ss)
|
2018-11-27 22:52:20 +01:00
|
|
|
switch t {
|
|
|
|
case Add:
|
2020-06-18 01:29:38 +02:00
|
|
|
h.toCount += n
|
2018-11-27 22:52:20 +01:00
|
|
|
case Delete:
|
2020-06-18 01:29:38 +02:00
|
|
|
h.fromCount += n
|
2018-11-27 22:52:20 +01:00
|
|
|
case Equal:
|
2020-06-18 01:29:38 +02:00
|
|
|
h.toCount += n
|
|
|
|
h.fromCount += n
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
for _, s := range ss {
|
|
|
|
h.ops = append(h.ops, &op{s, t})
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type op struct {
|
|
|
|
text string
|
|
|
|
t Operation
|
|
|
|
}
|
|
|
|
|
2020-06-18 01:29:38 +02:00
|
|
|
func (o *op) writeTo(sb *strings.Builder, color ColorConfig) {
|
|
|
|
colorKey := operationColorKey[o.t]
|
|
|
|
sb.WriteString(color[colorKey])
|
|
|
|
sb.WriteByte(operationChar[o.t])
|
|
|
|
if strings.HasSuffix(o.text, "\n") {
|
|
|
|
sb.WriteString(strings.TrimSuffix(o.text, "\n"))
|
|
|
|
} else {
|
|
|
|
sb.WriteString(o.text + "\n\\ No newline at end of file")
|
2020-03-17 17:19:58 +01:00
|
|
|
}
|
2020-06-18 01:29:38 +02:00
|
|
|
sb.WriteString(color.Reset(colorKey))
|
|
|
|
sb.WriteByte('\n')
|
2018-11-27 22:52:20 +01:00
|
|
|
}
|