Skip to content

Commit e9bd182

Browse files
authored
Merge pull request #423 from kpengboy/color-diffs
Colorize diff output
2 parents 0739f1f + 1c98e95 commit e9bd182

File tree

12 files changed

+176
-32
lines changed

12 files changed

+176
-32
lines changed

cmd/gmailctl/cmd/apply_cmd.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,18 @@ func init() {
5151
applyCmd.PersistentFlags().StringVarP(&applyFilename, "filename", "f", "", "configuration file")
5252
applyCmd.Flags().BoolVarP(&applyYes, "yes", "y", false, "don't ask for confirmation, just apply")
5353
applyCmd.Flags().BoolVarP(&applyRemoveLabels, "remove-labels", "r", false, "allow removing labels")
54-
applyCmd.Flags().BoolVarP(&applySkipTests, "yolo", "", false, "skip configuration tests")
55-
applyCmd.PersistentFlags().BoolVarP(&applyDebug, "debug", "", false, "print extra debugging information")
56-
applyCmd.PersistentFlags().IntVarP(&applyDiffContext, "diff-context", "", papply.DefaultContextLines, "number of lines of filter diff context to show")
54+
applyCmd.Flags().BoolVar(&applySkipTests, "yolo", false, "skip configuration tests")
55+
applyCmd.PersistentFlags().BoolVar(&applyDebug, "debug", false, "print extra debugging information")
56+
applyCmd.PersistentFlags().IntVar(&applyDiffContext, "diff-context", papply.DefaultContextLines, "number of lines of filter diff context to show")
5757
}
5858

