aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Payne <twpayne@gmail.com>2020-04-24 03:19:37 +0100
committerTom Payne <twpayne@gmail.com>2020-04-26 19:53:10 +0100
commit743920c9b9da0fb47702369c0a9d718ffd54d683 (patch)
tree343b6903840283122f42e0b136f41867f3d20f9b
parent05d46a029600047e8e4b566ae36fc2823709d67e (diff)
downloadgo-git-743920c9b9da0fb47702369c0a9d718ffd54d683.tar.gz
plumbing: diff, Add initial colored output support. Fixes #33.
-rw-r--r--internal/color/color.go38
-rw-r--r--plumbing/format/diff/colorconfig.go96
-rw-r--r--plumbing/format/diff/unified_encoder.go40
-rw-r--r--plumbing/format/diff/unified_encoder_test.go80
4 files changed, 244 insertions, 10 deletions
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
}