Skip to content

Commit

Permalink
Add initial suport for external diff tools
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Aug 5, 2021
1 parent 0eb5348 commit 002742c
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 11 deletions.
16 changes: 15 additions & 1 deletion docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ The following configuration variables are available:
| `bitwarden` | `command` | string | `bw` | Bitwarden CLI command |
| `cd` | `args` | []string | *none* | Extra args to shell in `cd` command |
| | `command` | string | *none* | Shell to run in `cd` command |
| `diff` | `exclude` | []string | *none* | Entry types to exclude from diff |
| `diff` | `args` | []string | *see `diff` below* | Extra args to external diff command |
| | `command` | string | *none* | External diff command |
| | `exclude` | []string | *none* | Entry types to exclude from diff |
| | `pager` | string | *none* | Diff-specific pager |
| `docs` | `maxWidth` | int | 80 | Maximum width of output |
| | `pager` | string | *none* | Docs-specific pager |
Expand Down Expand Up @@ -877,10 +879,22 @@ Print the difference between the target state and the destination state for
If a `diff.pager` command is set in the configuration file then the output will
be piped into it.

If `diff.command` is set then it will be invoked to show individual file
differences with `diff.args` passed as arguments. Each element of `diff.args` is
interpreted as a template with the variables `.Destination` and `.Target`
available corresponding to the path of the file in the source and target state
respectively. The default value of `diff.args` is `["{{ .Destination }}",
"{{ .Target }}"]`.

#### `--pager` *pager*

Pager to use for output.

#### `--use-builtin-diff`

Use chezmoi's builtin diff, even if the `diff.command` configuration variable is
set.

#### `diff` examples