5959
func apply(path string, interactive, test bool) error {
6060
if applyDiffContext < 0 {
6161
return errors.New("--diff-context must be non-negative")
6262
}
6363

64+
useColor := shouldUseColorDiff()
65+
6466
parseRes, err := parseConfig(path, "", test)
6567
if err != nil {
6668
return err
@@ -76,7 +78,7 @@ func apply(path string, interactive, test bool) error {
7678
return err
7779
}
7880

79-
diff, err := papply.Diff(parseRes.Res.GmailConfig, upstream, applyDebug, applyDiffContext)
81+
diff, err := papply.Diff(parseRes.Res.GmailConfig, upstream, applyDebug, applyDiffContext, useColor)
8082
if err != nil {
8183
return fmt.Errorf("cannot compare upstream with local config: %w", err)
8284
}

cmd/gmailctl/cmd/diff_cmd.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ func init() {
4040

4141
// Flags and configuration settings
4242
diffCmd.PersistentFlags().StringVarP(&diffFilename, "filename", "f", "", "configuration file")
43-
diffCmd.PersistentFlags().BoolVarP(&diffDebug, "debug", "", false, "print extra debugging information")
44-
diffCmd.PersistentFlags().IntVarP(&diffContext, "context", "", papply.DefaultContextLines, "number of lines of filter diff context to show")
43+
diffCmd.PersistentFlags().BoolVar(&diffDebug, "debug", false, "print extra debugging information")
44+
diffCmd.PersistentFlags().IntVar(&diffContext, "context", papply.DefaultContextLines, "number of lines of filter diff context to show")
4545
}
4646

4747
func diff(path string) error {
4848
if diffContext < 0 {
4949
return errors.New("--context must be non-negative")
5050
}
5151

52+
useColor := shouldUseColorDiff()
53+
5254
parseRes, err := parseConfig(path, "", false)
5355
if err != nil {
5456
return err
@@ -64,7 +66,7 @@ func diff(path string) error {
6466
return err
6567
}
6668

67-
diff, err := papply.Diff(parseRes.Res.GmailConfig, upstream, diffDebug, diffContext)
69+
diff, err := papply.Diff(parseRes.Res.GmailConfig, upstream, diffDebug, diffContext, useColor)
6870
if err != nil {
6971
return fmt.Errorf("cannot compare upstream with local config: %w", err)
7072
}

cmd/gmailctl/cmd/edit_cmd.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ var (
2323
editDiffContext int
2424
)
2525

26+
var editUseColor bool
27+
2628
var (
2729
defaultEditors = []string{
2830
"editor",
@@ -70,15 +72,17 @@ func init() {
7072
// Flags and configuration settings
7173
editCmd.PersistentFlags().StringVarP(&editFilename, "filename", "f", "", "configuration file")
7274
editCmd.Flags().BoolVarP(&editSkipTests, "yolo", "", false, "skip configuration tests")
73-
editCmd.PersistentFlags().BoolVarP(&editDebug, "debug", "", false, "print extra debugging information")
74-
editCmd.PersistentFlags().IntVarP(&editDiffContext, "diff-context", "", papply.DefaultContextLines, "number of lines of filter diff context to show")
75+
editCmd.PersistentFlags().BoolVar(&editDebug, "debug", false, "print extra debugging information")
76+
editCmd.PersistentFlags().IntVar(&editDiffContext, "diff-context", papply.DefaultContextLines, "number of lines of filter diff context to show")
7577
}
7678

7779
func edit(path string, test bool) error {
7880
if editDiffContext < 0 {
7981
return errors.New("--diff-context must be non-negative")
8082
}
8183

84+
editUseColor = shouldUseColorDiff()
85+
8286
// First make sure that Gmail can be contacted, so that we don't
8387
// waste the user's time editing a config file that cannot be
8488
// applied now.
@@ -229,7 +233,7 @@ func applyEdited(path, originalPath string, test bool, gmailapi *api.GmailAPI) e
229233
return err
230234
}
231235

232-
diff, err := papply.Diff(parseRes.Res.GmailConfig, upstream, editDebug, editDiffContext)
236+
diff, err := papply.Diff(parseRes.Res.GmailConfig, upstream, editDebug, editDiffContext, editUseColor)
233237
if err != nil {
234238
return errors.New("comparing upstream with local config")
235239
}

cmd/gmailctl/cmd/root_cmd.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import (
66
"os/user"
77
"path"
88

9+
"github.com/mattn/go-isatty"
910
"github.com/spf13/cobra"
1011
)
1112

1213
var cfgDir string
14+
var colorFlag string
1315

1416
// rootCmd is the command run when executing without subcommands.
1517
var rootCmd = &cobra.Command{
@@ -52,6 +54,9 @@ func init() {
5254
// Cobra supports persistent flags, which, if defined here,
5355
// will be global for your application.
5456
rootCmd.PersistentFlags().StringVar(&cfgDir, "config", "", "config directory (default is $HOME/.gmailctl)")
57+
rootCmd.PersistentFlags().StringVar(&colorFlag, "color", "auto",
58+
"whether to enable color output ('always', 'auto' or 'never')")
59+
rootCmd.PersistentFlags().Lookup("color").NoOptDefVal = "always"
5560
}
5661

5762
// initConfig reads in config file and ENV variables if set.
@@ -68,3 +73,20 @@ func initConfig() {
6873
}
6974
cfgDir = path.Join(usr.HomeDir, ".gmailctl")
7075
}
76+
77+
// shouldUseColorDiff decides, based on the value of the color flag and other
78+
// factors, whether gmailctl should use color output.
79+
func shouldUseColorDiff() bool {
80+
switch colorFlag {
81+
case "never":
82+
return false
83+
case "auto":
84+
return os.Getenv("TERM") != "dumb" &&
85+
(isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()))
86+
case "always":
87+
return true
88+
default:
89+
fatal(fmt.Errorf("--color must be 'always', 'auto' or 'never', not '%v'", colorFlag))
90+
return false
91+
}
92+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ go 1.23.7
55
toolchain go1.24.4
66

77
require (
8+
github.com/fatih/color v1.18.1-0.20241008080414-2aae7c9a9c41
89
github.com/google/go-jsonnet v0.21.0
910
github.com/gorilla/mux v1.8.1
1011
github.com/hashicorp/go-multierror v1.1.1
12+
github.com/mattn/go-isatty v0.0.20
1113
github.com/pmezard/go-difflib v1.0.0
1214
github.com/spf13/cobra v1.9.1
1315
github.com/stretchr/testify v1.10.0
@@ -30,6 +32,7 @@ require (
3032
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
3133
github.com/hashicorp/errwrap v1.1.0 // indirect
3234
github.com/inconshreveable/mousetrap v1.1.0 // indirect
35+
github.com/mattn/go-colorable v0.1.13 // indirect
3336
github.com/spf13/pflag v1.0.6 // indirect
3437
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
3538
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect

go.sum

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQ
77
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
88
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
99
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10+
github.com/fatih/color v1.18.1-0.20241008080414-2aae7c9a9c41 h1:XkQfr36qgf52W/9RfDNk/aNfBC6Cl++6uc/L+lHEKRk=
11+
github.com/fatih/color v1.18.1-0.20241008080414-2aae7c9a9c41/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
1012
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
1113
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
1214
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -42,6 +44,11 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
4244
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
4345
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
4446
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
47+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
48+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
49+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
50+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
51+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
4552
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4653
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4754
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -77,6 +84,8 @@ golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
7784
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
7885
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
7986
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
87+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
88+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8089
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
8190
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
8291
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=

integration_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func TestIntegration(t *testing.T) {
6868
require.Nil(t, err)
6969

7070
// Apply the diff.
71-
d, err := apply.Diff(pres.GmailConfig, upres, false, apply.DefaultContextLines)
71+
d, err := apply.Diff(pres.GmailConfig, upres, false, apply.DefaultContextLines, false /* colorize */)
7272
require.Nil(t, err)
7373
err = apply.Apply(d, gapi, true)
7474
require.Nil(t, err)
@@ -139,7 +139,7 @@ func TestIntegrationImportExport(t *testing.T) {
139139
require.Nil(t, err)
140140

141141
// Apply the diff.
142-
d, err := apply.Diff(pres.GmailConfig, upres, false, apply.DefaultContextLines)
142+
d, err := apply.Diff(pres.GmailConfig, upres, false, apply.DefaultContextLines, false /* colorize */)
143143
require.Nil(t, err)
144144
err = apply.Apply(d, gapi, true)
145145
require.Nil(t, err)
@@ -174,7 +174,7 @@ func TestIntegrationImportExport(t *testing.T) {
174174
}
175175

176176
func assertEmptyDiff(t *testing.T, local, remote apply.GmailConfig) {
177-
d, err := apply.Diff(local, remote, false, -1)
177+
d, err := apply.Diff(local, remote, false, -1 /* contextLines */, false /* colorize */)
178178
require.Nil(t, err)
179179
assert.True(t, d.FiltersDiff.Empty())
180180
assert.True(t, d.LabelsDiff.Empty())

internal/engine/apply/apply.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,20 @@ func (d ConfigDiff) Validate() error {
116116
}
117117

118118
// Diff computes the diff between local and upstream configuration.
119-
func Diff(local, upstream GmailConfig, debugInfo bool, contextLines int) (ConfigDiff, error) {
119+
func Diff(local, upstream GmailConfig, debugInfo bool, contextLines int, colorize bool) (ConfigDiff, error) {
120120
res := ConfigDiff{
121121
LocalConfig: local,
122122
}
123123
var err error
124124

125-
res.FiltersDiff, err = filter.Diff(upstream.Filters, local.Filters, debugInfo, contextLines)
125+
res.FiltersDiff, err = filter.Diff(upstream.Filters, local.Filters, debugInfo, contextLines, colorize)
126126
if err != nil {
127127
return res, fmt.Errorf("cannot compute filters diff: %w", err)
128128
}
129129

130130
if len(local.Labels) > 0 {
131131
// LabelsDiff management opted-in
132-
res.LabelsDiff, err = label.Diff(upstream.Labels, local.Labels)
132+
res.LabelsDiff, err = label.Diff(upstream.Labels, local.Labels, colorize)
133133
if err != nil {
134134
return res, fmt.Errorf("cannot compute labels diff: %w", err)
135135
}

internal/engine/filter/diff.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ import (
99
"github.com/pmezard/go-difflib/difflib"
1010

1111
"github.com/mbrt/gmailctl/internal/graph"
12+
"github.com/mbrt/gmailctl/internal/reporting"
1213
)
1314

1415
// Diff computes the diff between two lists of filters.
1516
//
1617
// To compute the diff, IDs are ignored, only the contents of the filters are actually considered.
17-
func Diff(upstream, local Filters, debugInfo bool, contextLines int) (FiltersDiff, error) {
18+
func Diff(upstream, local Filters, debugInfo bool, contextLines int, colorize bool) (FiltersDiff, error) {
1819
// Computing the diff is very expensive, so we have to minimize the number of filters
1920
// we have to analyze. To do so, we get rid of the filters that are exactly the same,
2021
// by hashing them.
2122
added, removed := changedFilters(upstream, local)
22-
return NewMinimalFiltersDiff(added, removed, debugInfo, contextLines), nil
23+
return NewMinimalFiltersDiff(added, removed, debugInfo, contextLines, colorize), nil
2324
}
2425

2526
// NewMinimalFiltersDiff creates a new FiltersDiff with reordered filters, where
@@ -28,11 +29,11 @@ func Diff(upstream, local Filters, debugInfo bool, contextLines int) (FiltersDif
2829
// The algorithm used is a quadratic approximation to the otherwise NP-complete
2930
// travel salesman problem. Hopefully the number of filters is low enough to
3031
// make this not too slow and the approximation not too bad.
31-
func NewMinimalFiltersDiff(added, removed Filters, printDebugInfo bool, contextLines int) FiltersDiff {
32+
func NewMinimalFiltersDiff(added, removed Filters, printDebugInfo bool, contextLines int, colorize bool) FiltersDiff {
3233
if len(added) > 0 && len(removed) > 0 {
3334
added, removed = reorderWithHungarian(added, removed)
3435
}
35-
return FiltersDiff{added, removed, printDebugInfo, contextLines}
36+
return FiltersDiff{added, removed, printDebugInfo, contextLines, colorize}
3637
}
3738

3839
// FiltersDiff contains filters that have been added and removed locally with respect to upstream.
@@ -41,6 +42,7 @@ type FiltersDiff struct {
4142
Removed Filters
4243
PrintDebugInfo bool
4344
ContextLines int
45+
Colorize bool
4446
}
4547

4648
// Empty returns true if the diff is empty.
@@ -69,6 +71,10 @@ func (f FiltersDiff) String() string {
6971
// We can't get a diff apparently, let's make something up here
7072
return fmt.Sprintf("Removed:\n%s\nAdded:\n%s", removed, added)
7173
}
74+
if f.Colorize {
75+
s = reporting.ColorizeDiff(s)
76+
}
77+
7278
return s
7379
}
7480

0 commit comments

Comments
 (0)