From c93c91c707e56f6638d38c2dbffcb3db7664b497 Mon Sep 17 00:00:00 2001 From: William Wei Date: Wed, 27 Apr 2022 21:53:30 -0700 Subject: [PATCH 1/4] simuluate git diff output - accepts all arguments passed from git - generate similar diff context like built-in git diff - omit dyff header when running as external git diff tool - set GIT_CONFIG_PARAMETERS="'color.ui=always'" to enable colors --- internal/cmd/git_diff.go | 183 +++++++++++++++++++++++++++++++++++++++ internal/cmd/root.go | 9 +- 2 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 internal/cmd/git_diff.go diff --git a/internal/cmd/git_diff.go b/internal/cmd/git_diff.go new file mode 100644 index 00000000..5dac5fe4 --- /dev/null +++ b/internal/cmd/git_diff.go @@ -0,0 +1,183 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/gonvenience/wrap" + "github.com/gonvenience/ytbx" + "github.com/spf13/cobra" + + . "github.com/gonvenience/bunt" + "github.com/homeport/dyff/pkg/dyff" +) + +const ( + // Arbitrary length of the git SHA1 in sortable format + // It does not calculate git-sha1 + GIT_SHA1_LENGTH = 7 +) + +// betweenCmd represents the between command +var gitDiffCmd = &cobra.Command{ + Use: "git-diff name infile1 infile1-sha1 infile1-mode infile2 infile2-sha1 infile2-mode [ rename-to ]", + Short: "Supposed to be used as a git diff command for yaml files", + Long: `Supposed to be used as a git diff command for yaml files +Example gitconfig: + +[diff "dyff"] + command = dyff git-diff + +Example in gitattributes file: +*.yaml diff=dyff +*.yml diff=dyff + +https://git-scm.com/docs/git-difftool for more information about gif diff +`, + Args: cobra.RangeArgs(7, 9), + RunE: func(cmd *cobra.Command, args []string) error { + + fmt.Print(generateGitDiffContext(args)) + from, to, err := ytbx.LoadFiles(args[1], args[4]) + if err != nil { + return wrap.Errorf(err, "failed to load input files") + } + + report, err := dyff.CompareInputFiles(from, to, + dyff.IgnoreOrderChanges(reportOptions.ignoreOrderChanges), + ) + if err != nil { + return wrap.Errorf(err, "failed to compare input files") + } + + reportOptions.omitHeader = true + return writeReport(cmd, report) + }, +} + +func init() { + rootCmd.AddCommand(gitDiffCmd) + + //applyReportOptionsFlags(gitDiffCmd) + GIT_CONFIG_PARAMETERS := os.Getenv("GIT_CONFIG_PARAMETERS") + enableColor := strings.Contains(GIT_CONFIG_PARAMETERS, "'color.ui=always'") + enableColor = enableColor || strings.Contains(GIT_CONFIG_PARAMETERS, "'color.diff=always'") + // color.diff=auto|true|false|xxx will disable color and default to dyff auto + // by checking + if !strings.Contains(GIT_CONFIG_PARAMETERS, "'color.diff=always'") && strings.Contains(GIT_CONFIG_PARAMETERS, "'color.diff=") { + enableColor = false + } + if enableColor { + SetColorSettings(ON, ON) + } +} + +// https://github.com/git/git/blob/6cd33dceed60949e2dbc32e3f0f5e67c4c882e1e/diff.c#L6221 +// https://github.com/git/git/blob/a68dfadae5e95c7f255cf38c9efdcbc2e36d1931/diff.c#L4244-L4250 +/* An external diff command takes: + * + * diff-cmd name infile1 infile1-sha1 infile1-mode \ + * infile2 infile2-sha1 infile2-mode [ rename-to ] + * + */ +func generateGitDiffContext(args []string) string { + + // Example output format from git diff: + /* + -------------------------------------------------- + diff --git a/.github/dependabot.yml b/.github/dependabot.yml + new file mode 100644 + index 0000000..72e0f35 + --- /dev/null + +++ b/.github/dependabot.yml + + -------------------------------------------------- + diff --git a/.github/dependabot.yml b/.github/dependabot.yml + deleted file mode 100644 + index 72e0f35..0000000 + --- a/.github/dependabot.yml + +++ /dev/null + + -------------------------------------------------- + diff --git a/.github/dependabot.yml b/.github/dependabot.yml + index 72e0f35..8f2b7dc 100644 + --- a/.github/dependabot.yml + +++ b/.github/dependabot.yml + + -------------------------------------------------- + diff --git a/.github/dependabot.yml b/.github/dependabot.yml + old mode 100644 + new mode 100755 + index 72e0f35..8f2b7dc + --- a/.github/dependabot.yml + +++ b/.github/dependabot.yml + */ + + var sbGitHashCtx strings.Builder + var gitname, infile1, infile1sha1, infile1mode, infile2, infile2sha2, infile2mode string = args[0], args[1], args[2], args[3], args[4], args[5], args[6] + sbGitHashCtx.WriteString(fmt.Sprintf("dyff --git a/%s b/%s\n", gitname, gitname)) + if infile1 == os.DevNull { + sbGitHashCtx.WriteString(fmt.Sprintf("new file mode %s\n", infile2mode)) + // caculate git object hash of the file + infile1sha1 = "0000000" + } else if infile2 == os.DevNull { + sbGitHashCtx.WriteString(fmt.Sprintf("deleted file mode %s\n", infile1mode)) + infile2sha2 = "0000000" + } else if infile1mode != infile2mode { + sbGitHashCtx.WriteString(fmt.Sprintf("old mode %s\n", infile1mode)) + sbGitHashCtx.WriteString(fmt.Sprintf("new mode %s\n", infile2mode)) + } + + // file renamed ? Add rename info passed from git diff + if len(args) >= 9 { + sbGitHashCtx.WriteString(args[8]) + // renamed and no changes + if strings.Contains(args[8], "similarity index 100") { + return sbGitHashCtx.String() + } + } else { + sbGitHashCtx.WriteString(fmt.Sprintf("index %s..%s\n", formatGitSHA1(infile1sha1), formatGitSHA1(infile2sha2))) + } + + if infile1 == os.DevNull { + sbGitHashCtx.WriteString("--- /dev/null\n") + } else { + sbGitHashCtx.WriteString(fmt.Sprintf("--- a/%s\n", gitname)) + } + if infile2 == os.DevNull { + sbGitHashCtx.WriteString("+++ /dev/null\n") + } else if len(args) >= 9 { // file renamed + sbGitHashCtx.WriteString(fmt.Sprintf("+++ b/%s\n", args[7])) + } else { + sbGitHashCtx.WriteString(fmt.Sprintf("+++ b/%s\n", gitname)) + } + return sbGitHashCtx.String() +} + +func formatGitSHA1(gitSHA1 string) string { + if len(gitSHA1) > GIT_SHA1_LENGTH { + return gitSHA1[:GIT_SHA1_LENGTH] + } + return "0000000" +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 9db949f9..35c4f34f 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -81,7 +81,14 @@ func Execute() error { // In case `KUBECTL_EXTERNAL_DIFF` is set with `dyff`, it is very likely // that `kubectl` intends to use `dyff` for its `diff` command. Therefore, // enable Kubernetes specific entity detection and fix the order issue. - if strings.Contains(os.Getenv("KUBECTL_EXTERNAL_DIFF"), name) { + + // Reordering positional arguments can cause problems when dyff is used as an + // external git diff command. + // If `GIT_DIFF_PATH_TOTAL` is set, likely dyff is invoked by `git diff` + // + + if strings.Contains(os.Getenv("KUBECTL_EXTERNAL_DIFF"), name) && + os.Getenv("GIT_DIFF_PATH_TOTAL") == "" { // Rearrange the arguments to match `dyff between --flags from to` to // mitigate an issue in `kubectl`, which puts the `from` and `to` at // the second and third position in the command arguments. From 47c2dc03345a724aa0f29f51eab925250ced8e37 Mon Sep 17 00:00:00 2001 From: William Wei Date: Thu, 28 Apr 2022 17:28:15 -0700 Subject: [PATCH 2/4] explicit argument for kubectl diff checking with KUBECTL_EXTERNAL_DIFF env variable this will be running in all cases even not running kubectl diff command --- internal/cmd/between.go | 2 ++ internal/cmd/git_diff.go | 1 - internal/cmd/root.go | 28 ++++++++++++++++------------ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/internal/cmd/between.go b/internal/cmd/between.go index 5c03edd8..202dbe73 100644 --- a/internal/cmd/between.go +++ b/internal/cmd/between.go @@ -30,6 +30,7 @@ import ( type betweenCmdOptions struct { swap bool + kubectlExternalDiff bool translateListToDocuments bool chroot string chrootFrom string @@ -118,6 +119,7 @@ func init() { // Input documents modification flags betweenCmd.Flags().BoolVar(&betweenCmdSettings.swap, "swap", false, "Swap 'from' and 'to' for comparison") + betweenCmd.Flags().BoolVar(&betweenCmdSettings.kubectlExternalDiff, "kubectl-external-diff", false, "Invoked by kubectl external diff, supposed to be configured in KUBECTL_EXTERNAL_DIFF environment variable") betweenCmd.Flags().StringVar(&betweenCmdSettings.chroot, "chroot", "", "change the root level of the input file to another point in the document") betweenCmd.Flags().StringVar(&betweenCmdSettings.chrootFrom, "chroot-of-from", "", "only change the root level of the from input file") betweenCmd.Flags().StringVar(&betweenCmdSettings.chrootTo, "chroot-of-to", "", "only change the root level of the to input file") diff --git a/internal/cmd/git_diff.go b/internal/cmd/git_diff.go index 5dac5fe4..0fdff695 100644 --- a/internal/cmd/git_diff.go +++ b/internal/cmd/git_diff.go @@ -79,7 +79,6 @@ https://git-scm.com/docs/git-difftool for more information about gif diff func init() { rootCmd.AddCommand(gitDiffCmd) - //applyReportOptionsFlags(gitDiffCmd) GIT_CONFIG_PARAMETERS := os.Getenv("GIT_CONFIG_PARAMETERS") enableColor := strings.Contains(GIT_CONFIG_PARAMETERS, "'color.ui=always'") enableColor = enableColor || strings.Contains(GIT_CONFIG_PARAMETERS, "'color.diff=always'") diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 35c4f34f..2ab14834 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -23,7 +23,6 @@ package cmd import ( "os" "path/filepath" - "strings" "github.com/gonvenience/bunt" "github.com/gonvenience/term" @@ -78,17 +77,15 @@ func ResetSettings() { // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() error { - // In case `KUBECTL_EXTERNAL_DIFF` is set with `dyff`, it is very likely - // that `kubectl` intends to use `dyff` for its `diff` command. Therefore, - // enable Kubernetes specific entity detection and fix the order issue. + // Run with `--kubectl-external-diff` explicitly to trigger argument reordering. + // Example when running: + // KUBECTL_EXTERNAL_DIFF="dyff between --kubectl-external-diff" kubectl diff -f xxx/yyy/zzz.yaml + // kubectl will invoke dyff with the arguments: (injecting two directores as arg[1] and arg[2]) + // dyff tmp/fileA tmp/fileB between --kubectl-external-diff - // Reordering positional arguments can cause problems when dyff is used as an - // external git diff command. - // If `GIT_DIFF_PATH_TOTAL` is set, likely dyff is invoked by `git diff` - // + if contains(os.Args, "--kubectl-external-diff") { + ytbx.PreserveKeyOrderInJSON = true - if strings.Contains(os.Getenv("KUBECTL_EXTERNAL_DIFF"), name) && - os.Getenv("GIT_DIFF_PATH_TOTAL") == "" { // Rearrange the arguments to match `dyff between --flags from to` to // mitigate an issue in `kubectl`, which puts the `from` and `to` at // the second and third position in the command arguments. @@ -104,8 +101,6 @@ func Execute() error { os.Args = append(args, paths...) - // Enable Kubernetes specific entity detection implicitly - reportOptions.kubernetesEntityDetection = true } if err := rootCmd.Execute(); err != nil { @@ -135,3 +130,12 @@ func init() { rootCmd.PersistentFlags().IntVarP(&term.FixedTerminalWidth, "fixed-width", "w", -1, "disable terminal width detection and use provided fixed value") rootCmd.PersistentFlags().BoolVarP(&ytbx.PreserveKeyOrderInJSON, "preserve-key-order-in-json", "k", false, "use ordered keys during JSON decoding (non standard behavior)") } + +func contains(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + return false +} From 9b3878d590cef2a5abc417c4c91b6e40d0ff7ece Mon Sep 17 00:00:00 2001 From: William Wei Date: Thu, 28 Apr 2022 17:44:13 -0700 Subject: [PATCH 3/4] Update docs for git diff usages --- README.md | 8 +++++--- internal/cmd/git_diff.go | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e0ad8678..c49cbf21 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ _Please note:_ This will install `dyff` based on the latest available code base. ```bash # Setup - export KUBECTL_EXTERNAL_DIFF="dyff between --omit-header --set-exit-code" + export KUBECTL_EXTERNAL_DIFF="dyff between --omit-header --set-exit-code --kubectl-external-diff" # Usage kubectl diff [...] @@ -96,12 +96,14 @@ _Please note:_ This will install `dyff` based on the latest available code base. ```bash # Setup... - git config --local diff.dyff.command 'dyff_between() { dyff --color on between --omit-header "$2" "$5"; }; dyff_between' - echo '*.yml diff=dyff' >> .gitattributes + git config --local diff.dyff.command 'dyff git-diff' + echo '*.yml diff=dyff' >> .gitattributes + echo '*.yaml diff=dyff' >> .gitattributes # And have fun, e.g.: git log --ext-diff -u git show --ext-diff HEAD + GIT_CONFIG_PARAMETERS="'color.ui=always'" git log --ext-diff -u '**/*.yaml' '**/*.yml' ``` ![dyff between example of a Git commit](.docs/dyff-between-git-commits-example.png?raw=true "dyff in Git example of an example commit") diff --git a/internal/cmd/git_diff.go b/internal/cmd/git_diff.go index 0fdff695..d811000e 100644 --- a/internal/cmd/git_diff.go +++ b/internal/cmd/git_diff.go @@ -34,7 +34,7 @@ import ( ) const ( - // Arbitrary length of the git SHA1 in sortable format + // Arbitrary length of the git SHA1 in short format // It does not calculate git-sha1 GIT_SHA1_LENGTH = 7 ) From 09093238f3659cd6ee53cb972c0de01a2b08173d Mon Sep 17 00:00:00 2001 From: William Wei Date: Tue, 3 May 2022 14:02:19 -0700 Subject: [PATCH 4/4] kubectl external diff argument re-order move arg[1], arg[2] to the end of argument list --- go.mod | 1 + internal/cmd/root.go | 32 ++++++++++++++++++-------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index dba58a4d..2b7182a7 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/mitchellh/hashstructure v1.1.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.19.0 + github.com/pkg/errors v0.9.1 github.com/sergi/go-diff v1.2.0 github.com/spf13/cobra v1.4.0 github.com/texttheater/golang-levenshtein v1.0.1 diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 2ab14834..8be819d1 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -27,6 +27,7 @@ import ( "github.com/gonvenience/bunt" "github.com/gonvenience/term" "github.com/gonvenience/ytbx" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -83,24 +84,27 @@ func Execute() error { // kubectl will invoke dyff with the arguments: (injecting two directores as arg[1] and arg[2]) // dyff tmp/fileA tmp/fileB between --kubectl-external-diff - if contains(os.Args, "--kubectl-external-diff") { + if indexSlice(os.Args, "--kubectl-external-diff") > 1 && indexSlice(os.Args, "between") > 1 { ytbx.PreserveKeyOrderInJSON = true - // Rearrange the arguments to match `dyff between --flags from to` to // mitigate an issue in `kubectl`, which puts the `from` and `to` at // the second and third position in the command arguments. - var paths, args []string - for _, entry := range os.Args { - if info, err := os.Stat(entry); err == nil && info.IsDir() { - paths = append(paths, entry) + // only check arg[1] and arg[2], in case `dyff` or `between` are directories + for i := 1; i <= 2; i++ { + if info, err := os.Stat(os.Args[i]); err == nil && info.IsDir() { } else { - args = append(args, entry) + return ExitCode{ + Value: 255, + Cause: errors.Errorf("%s is not a directory? is this invoked by kubectl diff?", os.Args[i]), + } } } - - os.Args = append(args, paths...) - + var args []string + args = append(args, os.Args[0]) + args = append(args, os.Args[3:]...) + args = append(args, os.Args[1], os.Args[2]) + os.Args = args } if err := rootCmd.Execute(); err != nil { @@ -131,11 +135,11 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&ytbx.PreserveKeyOrderInJSON, "preserve-key-order-in-json", "k", false, "use ordered keys during JSON decoding (non standard behavior)") } -func contains(s []string, str string) bool { - for _, v := range s { +func indexSlice(s []string, str string) int { + for i, v := range s { if v == str { - return true + return i } } - return false + return -1 }