From 1b98c0069a1d1057c6f3c588b02b15f5419c7afb Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Thu, 30 Apr 2020 22:41:18 +0100 Subject: plumbing: diff, refresh internals --- plumbing/format/diff/unified_encoder.go | 382 ++++++++++++++------------------ 1 file changed, 166 insertions(+), 216 deletions(-) (limited to 'plumbing') diff --git a/plumbing/format/diff/unified_encoder.go b/plumbing/format/diff/unified_encoder.go index bd115f8..19c3f0b 100644 --- a/plumbing/format/diff/unified_encoder.go +++ b/plumbing/format/diff/unified_encoder.go @@ -1,70 +1,53 @@ package diff import ( - "bytes" "fmt" "io" "regexp" + "strconv" "strings" "github.com/go-git/go-git/v5/plumbing" ) -const ( - diffInit = "diff --git a/%s b/%s\n" +// DefaultContextLines is the default number of context lines. +const DefaultContextLines = 3 - chunkStart = "@@ -" - chunkMiddle = " +" - chunkEnd = " @@" - chunkCount = "%d,%d" +var ( + splitLinesRegexp = regexp.MustCompile(`[^\n]*(\n|$)`) - noFilePath = "/dev/null" - aDir = "a/" - bDir = "b/" - - fPath = "--- %s\n" - tPath = "+++ %s\n" - binary = "Binary files %s and %s differ\n" - - addLine = "%s+%s%s%s" - deleteLine = "%s-%s%s%s" - equalLine = "%s %s%s%s" - noNewLine = "\n\\ No newline at end of file\n" - - oldMode = "old mode %o\n" - newMode = "new mode %o\n" - deletedFileMode = "deleted file mode %o\n" - newFileMode = "new file mode %o\n" - - renameFrom = "from" - renameTo = "to" - renameFileMode = "rename %s %s\n" - - indexAndMode = "index %s..%s %o\n" - indexNoMode = "index %s..%s\n" + operationChar = map[Operation]byte{ + Add: '+', + Delete: '-', + Equal: ' ', + } - DefaultContextLines = 3 + operationColorKey = map[Operation]ColorKey{ + Add: New, + Delete: Old, + Equal: Context, + } ) -// UnifiedEncoder encodes an unified diff into the provided Writer. -// There are some unsupported features: -// - Similarity index for renames -// - Sort hash representation +// UnifiedEncoder encodes an unified diff into the provided Writer. It does not +// support similarity index for renames or sorting hash representations. type UnifiedEncoder struct { io.Writer - // ctxLines is the count of unchanged lines that will appear - // surrounding a change. - ctxLines int + // contextLines is the count of unchanged lines that will appear surrounding + // a change. + contextLines int // colorConfig is the color configuration. The default is no color. color ColorConfig - - buf bytes.Buffer } -func NewUnifiedEncoder(w io.Writer, ctxLines int) *UnifiedEncoder { - return &UnifiedEncoder{ctxLines: ctxLines, Writer: w} +// NewUnifiedEncoder returns a new UnifiedEncoder that writes to w. +func NewUnifiedEncoder(w io.Writer, contextLines int) *UnifiedEncoder { + return &UnifiedEncoder{ + Writer: w, + contextLines: contextLines, + } } // SetColor sets e's color configuration and returns e. @@ -73,102 +56,78 @@ func (e *UnifiedEncoder) SetColor(colorConfig ColorConfig) *UnifiedEncoder { return e } +// Encode encodes patch. func (e *UnifiedEncoder) Encode(patch Patch) error { - e.printMessage(patch.Message()) + sb := &strings.Builder{} - if err := e.encodeFilePatch(patch.FilePatches()); err != nil { - return err - } - - _, err := e.buf.WriteTo(e) - - return err -} - -func (e *UnifiedEncoder) encodeFilePatch(filePatches []FilePatch) error { - for _, p := range filePatches { - f, t := p.Files() - if err := e.header(f, t, p.IsBinary()); err != nil { - return err + if message := patch.Message(); message != "" { + sb.WriteString(message) + if !strings.HasSuffix(message, "\n") { + sb.WriteByte('\n') } + } - g := newHunksGenerator(p.Chunks(), e.ctxLines) - for _, c := range g.Generate() { - c.WriteTo(&e.buf, e.color) + 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) } } - return nil + _, err := e.Write([]byte(sb.String())) + return err } -func (e *UnifiedEncoder) printMessage(message string) { - isEmpty := message == "" - hasSuffix := strings.HasSuffix(message, "\n") - if !isEmpty && !hasSuffix { - message += "\n" +func (e *UnifiedEncoder) writeFilePatchHeader(sb *strings.Builder, filePatch FilePatch) { + from, to := filePatch.Files() + if from == nil && to == nil { + return } + isBinary := filePatch.IsBinary() - e.buf.WriteString(message) -} - -func (e *UnifiedEncoder) header(from, to File, isBinary bool) error { + sb.WriteString(e.color[Meta]) switch { - case from == nil && to == nil: - return nil case from != nil && to != nil: - e.buf.WriteString(e.color[Meta]) - hashEquals := from.Hash() == to.Hash() - - fmt.Fprintf(&e.buf, diffInit, from.Path(), to.Path()) - + fmt.Fprintf(sb, "diff --git a/%s b/%s\n", from.Path(), to.Path()) if from.Mode() != to.Mode() { - fmt.Fprintf(&e.buf, oldMode+newMode, from.Mode(), to.Mode()) + fmt.Fprintf(sb, "old mode %o\n", from.Mode()) + fmt.Fprintf(sb, "new mode %o\n", to.Mode()) } - if from.Path() != to.Path() { - fmt.Fprintf(&e.buf, - renameFileMode+renameFileMode, - renameFrom, from.Path(), renameTo, to.Path()) + fmt.Fprintf(sb, "rename from %s\n", from.Path()) + fmt.Fprintf(sb, "rename to %s\n", to.Path()) } - if from.Mode() != to.Mode() && !hashEquals { - fmt.Fprintf(&e.buf, indexNoMode, from.Hash(), to.Hash()) + fmt.Fprintf(sb, "index %s..%s\n", from.Hash(), to.Hash()) } else if !hashEquals { - fmt.Fprintf(&e.buf, indexAndMode, from.Hash(), to.Hash(), from.Mode()) + fmt.Fprintf(sb, "index %s..%s %o\n", from.Hash(), to.Hash(), from.Mode()) } - if !hashEquals { - e.pathLines(isBinary, aDir+from.Path(), bDir+to.Path()) + e.writePathLines(sb, "a/"+from.Path(), "b/"+to.Path(), isBinary) } - - e.buf.WriteString(e.color.Reset(Meta)) case from == nil: - e.buf.WriteString(e.color[Meta]) - fmt.Fprintf(&e.buf, diffInit, to.Path(), to.Path()) - fmt.Fprintf(&e.buf, newFileMode, to.Mode()) - fmt.Fprintf(&e.buf, indexNoMode, plumbing.ZeroHash, to.Hash()) - e.pathLines(isBinary, noFilePath, bDir+to.Path()) - e.buf.WriteString(e.color.Reset(Meta)) + fmt.Fprintf(sb, "diff --git a/%s b/%s\n", to.Path(), to.Path()) + fmt.Fprintf(sb, "new file mode %o\n", to.Mode()) + fmt.Fprintf(sb, "index %s..%s\n", plumbing.ZeroHash, to.Hash()) + e.writePathLines(sb, "/dev/null", "b/"+to.Path(), isBinary) case to == nil: - e.buf.WriteString(e.color[Meta]) - fmt.Fprintf(&e.buf, diffInit, from.Path(), from.Path()) - fmt.Fprintf(&e.buf, deletedFileMode, from.Mode()) - fmt.Fprintf(&e.buf, indexNoMode, from.Hash(), plumbing.ZeroHash) - e.pathLines(isBinary, aDir+from.Path(), noFilePath) - e.buf.WriteString(e.color.Reset(Meta)) + fmt.Fprintf(sb, "diff --git a/%s b/%s\n", from.Path(), from.Path()) + fmt.Fprintf(sb, "deleted file mode %o\n", from.Mode()) + fmt.Fprintf(sb, "index %s..%s\n", from.Hash(), plumbing.ZeroHash) + e.writePathLines(sb, "a/"+from.Path(), "/dev/null", isBinary) } - - return nil + sb.WriteString(e.color.Reset(Meta)) } -func (e *UnifiedEncoder) pathLines(isBinary bool, fromPath, toPath string) { - format := fPath + tPath +func (e *UnifiedEncoder) writePathLines(sb *strings.Builder, fromPath, toPath string, isBinary bool) { if isBinary { - format = binary + fmt.Fprintf(sb, "Binary files %s and %s differ\n", fromPath, toPath) + } else { + fmt.Fprintf(sb, "--- %s\n", fromPath) + fmt.Fprintf(sb, "+++ %s\n", toPath) } - - fmt.Fprintf(&e.buf, format, fromPath, toPath) } type hunksGenerator struct { @@ -187,84 +146,84 @@ func newHunksGenerator(chunks []Chunk, ctxLines int) *hunksGenerator { } } -func (c *hunksGenerator) Generate() []*hunk { - for i, chunk := range c.chunks { - ls := splitLines(chunk.Content()) - lsLen := len(ls) +func (g *hunksGenerator) Generate() []*hunk { + for i, chunk := range g.chunks { + lines := splitLines(chunk.Content()) + nLines := len(lines) switch chunk.Type() { case Equal: - c.fromLine += lsLen - c.toLine += lsLen - c.processEqualsLines(ls, i) + g.fromLine += nLines + g.toLine += nLines + g.processEqualsLines(lines, i) case Delete: - if lsLen != 0 { - c.fromLine++ + if nLines != 0 { + g.fromLine++ } - c.processHunk(i, chunk.Type()) - c.fromLine += lsLen - 1 - c.current.AddOp(chunk.Type(), ls...) + g.processHunk(i, chunk.Type()) + g.fromLine += nLines - 1 + g.current.AddOp(chunk.Type(), lines...) case Add: - if lsLen != 0 { - c.toLine++ + if nLines != 0 { + g.toLine++ } - c.processHunk(i, chunk.Type()) - c.toLine += lsLen - 1 - c.current.AddOp(chunk.Type(), ls...) + g.processHunk(i, chunk.Type()) + g.toLine += nLines - 1 + g.current.AddOp(chunk.Type(), lines...) } - if i == len(c.chunks)-1 && c.current != nil { - c.hunks = append(c.hunks, c.current) + if i == len(g.chunks)-1 && g.current != nil { + g.hunks = append(g.hunks, g.current) } } - return c.hunks + return g.hunks } -func (c *hunksGenerator) processHunk(i int, op Operation) { - if c.current != nil { +func (g *hunksGenerator) processHunk(i int, op Operation) { + if g.current != nil { return } var ctxPrefix string - linesBefore := len(c.beforeContext) - if linesBefore > c.ctxLines { - ctxPrefix = c.beforeContext[linesBefore-c.ctxLines-1] - c.beforeContext = c.beforeContext[linesBefore-c.ctxLines:] - linesBefore = c.ctxLines + 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 } - c.current = &hunk{ctxPrefix: strings.TrimSuffix(ctxPrefix, "\n")} - c.current.AddOp(Equal, c.beforeContext...) + g.current = &hunk{ctxPrefix: strings.TrimSuffix(ctxPrefix, "\n")} + g.current.AddOp(Equal, g.beforeContext...) switch op { case Delete: - c.current.fromLine, c.current.toLine = - c.addLineNumbers(c.fromLine, c.toLine, linesBefore, i, Add) + g.current.fromLine, g.current.toLine = + g.addLineNumbers(g.fromLine, g.toLine, linesBefore, i, Add) case Add: - c.current.toLine, c.current.fromLine = - c.addLineNumbers(c.toLine, c.fromLine, linesBefore, i, Delete) + g.current.toLine, g.current.fromLine = + g.addLineNumbers(g.toLine, g.fromLine, linesBefore, i, Delete) } - c.beforeContext = nil + g.beforeContext = nil } -// addLineNumbers obtains the line numbers in a new chunk -func (c *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op Operation) (cla, clb int) { +// 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) { cla = la - linesBefore // we need to search for a reference for the next diff switch { - case linesBefore != 0 && c.ctxLines != 0: - if lb > c.ctxLines { - clb = lb - c.ctxLines + 1 + case linesBefore != 0 && g.ctxLines != 0: + if lb > g.ctxLines { + clb = lb - g.ctxLines + 1 } else { clb = 1 } - case c.ctxLines == 0: + case g.ctxLines == 0: clb = lb - case i != len(c.chunks)-1: - next := c.chunks[i+1] + case i != len(g.chunks)-1: + next := g.chunks[i+1] if next.Type() == op || next.Type() == Equal { // this diff will be into this chunk clb = lb + 1 @@ -274,34 +233,32 @@ func (c *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op O return } -func (c *hunksGenerator) processEqualsLines(ls []string, i int) { - if c.current == nil { - c.beforeContext = append(c.beforeContext, ls...) +func (g *hunksGenerator) processEqualsLines(ls []string, i int) { + if g.current == nil { + g.beforeContext = append(g.beforeContext, ls...) return } - c.afterContext = append(c.afterContext, ls...) - if len(c.afterContext) <= c.ctxLines*2 && i != len(c.chunks)-1 { - c.current.AddOp(Equal, c.afterContext...) - c.afterContext = nil + 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 } else { - ctxLines := c.ctxLines - if ctxLines > len(c.afterContext) { - ctxLines = len(c.afterContext) + ctxLines := g.ctxLines + if ctxLines > len(g.afterContext) { + ctxLines = len(g.afterContext) } - c.current.AddOp(Equal, c.afterContext[:ctxLines]...) - c.hunks = append(c.hunks, c.current) + g.current.AddOp(Equal, g.afterContext[:ctxLines]...) + g.hunks = append(g.hunks, g.current) - c.current = nil - c.beforeContext = c.afterContext[ctxLines:] - c.afterContext = nil + g.current = nil + g.beforeContext = g.afterContext[ctxLines:] + g.afterContext = nil } } -var splitLinesRE = regexp.MustCompile(`[^\n]*(\n|$)`) - func splitLines(s string) []string { - out := splitLinesRE.FindAllString(s, -1) + out := splitLinesRegexp.FindAllString(s, -1) if out[len(out)-1] == "" { out = out[:len(out)-1] } @@ -319,55 +276,59 @@ type hunk struct { ops []*op } -func (c *hunk) WriteTo(buf *bytes.Buffer, color ColorConfig) { - buf.WriteString(color[Frag]) - buf.WriteString(chunkStart) +func (h *hunk) writeTo(sb *strings.Builder, color ColorConfig) { + sb.WriteString(color[Frag]) + sb.WriteString("@@ -") - if c.fromCount == 1 { - fmt.Fprintf(buf, "%d", c.fromLine) + if h.fromCount == 1 { + sb.WriteString(strconv.Itoa(h.fromLine)) } else { - fmt.Fprintf(buf, chunkCount, c.fromLine, c.fromCount) + sb.WriteString(strconv.Itoa(h.fromLine)) + sb.WriteByte(',') + sb.WriteString(strconv.Itoa(h.fromCount)) } - buf.WriteString(chunkMiddle) + sb.WriteString(" +") - if c.toCount == 1 { - fmt.Fprintf(buf, "%d", c.toLine) + if h.toCount == 1 { + sb.WriteString(strconv.Itoa(h.toLine)) } else { - fmt.Fprintf(buf, chunkCount, c.toLine, c.toCount) + sb.WriteString(strconv.Itoa(h.toLine)) + sb.WriteByte(',') + sb.WriteString(strconv.Itoa(h.toCount)) } - buf.WriteString(chunkEnd) - buf.WriteString(color.Reset(Frag)) + sb.WriteString(" @@") + sb.WriteString(color.Reset(Frag)) - if c.ctxPrefix != "" { - buf.WriteByte(' ') - buf.WriteString(color[Func]) - buf.WriteString(c.ctxPrefix) - buf.WriteString(color.Reset(Func)) + if h.ctxPrefix != "" { + sb.WriteByte(' ') + sb.WriteString(color[Func]) + sb.WriteString(h.ctxPrefix) + sb.WriteString(color.Reset(Func)) } - buf.WriteByte('\n') + sb.WriteByte('\n') - for _, d := range c.ops { - buf.WriteString(d.String(color)) + for _, op := range h.ops { + op.writeTo(sb, color) } } -func (c *hunk) AddOp(t Operation, s ...string) { - ls := len(s) +func (h *hunk) AddOp(t Operation, ss ...string) { + n := len(ss) switch t { case Add: - c.toCount += ls + h.toCount += n case Delete: - c.fromCount += ls + h.fromCount += n case Equal: - c.toCount += ls - c.fromCount += ls + h.toCount += n + h.fromCount += n } - for _, l := range s { - c.ops = append(c.ops, &op{l, t}) + for _, s := range ss { + h.ops = append(h.ops, &op{s, t}) } } @@ -376,24 +337,13 @@ type op struct { t Operation } -func (o *op) String(color ColorConfig) string { - var prefix, suffix string - var colorKey ColorKey - switch o.t { - case Add: - prefix = addLine - colorKey = New - case Delete: - prefix = deleteLine - colorKey = Old - case Equal: - prefix = equalLine - colorKey = Context +func (o *op) writeTo(sb *strings.Builder, color ColorConfig) { + colorKey := operationColorKey[o.t] + sb.WriteString(color[colorKey]) + sb.WriteByte(operationChar[o.t]) + sb.WriteString(o.text) + sb.WriteString(color.Reset(colorKey)) + if !strings.HasSuffix(o.text, "\n") { + sb.WriteString("\n\\ No newline at end of file\n") } - n := len(o.text) - if n > 0 && o.text[n-1] != '\n' { - suffix = noNewLine - } - - return fmt.Sprintf(prefix, color[colorKey], o.text, color.Reset(colorKey), suffix) } -- cgit