From 743920c9b9da0fb47702369c0a9d718ffd54d683 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Fri, 24 Apr 2020 03:19:37 +0100 Subject: plumbing: diff, Add initial colored output support. Fixes #33. --- internal/color/color.go | 38 +++++++++++ plumbing/format/diff/colorconfig.go | 96 ++++++++++++++++++++++++++++ plumbing/format/diff/unified_encoder.go | 40 +++++++++--- plumbing/format/diff/unified_encoder_test.go | 80 ++++++++++++++++++++++- 4 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 internal/color/color.go create mode 100644 plumbing/format/diff/colorconfig.go diff --git a/internal/color/color.go b/internal/color/color.go new file mode 100644 index 0000000..2cd74bd --- /dev/null +++ b/internal/color/color.go @@ -0,0 +1,38 @@ +package color + +// TODO read colors from a github.com/go-git/go-git/plumbing/format/config.Config struct +// TODO implement color parsing, see https://github.com/git/git/blob/v2.26.2/color.c + +// Colors. See https://github.com/git/git/blob/v2.26.2/color.h#L24-L53. +const ( + Normal = "" + Reset = "\033[m" + Bold = "\033[1m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Magenta = "\033[35m" + Cyan = "\033[36m" + BoldRed = "\033[1;31m" + BoldGreen = "\033[1;32m" + BoldYellow = "\033[1;33m" + BoldBlue = "\033[1;34m" + BoldMagenta = "\033[1;35m" + BoldCyan = "\033[1;36m" + FaintRed = "\033[2;31m" + FaintGreen = "\033[2;32m" + FaintYellow = "\033[2;33m" + FaintBlue = "\033[2;34m" + FaintMagenta = "\033[2;35m" + FaintCyan = "\033[2;36m" + BgRed = "\033[41m" + BgGreen = "\033[42m" + BgYellow = "\033[43m" + BgBlue = "\033[44m" + BgMagenta = "\033[45m" + BgCyan = "\033[46m" + Faint = "\033[2m" + FaintItalic = "\033[2;3m" + Reverse = "\033[7m" +) diff --git a/plumbing/format/diff/colorconfig.go b/plumbing/format/diff/colorconfig.go new file mode 100644 index 0000000..b7c32e6 --- /dev/null +++ b/plumbing/format/diff/colorconfig.go @@ -0,0 +1,96 @@ +package diff + +import "github.com/go-git/go-git/v5/internal/color" + +// A ColorKey is a key into a ColorConfig map and also equal to the key in the +// diff.color subsection of the config. See +// https://github.com/git/git/blob/v2.26.2/diff.c#L83-L106. +type ColorKey string + +// ColorKeys. +const ( + Context ColorKey = "context" + Meta ColorKey = "meta" + Frag ColorKey = "frag" + Old ColorKey = "old" + New ColorKey = "new" + Commit ColorKey = "commit" + Whitespace ColorKey = "whitespace" + Func ColorKey = "func" + OldMoved ColorKey = "oldMoved" + OldMovedAlternative ColorKey = "oldMovedAlternative" + OldMovedDimmed ColorKey = "oldMovedDimmed" + OldMovedAlternativeDimmed ColorKey = "oldMovedAlternativeDimmed" + NewMoved ColorKey = "newMoved" + NewMovedAlternative ColorKey = "newMovedAlternative" + NewMovedDimmed ColorKey = "newMovedDimmed" + NewMovedAlternativeDimmed ColorKey = "newMovedAlternativeDimmed" + ContextDimmed ColorKey = "contextDimmed" + OldDimmed ColorKey = "oldDimmed" + NewDimmed ColorKey = "newDimmed" + ContextBold ColorKey = "contextBold" + OldBold ColorKey = "oldBold" + NewBold ColorKey = "newBold" +) + +// A ColorConfig is a color configuration. A nil or empty ColorConfig +// corresponds to no color. +type ColorConfig map[ColorKey]string + +// A ColorConfigOption sets an option on a ColorConfig. +type ColorConfigOption func(ColorConfig) + +// WithColor sets the color for key. +func WithColor(key ColorKey, color string) ColorConfigOption { + return func(cc ColorConfig) { + cc[key] = color + } +} + +// defaultColorConfig is the default color configuration. See +// https://github.com/git/git/blob/v2.26.2/diff.c#L57-L81. +var defaultColorConfig = ColorConfig{ + Context: color.Normal, + Meta: color.Bold, + Frag: color.Cyan, + Old: color.Red, + New: color.Green, + Commit: color.Yellow, + Whitespace: color.BgRed, + Func: color.Normal, + OldMoved: color.BoldMagenta, + OldMovedAlternative: color.BoldBlue, + OldMovedDimmed: color.Faint, + OldMovedAlternativeDimmed: color.FaintItalic, + NewMoved: color.BoldCyan, + NewMovedAlternative: color.BoldYellow, + NewMovedDimmed: color.Faint, + NewMovedAlternativeDimmed: color.FaintItalic, + ContextDimmed: color.Faint, + OldDimmed: color.FaintRed, + NewDimmed: color.FaintGreen, + ContextBold: color.Bold, + OldBold: color.BoldRed, + NewBold: color.BoldGreen, +} + +// NewColorConfig returns a new ColorConfig. +func NewColorConfig(options ...ColorConfigOption) ColorConfig { + cc := make(ColorConfig) + for key, value := range defaultColorConfig { + cc[key] = value + } + for _, option := range options { + option(cc) + } + return cc +} + +// Reset returns the ANSI escape sequence to reset a color set from cc. If cc is +// nil or empty then no reset is needed so it returns the empty string. +func (cc ColorConfig) Reset() string { + if len(cc) == 0 { + return "" + } + return color.Reset +} diff --git a/plumbing/format/diff/unified_encoder.go b/plumbing/format/diff/unified_encoder.go index f2bc910..7b0c31e 100644 --- a/plumbing/format/diff/unified_encoder.go +++ b/plumbing/format/diff/unified_encoder.go @@ -26,9 +26,9 @@ const ( tPath = "+++ %s\n" binary = "Binary files %s and %s differ\n" - addLine = "+%s%s" - deleteLine = "-%s%s" - equalLine = " %s%s" + 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" @@ -57,6 +57,9 @@ type UnifiedEncoder struct { // surrounding a change. ctxLines int + // colorConfig is the color configuration. The default is no color. + color ColorConfig + buf bytes.Buffer } @@ -64,6 +67,12 @@ func NewUnifiedEncoder(w io.Writer, ctxLines int) *UnifiedEncoder { return &UnifiedEncoder{ctxLines: ctxLines, Writer: w} } +// SetColor sets e's color configuration and returns e. +func (e *UnifiedEncoder) SetColor(colorConfig ColorConfig) *UnifiedEncoder { + e.color = colorConfig + return e +} + func (e *UnifiedEncoder) Encode(patch Patch) error { e.printMessage(patch.Message()) @@ -85,7 +94,7 @@ func (e *UnifiedEncoder) encodeFilePatch(filePatches []FilePatch) error { g := newHunksGenerator(p.Chunks(), e.ctxLines) for _, c := range g.Generate() { - c.WriteTo(&e.buf) + c.WriteTo(&e.buf, e.color) } } @@ -107,6 +116,8 @@ func (e *UnifiedEncoder) header(from, to File, isBinary bool) error { 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()) @@ -130,16 +141,22 @@ func (e *UnifiedEncoder) header(from, to File, isBinary bool) error { if !hashEquals { e.pathLines(isBinary, aDir+from.Path(), bDir+to.Path()) } + + e.buf.WriteString(e.color.Reset()) 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()) 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()) } return nil @@ -302,7 +319,8 @@ type hunk struct { ops []*op } -func (c *hunk) WriteTo(buf *bytes.Buffer) { +func (c *hunk) WriteTo(buf *bytes.Buffer, color ColorConfig) { + buf.WriteString(color[Frag]) buf.WriteString(chunkStart) if c.fromCount == 1 { @@ -320,9 +338,10 @@ func (c *hunk) WriteTo(buf *bytes.Buffer) { } fmt.Fprintf(buf, chunkEnd, c.ctxPrefix) + buf.WriteString(color.Reset()) for _, d := range c.ops { - buf.WriteString(d.String()) + buf.WriteString(d.String(color)) } } @@ -348,20 +367,23 @@ type op struct { t Operation } -func (o *op) String() string { - var prefix, suffix string +func (o *op) String(color ColorConfig) string { + var setColor, prefix, suffix string switch o.t { case Add: prefix = addLine + setColor = color[New] case Delete: prefix = deleteLine + setColor = color[Old] case Equal: prefix = equalLine + setColor = color[Context] } n := len(o.text) if n > 0 && o.text[n-1] != '\n' { suffix = noNewLine } - return fmt.Sprintf(prefix, o.text, suffix) + return fmt.Sprintf(prefix, setColor, o.text, color.Reset(), suffix) } diff --git a/plumbing/format/diff/unified_encoder_test.go b/plumbing/format/diff/unified_encoder_test.go index 1a9b2dd..f72424a 100644 --- a/plumbing/format/diff/unified_encoder_test.go +++ b/plumbing/format/diff/unified_encoder_test.go @@ -4,6 +4,7 @@ import ( "bytes" "testing" + "github.com/go-git/go-git/v5/internal/color" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" @@ -56,7 +57,7 @@ func (s *UnifiedEncoderTestSuite) TestEncode(c *C) { c.Log("executing: ", f.desc) buffer := bytes.NewBuffer(nil) - e := NewUnifiedEncoder(buffer, f.context) + e := NewUnifiedEncoder(buffer, f.context).SetColor(f.color) err := e.Encode(f.patch) c.Assert(err, IsNil) @@ -860,6 +861,82 @@ index 0adddcde4fd38042c354518351820eb06c417c82..d39ae38aad7ba9447b5e7998b2e4714f +Y \ No newline at end of file `, +}, { + patch: testPatch{ + message: "", + filePatches: []testFilePatch{{ + from: &testFile{ + mode: filemode.Regular, + path: "README.md", + seed: "hello\nworld\n", + }, + to: &testFile{ + mode: filemode.Regular, + path: "README.md", + seed: "hello\nbug\n", + }, + chunks: []testChunk{{ + content: "hello\n", + op: Equal, + }, { + content: "world\n", + op: Delete, + }, { + content: "bug\n", + op: Add, + }}, + }}, + }, + desc: "positive negative number with color", + context: 2, + color: NewColorConfig(), + diff: "" + + color.Bold + "diff --git a/README.md b/README.md\n" + + "index 94954abda49de8615a048f8d2e64b5de848e27a1..f3dad9514629b9ff9136283ae331ad1fc95748a8 100644\n" + + "--- a/README.md\n" + + "+++ b/README.md\n" + + color.Reset + color.Cyan + "@@ -1,2 +1,2 @@\n" + + color.Normal + color.Reset + " hello\n" + + color.Reset + color.Red + "-world\n" + + color.Reset + color.Green + "+bug\n" + + color.Reset, +}, { + patch: testPatch{ + message: "", + filePatches: []testFilePatch{{ + from: &testFile{ + mode: filemode.Regular, + path: "test.txt", + seed: "test\n", + }, + to: &testFile{ + mode: filemode.Regular, + path: "test.txt", + seed: "test2\n", + }, + + chunks: []testChunk{{ + content: "test\n", + op: Delete, + }, { + content: "test2\n", + op: Add, + }}, + }}, + }, + + desc: "one line change with color", + context: 1, + color: NewColorConfig(), + diff: "" + + color.Bold + "diff --git a/test.txt b/test.txt\n" + + "index 9daeafb9864cf43055ae93beb0afd6c7d144bfa4..180cf8328022becee9aaa2577a8f84ea2b9f3827 100644\n" + + "--- a/test.txt\n" + + "+++ b/test.txt\n" + + color.Reset + color.Cyan + "@@ -1 +1 @@\n" + + color.Reset + color.Red + "-test\n" + + color.Reset + color.Green + "+test2\n" + + color.Reset, }} type testPatch struct { @@ -944,6 +1021,7 @@ func (t testChunk) Type() Operation { type fixture struct { desc string context int + color ColorConfig diff string patch Patch } -- cgit