```console
Expand Down
197 changes: 197 additions & 0 deletions internal/chezmoi/externaldiffsystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package chezmoi

import (
"errors"
"io/fs"
"os"
"os/exec"
"strconv"
"strings"
"text/template"

vfs "github.com/twpayne/go-vfs/v3"
)

// An ExternalDiffSystem is a DiffSystem that uses an external diff tool.
type ExternalDiffSystem struct {
system System
command string
args []string
destDirAbsPath AbsPath
tempDirAbsPath AbsPath
}

// NewExternalDiffSystem creates a new ExternalDiffSystem.
func NewExternalDiffSystem(system System, command string, args []string, destDirAbsPath AbsPath) *ExternalDiffSystem {
return &ExternalDiffSystem{
system: system,
command: command,
args: args,
destDirAbsPath: destDirAbsPath,
}
}

// Close frees all resources held by s.
func (s *ExternalDiffSystem) Close() error {
if s.tempDirAbsPath != "" {
if err := os.RemoveAll(string(s.tempDirAbsPath)); err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
s.tempDirAbsPath = ""
}
return nil
}

// Chmod implements System.Chmod.
func (s *ExternalDiffSystem) Chmod(name AbsPath, mode fs.FileMode) error {
return s.system.Chmod(name, mode)
}

// Glob implements System.Glob.
func (s *ExternalDiffSystem) Glob(pattern string) ([]string, error) {
return s.system.Glob(pattern)
}

// IdempotentCmdCombinedOutput implements System.IdempotentCmdCombinedOutput.
func (s *ExternalDiffSystem) IdempotentCmdCombinedOutput(cmd *exec.Cmd) ([]byte, error) {
return s.system.IdempotentCmdCombinedOutput(cmd)
}

// IdempotentCmdOutput implements System.IdempotentCmdOutput.
func (s *ExternalDiffSystem) IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) {
return s.system.IdempotentCmdOutput(cmd)
}

// Lstat implements System.Lstat.
func (s *ExternalDiffSystem) Lstat(name AbsPath) (fs.FileInfo, error) {
return s.system.Lstat(name)
}

// Mkdir implements System.Mkdir.
func (s *ExternalDiffSystem) Mkdir(name AbsPath, perm fs.FileMode) error {
return s.system.Mkdir(name, perm)
}

// RawPath implements System.RawPath.
func (s *ExternalDiffSystem) RawPath(path AbsPath) (AbsPath, error) {
return s.system.RawPath(path)
}

// ReadDir implements System.ReadDir.
func (s *ExternalDiffSystem) ReadDir(name AbsPath) ([]fs.DirEntry, error) {
return s.system.ReadDir(name)
}

// ReadFile implements System.ReadFile.
func (s *ExternalDiffSystem) ReadFile(name AbsPath) ([]byte, error) {
return s.system.ReadFile(name)
}

// Readlink implements System.Readlink.
func (s *ExternalDiffSystem) Readlink(name AbsPath) (string, error) {
return s.system.Readlink(name)
}

// RemoveAll implements System.RemoveAll.
func (s *ExternalDiffSystem) RemoveAll(name AbsPath) error {
// FIXME generate suitable inputs for s.command
return s.system.RemoveAll(name)
}

// Rename implements System.Rename.
func (s *ExternalDiffSystem) Rename(oldpath, newpath AbsPath) error {
// FIXME generate suitable inputs for s.command
return s.system.Rename(oldpath, newpath)
}

// RunCmd implements System.RunCmd.
func (s *ExternalDiffSystem) RunCmd(cmd *exec.Cmd) error {
return s.system.RunCmd(cmd)
}

// RunIdempotentCmd implements System.RunIdempotentCmd.
func (s *ExternalDiffSystem) RunIdempotentCmd(cmd *exec.Cmd) error {
return s.system.RunIdempotentCmd(cmd)
}

// RunScript implements System.RunScript.
func (s *ExternalDiffSystem) RunScript(scriptname RelPath, dir AbsPath, data []byte, interpreter *Interpreter) error {
// FIXME generate suitable inputs for s.command
return s.system.RunScript(scriptname, dir, data, interpreter)
}

// Stat implements System.Stat.
func (s *ExternalDiffSystem) Stat(name AbsPath) (fs.FileInfo, error) {
return s.system.Stat(name)
}

// UnderlyingFS implements System.UnderlyingFS.
func (s *ExternalDiffSystem) UnderlyingFS() vfs.FS {
return s.system.UnderlyingFS()
}

// WriteFile implements System.WriteFile.
func (s *ExternalDiffSystem) WriteFile(filename AbsPath, data []byte, perm fs.FileMode) error {
targetRelPath, err := filename.TrimDirPrefix(s.destDirAbsPath)
if err != nil {
return err
}
tempDirAbsPath, err := s.tempDir()
if err != nil {
return err
}
targetAbsPath := tempDirAbsPath.Join(targetRelPath)
if err := os.WriteFile(string(targetAbsPath), data, perm); err != nil {
return err
}
return s.runDiffCommand(filename, targetAbsPath)
}

// WriteSymlink implements System.WriteSymlink.
func (s *ExternalDiffSystem) WriteSymlink(oldname string, newname AbsPath) error {
// FIXME generate suitable inputs for s.command
return s.system.WriteSymlink(oldname, newname)
}

// tempDir creates a temporary directory for s if it does not already exist and
// returns its path.
func (s *ExternalDiffSystem) tempDir() (AbsPath, error) {
if s.tempDirAbsPath == "" {
tempDir, err := os.MkdirTemp("", "chezmoi-diff")
if err != nil {
return "", err
}
s.tempDirAbsPath = AbsPath(tempDir)
}
return s.tempDirAbsPath, nil
}

// runDiffCommand runs the external diff command.
func (s *ExternalDiffSystem) runDiffCommand(destAbsPath, targetAbsPath AbsPath) error {
templateData := struct {
Destination string
Target string
}{
Destination: string(destAbsPath),
Target: string(targetAbsPath),
}
args := make([]string, 0, len(s.args))
for i, arg := range s.args {
tmpl, err := template.New("diff.args[" + strconv.Itoa(i) + "]").Parse(arg)
if err != nil {
return err
}
var sb strings.Builder
if err := tmpl.Execute(&sb, templateData); err != nil {
return err
}
args = append(args, sb.String())
}

//nolint:gosec
cmd := exec.Command(s.command, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return s.system.RunIdempotentCmd(cmd)
}
4 changes: 4 additions & 0 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ func newConfig(options ...configOption) (*Config, error) {
recursive: true,
},
Diff: diffCmdConfig{
Args: []string{
"{{ .Destination }}",
"{{ .Target }}",
},
Exclude: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesNone),
include: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesAll),
},
Expand Down
33 changes: 23 additions & 10 deletions internal/cmd/diffcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import (
)

type diffCmdConfig struct {
Exclude *chezmoi.EntryTypeSet `mapstructure:"exclude"`
Pager string `mapstructure:"pager"`
include *chezmoi.EntryTypeSet
recursive bool
Command string `mapstructure:"command"`
Args []string `mapstructure:"args"`
Exclude *chezmoi.EntryTypeSet `mapstructure:"exclude"`
Pager string `mapstructure:"pager"`
include *chezmoi.EntryTypeSet
recursive bool
useBuiltinDiff bool
}

func (c *Config) newDiffCmd() *cobra.Command {
Expand All @@ -32,6 +35,7 @@ func (c *Config) newDiffCmd() *cobra.Command {
flags.VarP(c.Diff.include, "include", "i", "Include entry types")
flags.BoolVarP(&c.Diff.recursive, "recursive", "r", c.Diff.recursive, "Recurse into subdirectories")
flags.StringVar(&c.Diff.Pager, "pager", c.Diff.Pager, "Set pager")
flags.BoolVarP(&c.Diff.useBuiltinDiff, "use-builtin-diff", "", c.Diff.useBuiltinDiff, "Use the builtin diff")

return diffCmd
}
Expand All @@ -43,13 +47,22 @@ func (c *Config) runDiffCmd(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
gitDiffSystem := chezmoi.NewGitDiffSystem(dryRunSystem, &sb, c.DestDirAbsPath, color)
if err := c.applyArgs(gitDiffSystem, c.DestDirAbsPath, args, applyArgsOptions{
if c.Diff.useBuiltinDiff || c.Diff.Command == "" {
gitDiffSystem := chezmoi.NewGitDiffSystem(dryRunSystem, &sb, c.DestDirAbsPath, color)
if err := c.applyArgs(gitDiffSystem, c.DestDirAbsPath, args, applyArgsOptions{
include: c.Diff.include.Sub(c.Diff.Exclude),
recursive: c.Diff.recursive,
umask: c.Umask,
}); err != nil {
return err
}
return c.pageOutputString(sb.String(), c.Diff.Pager)
}
diffSystem := chezmoi.NewExternalDiffSystem(dryRunSystem, c.Diff.Command, c.Diff.Args, c.DestDirAbsPath)
defer diffSystem.Close()
return c.applyArgs(diffSystem, c.DestDirAbsPath, args, applyArgsOptions{
include: c.Diff.include.Sub(c.Diff.Exclude),
recursive: c.Diff.recursive,
umask: c.Umask,
}); err != nil {
return err
}
return c.pageOutputString(sb.String(), c.Diff.Pager)
})
}
31 changes: 31 additions & 0 deletions internal/cmd/testdata/scripts/diff_unix.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[windows] skip 'UNIX only'

chmod 755 bin/vimdiff

# test that chezmoi diff invokes diff.command when configured
chezmoi diff
stdout 'vimdiff .*/home/user/\.file .*/chezmoi-diff.*/\.file' # FIXME should be able to use ${HOME@R} in this regexp

# test that chezmoi diff --use-builtin-diff uses the builtin diff even if diff.command is configured
chezmoi diff --use-builtin-diff
cmp stdout golden/diff

-- bin/vimdiff --
#!/bin/sh

echo vimdiff "$*"
-- golden/diff --
diff --git a/.file b/.file
index bd729e8ee3cc005444c67dc77eed60016886b5e0..b508963510528ab709627ec448026a10a64c72ef 100644
--- a/.file
+++ b/.file
@@ -1 +1 @@
-# destination contents of .file
+# target contents of .file
-- home/user/.config/chezmoi/chezmoi.toml --
[diff]
command = "vimdiff"
-- home/user/.file --
# destination contents of .file
-- home/user/.local/share/chezmoi/dot_file --
# target contents of .file

0 comments on commit 002742c

Please sign in to comment.