diff --git a/README.md b/README.md index 1e35cad3..909e5862 100644 --- a/README.md +++ b/README.md @@ -182,3 +182,11 @@ The above will install this plugin into your `$HELM_HOME/plugins` directory. - You need to have [Go](http://golang.org) installed. Make sure to set `$GOPATH` - If you don't have [Glide](http://glide.sh) installed, this will install it into `$GOPATH/bin` for you. + +### Running Tests +Automated tests are implemented with [*testing*](https://golang.org/pkg/testing/). + +To run all tests: +``` +go test -v ./... +``` diff --git a/cmd/revision.go b/cmd/revision.go index 2e4fc597..b0e35d7d 100644 --- a/cmd/revision.go +++ b/cmd/revision.go @@ -17,6 +17,7 @@ type revision struct { client helm.Interface suppressedKinds []string revisions []string + outputContext int } const revisionCmdLongUsage = ` @@ -69,6 +70,7 @@ func revisionCmd() *cobra.Command { revisionCmd.Flags().BoolP("suppress-secrets", "q", false, "suppress secrets in the output") revisionCmd.Flags().StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output") + revisionCmd.Flags().IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes") revisionCmd.SuggestionsMinimumDistance = 1 return revisionCmd } @@ -89,7 +91,7 @@ func (d *revision) differentiate() error { return prettyError(err) } - diff.DiffManifests(manifest.Parse(revisionResponse.Release.Manifest), manifest.Parse(releaseResponse.Release.Manifest), d.suppressedKinds, os.Stdout) + diff.DiffManifests(manifest.Parse(revisionResponse.Release.Manifest), manifest.Parse(releaseResponse.Release.Manifest), d.suppressedKinds, d.outputContext, os.Stdout) case 2: revision1, _ := strconv.Atoi(d.revisions[0]) @@ -108,7 +110,7 @@ func (d *revision) differentiate() error { return prettyError(err) } - diff.DiffManifests(manifest.Parse(revisionResponse1.Release.Manifest), manifest.Parse(revisionResponse2.Release.Manifest), d.suppressedKinds, os.Stdout) + diff.DiffManifests(manifest.Parse(revisionResponse1.Release.Manifest), manifest.Parse(revisionResponse2.Release.Manifest), d.suppressedKinds, d.outputContext, os.Stdout) default: return errors.New("Invalid Arguments") diff --git a/cmd/rollback.go b/cmd/rollback.go index 97a5b2a2..0eeb2cc3 100644 --- a/cmd/rollback.go +++ b/cmd/rollback.go @@ -16,6 +16,7 @@ type rollback struct { client helm.Interface suppressedKinds []string revisions []string + outputContext int } const rollbackCmdLongUsage = ` @@ -59,6 +60,7 @@ func rollbackCmd() *cobra.Command { rollbackCmd.Flags().BoolP("suppress-secrets", "q", false, "suppress secrets in the output") rollbackCmd.Flags().StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output") + rollbackCmd.Flags().IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes") rollbackCmd.SuggestionsMinimumDistance = 1 return rollbackCmd } @@ -80,7 +82,7 @@ func (d *rollback) backcast() error { } // create a diff between the current manifest and the version of the manifest that a user is intended to rollback - diff.DiffManifests(manifest.Parse(releaseResponse.Release.Manifest), manifest.Parse(revisionResponse.Release.Manifest), d.suppressedKinds, os.Stdout) + diff.DiffManifests(manifest.Parse(releaseResponse.Release.Manifest), manifest.Parse(revisionResponse.Release.Manifest), d.suppressedKinds, d.outputContext, os.Stdout) return nil } diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 4577be84..b018cdfc 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -22,6 +22,7 @@ type diffCmd struct { resetValues bool allowUnreleased bool suppressedKinds []string + outputContext int } const globalUsage = `Show a diff explaining what a helm upgrade would change. @@ -67,6 +68,7 @@ func newChartCommand() *cobra.Command { f.BoolVar(&diff.resetValues, "reset-values", false, "reset the values to the ones built into the chart and merge in any new values") f.BoolVar(&diff.allowUnreleased, "allow-unreleased", false, "enables diffing of releases that are not yet deployed via Helm") f.StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output") + f.IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes") return cmd @@ -137,7 +139,7 @@ func (d *diffCmd) run() error { newSpecs = manifest.Parse(upgradeResponse.Release.Manifest) } - diff.DiffManifests(currentSpecs, newSpecs, d.suppressedKinds, os.Stdout) + diff.DiffManifests(currentSpecs, newSpecs, d.suppressedKinds, d.outputContext, os.Stdout) return nil } diff --git a/diff/diff.go b/diff/diff.go index 2872a446..651ad633 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "strings" + "math" "github.com/aryann/difflib" "github.com/mgutz/ansi" @@ -11,18 +12,18 @@ import ( "github.com/databus23/helm-diff/manifest" ) -func DiffManifests(oldIndex, newIndex map[string]*manifest.MappingResult, suppressedKinds []string, to io.Writer) { +func DiffManifests(oldIndex, newIndex map[string]*manifest.MappingResult, suppressedKinds []string, context int, to io.Writer) { for key, oldContent := range oldIndex { if newContent, ok := newIndex[key]; ok { if oldContent.Content != newContent.Content { // modified fmt.Fprintf(to, ansi.Color("%s has changed:", "yellow")+"\n", key) - printDiff(suppressedKinds, oldContent.Kind, oldContent.Content, newContent.Content, to) + printDiff(suppressedKinds, oldContent.Kind, context, oldContent.Content, newContent.Content, to) } } else { // removed fmt.Fprintf(to, ansi.Color("%s has been removed:", "yellow")+"\n", key) - printDiff(suppressedKinds, oldContent.Kind, oldContent.Content, "", to) + printDiff(suppressedKinds, oldContent.Kind, context, oldContent.Content, "", to) } } @@ -30,12 +31,12 @@ func DiffManifests(oldIndex, newIndex map[string]*manifest.MappingResult, suppre if _, ok := oldIndex[key]; !ok { // added fmt.Fprintf(to, ansi.Color("%s has been added:", "yellow")+"\n", key) - printDiff(suppressedKinds, newContent.Kind, "", newContent.Content, to) + printDiff(suppressedKinds, newContent.Kind, context, "", newContent.Content, to) } } } -func printDiff(suppressedKinds []string, kind, before, after string, to io.Writer) { +func printDiff(suppressedKinds []string, kind string, context int, before, after string, to io.Writer) { diffs := difflib.Diff(strings.Split(before, "\n"), strings.Split(after, "\n")) for _, ckind := range suppressedKinds { @@ -46,16 +47,71 @@ func printDiff(suppressedKinds []string, kind, before, after string, to io.Write } } - for _, diff := range diffs { - text := diff.Payload + if context >= 0 { + distances := calculateDistances(diffs) + omitting := false + for i, diff := range diffs { + if distances[i] > context { + if !omitting { + fmt.Fprintln(to, "...") + omitting = true + } + } else { + omitting = false + printDiffRecord(diff, to) + } + } + } else { + for _, diff := range diffs { + printDiffRecord(diff, to) + } + } +} + +func printDiffRecord(diff difflib.DiffRecord, to io.Writer) { + text := diff.Payload + + switch diff.Delta { + case difflib.RightOnly: + fmt.Fprintf(to, "%s\n", ansi.Color("+ "+text, "green")) + case difflib.LeftOnly: + fmt.Fprintf(to, "%s\n", ansi.Color("- "+text, "red")) + case difflib.Common: + fmt.Fprintf(to, "%s\n", " "+text) + } +} - switch diff.Delta { - case difflib.RightOnly: - fmt.Fprintf(to, "%s\n", ansi.Color("+ "+text, "green")) - case difflib.LeftOnly: - fmt.Fprintf(to, "%s\n", ansi.Color("- "+text, "red")) - case difflib.Common: - fmt.Fprintf(to, "%s\n", " "+text) +// Calculate distance of every diff-line to the closest change +func calculateDistances(diffs []difflib.DiffRecord) map[int]int { + distances := map[int]int{} + + // Iterate forwards through diffs, set 'distance' based on closest 'change' before this line + change := -1 + for i, diff := range diffs { + if diff.Delta != difflib.Common { + change = i + } + distance := math.MaxInt32 + if change != -1 { + distance = i - change + } + distances[i] = distance + } + + // Iterate backwards through diffs, reduce 'distance' based on closest 'change' after this line + change = -1 + for i := len(diffs) - 1; i >= 0; i-- { + diff := diffs[i] + if diff.Delta != difflib.Common { + change = i + } + if change != -1 { + distance := change - i + if distance < distances[i] { + distances[i] = distance + } } } + + return distances } diff --git a/diff/diff_test.go b/diff/diff_test.go new file mode 100644 index 00000000..3508678f --- /dev/null +++ b/diff/diff_test.go @@ -0,0 +1,123 @@ +package diff + +import ( + "testing" + "bytes" + "github.com/mgutz/ansi" +) + +var text1 = "" + + "line1\n" + + "line2\n" + + "line3\n" + + "line4\n" + + "line5\n" + + "line6\n" + + "line7\n" + + "line8\n" + + "line9\n" + + "line10" + +var text2 = "" + + "line1 - different!\n" + + "line2 - different!\n" + + "line3\n" + + "line4\n" + + "line5\n" + + "line6\n" + + "line7\n" + + "line8 - different!\n" + + "line9\n" + + "line10" + +func TestPrintDiffWithContext(t *testing.T) { + + t.Run("context-disabled", func(t *testing.T) { + assertDiff(t, text1, text2, -1, ""+ + "- line1\n"+ + "- line2\n"+ + "+ line1 - different!\n"+ + "+ line2 - different!\n"+ + " line3\n"+ + " line4\n"+ + " line5\n"+ + " line6\n"+ + " line7\n"+ + "- line8\n"+ + "+ line8 - different!\n"+ + " line9\n"+ + " line10\n") + }) + + t.Run("context-0", func(t *testing.T) { + assertDiff(t, text1, text2, 0, ""+ + "- line1\n"+ + "- line2\n"+ + "+ line1 - different!\n"+ + "+ line2 - different!\n"+ + "...\n"+ + "- line8\n"+ + "+ line8 - different!\n"+ + "...\n") + }) + + t.Run("context-1", func(t *testing.T) { + assertDiff(t, text1, text2, 1, ""+ + "- line1\n"+ + "- line2\n"+ + "+ line1 - different!\n"+ + "+ line2 - different!\n"+ + " line3\n"+ + "...\n"+ + " line7\n"+ + "- line8\n"+ + "+ line8 - different!\n"+ + " line9\n"+ + "...\n") + }) + + t.Run("context-2", func(t *testing.T) { + assertDiff(t, text1, text2, 2, ""+ + "- line1\n"+ + "- line2\n"+ + "+ line1 - different!\n"+ + "+ line2 - different!\n"+ + " line3\n"+ + " line4\n"+ + "...\n"+ + " line6\n"+ + " line7\n"+ + "- line8\n"+ + "+ line8 - different!\n"+ + " line9\n"+ + " line10\n") + }) + + t.Run("context-3", func(t *testing.T) { + assertDiff(t, text1, text2, 3, ""+ + "- line1\n"+ + "- line2\n"+ + "+ line1 - different!\n"+ + "+ line2 - different!\n"+ + " line3\n"+ + " line4\n"+ + " line5\n"+ + " line6\n"+ + " line7\n"+ + "- line8\n"+ + "+ line8 - different!\n"+ + " line9\n"+ + " line10\n") + }) + +} + +func assertDiff(t *testing.T, before, after string, context int, expected string) { + ansi.DisableColors(true) + var output bytes.Buffer + printDiff([]string{}, "some-resource", context, before, after, &output) + actual := output.String() + if actual != expected { + t.Errorf("Unexpected diff output: \nExpected:\n#%v# \nActual:\n#%v#", expected, actual) + } +}