Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simuluate git diff output #221

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [...]
Expand All @@ -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")
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/between.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

type betweenCmdOptions struct {
swap bool
kubectlExternalDiff bool
translateListToDocuments bool
chroot string
chrootFrom string
Expand Down Expand Up @@ -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")
Expand Down
182 changes: 182 additions & 0 deletions internal/cmd/git_diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// 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 short 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)

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 <stdout-is-tty>
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"
}
45 changes: 30 additions & 15 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ package cmd
import (
"os"
"path/filepath"
"strings"

"github.com/gonvenience/bunt"
"github.com/gonvenience/term"
"github.com/gonvenience/ytbx"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -78,27 +78,33 @@ 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.
if strings.Contains(os.Getenv("KUBECTL_EXTERNAL_DIFF"), name) {
// 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

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...)

// Enable Kubernetes specific entity detection implicitly
reportOptions.kubernetesEntityDetection = true
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 {
Expand Down Expand Up @@ -128,3 +134,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 indexSlice(s []string, str string) int {
for i, v := range s {
if v == str {
return i
}
}
return -1
}