Skip to content

Commit

Permalink
feat: Use textconv with external diff commands
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Dec 14, 2024
1 parent 4cb1123 commit 3558b71
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 25 deletions.
5 changes: 5 additions & 0 deletions internal/chezmoi/chezmoi.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ var FileModeTypeNames = map[fs.FileMode]string{
fs.ModeCharDevice: "char device",
}

// A TextConvFunc converts the contents of a file into a more human-readable
// form. It returns the converted data, whether any conversion occurred, and any
// error.
type TextConvFunc func(string, []byte) ([]byte, bool, error)

// FQDNHostname returns the FQDN hostname.
func FQDNHostname(fileSystem vfs.FS) (string, error) {
// First, try os.Hostname. If it returns something that looks like a FQDN
Expand Down
65 changes: 51 additions & 14 deletions internal/chezmoi/externaldiffsystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ type ExternalDiffSystem struct {
filter *EntryTypeFilter
reverse bool
scriptContents bool
textConvFunc TextConvFunc
}

// ExternalDiffSystemOptions are options for NewExternalDiffSystem.
type ExternalDiffSystemOptions struct {
Filter *EntryTypeFilter
Reverse bool
ScriptContents bool
TextConvFunc TextConvFunc
}

// NewExternalDiffSystem creates a new ExternalDiffSystem.
Expand All @@ -52,6 +54,7 @@ func NewExternalDiffSystem(
filter: options.Filter,
reverse: options.Reverse,
scriptContents: options.ScriptContents,
textConvFunc: options.TextConvFunc,
}
}

Expand Down Expand Up @@ -222,34 +225,68 @@ func (s *ExternalDiffSystem) UnderlyingFS() vfs.FS {
// WriteFile implements System.WriteFile.
func (s *ExternalDiffSystem) WriteFile(filename AbsPath, data []byte, perm fs.FileMode) error {
if s.filter.IncludeEntryTypeBits(EntryTypeFiles) {
targetRelPath, err := filename.TrimDirPrefix(s.destDirAbsPath)
if err != nil {
return err
}
tempDirAbsPath, err := s.tempDir()
if err != nil {
return err
}

// If filename does not exist, replace it with /dev/null to avoid
// passing the name of a non-existent file to the external diff command.
destAbsPath := filename
switch _, err := os.Stat(destAbsPath.String()); {
// Otherwise, if the file exists and a textconv filter is configured,
// run the filter and update fromAbsPath to point to the converted data.
fromAbsPath := filename
switch fileInfo, err := os.Lstat(fromAbsPath.String()); {
case errors.Is(err, fs.ErrNotExist):
destAbsPath = devNullAbsPath
fromAbsPath = devNullAbsPath
case err != nil:
return err
case s.textConvFunc != nil:
// Maybe convert the from data with textconv.
fromData, err := os.ReadFile(fromAbsPath.String())
if err != nil {
return err
}
switch convertedFromData, converted, err := s.textConvFunc(fromAbsPath.String(), fromData); {
case err != nil:
return err
case converted:
tempFromAbsPath := tempDirAbsPath.Join(NewRelPath("a"), targetRelPath)
if err := os.MkdirAll(tempFromAbsPath.Dir().String(), 0o700); err != nil {
return err
}
if err := os.WriteFile(tempFromAbsPath.String(), convertedFromData, fileInfo.Mode().Perm()); err != nil {
return err
}
fromAbsPath = tempFromAbsPath
}
}

// Write the target contents to a file in a temporary directory.
targetRelPath, err := filename.TrimDirPrefix(s.destDirAbsPath)
if err != nil {
return err
}
tempDirAbsPath, err := s.tempDir()
if err != nil {
return err
toAbsPath := tempDirAbsPath.Join(targetRelPath)
toData := data
if s.textConvFunc != nil {
// Maybe convert the to data with textconv.
switch convertedToData, converted, err := s.textConvFunc(filename.String(), toData); {
case err != nil:
return err
case converted:
toAbsPath = tempDirAbsPath.Join(NewRelPath("b"), targetRelPath)
toData = convertedToData
}
}
targetAbsPath := tempDirAbsPath.Join(targetRelPath)
if err := os.MkdirAll(targetAbsPath.Dir().String(), 0o700); err != nil {
if err := os.MkdirAll(toAbsPath.Dir().String(), 0o700); err != nil {
return err
}
if err := os.WriteFile(targetAbsPath.String(), data, perm); err != nil {
if err := os.WriteFile(toAbsPath.String(), toData, perm); err != nil {
return err
}

if err := s.runDiffCommand(destAbsPath, targetAbsPath); err != nil {
// Run the external diff command.
if err := s.runDiffCommand(fromAbsPath, toAbsPath); err != nil {
return err
}
}
Expand Down
7 changes: 2 additions & 5 deletions internal/chezmoi/gitdiffsystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import (
vfs "github.com/twpayne/go-vfs/v5"
)

// A TextConvFunc converts the contents of a file into a more human-readable form.
type TextConvFunc func(string, []byte) ([]byte, error)

// A GitDiffSystem wraps a System and logs all of the actions executed as a git
// diff.
type GitDiffSystem struct {
Expand Down Expand Up @@ -278,7 +275,7 @@ func (s *GitDiffSystem) encodeDiff(absPath AbsPath, toData []byte, toMode fs.Fil
return err
}
if s.textConvFunc != nil {
fromData, err = s.textConvFunc(absPath.String(), fromData)
fromData, _, err = s.textConvFunc(absPath.String(), fromData)
if err != nil {
return err
}
Expand All @@ -297,7 +294,7 @@ func (s *GitDiffSystem) encodeDiff(absPath AbsPath, toData []byte, toMode fs.Fil

if s.textConvFunc != nil {
var err error
toData, err = s.textConvFunc(absPath.String(), toData)
toData, _, err = s.textConvFunc(absPath.String(), toData)
if err != nil {
return err
}
Expand Down
5 changes: 3 additions & 2 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1228,14 +1228,14 @@ func (c *Config) diffFile(
}
if fromMode.IsRegular() {
var err error
fromData, err = c.TextConv.convert(path.String(), fromData)
fromData, _, err = c.TextConv.convert(path.String(), fromData)
if err != nil {
return err
}
}
if toMode.IsRegular() {
var err error
toData, err = c.TextConv.convert(path.String(), toData)
toData, _, err = c.TextConv.convert(path.String(), toData)
if err != nil {
return err
}
Expand Down Expand Up @@ -1817,6 +1817,7 @@ func (c *Config) newDiffSystem(s chezmoi.System, w io.Writer, dirAbsPath chezmoi
Filter: chezmoi.NewEntryTypeFilter(c.Diff.include.Bits(), c.Diff.Exclude.Bits()),
Reverse: c.Diff.Reverse,
ScriptContents: c.Diff.ScriptContents,
TextConvFunc: c.TextConv.convert,
}
return chezmoi.NewExternalDiffSystem(s, c.Diff.Command, c.Diff.Args, c.DestDirAbsPath, options)
}
Expand Down
35 changes: 35 additions & 0 deletions internal/cmd/testdata/scripts/externaldiff.txtar
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[windows] skip 'UNIX only'

chmod 755 bin/external-diff

# test that chezmoi diff invokes the external diff command for scripts
exec chezmoi diff
stdout '# contents of script'
Expand All @@ -20,6 +22,26 @@ chhome home3/user
exec chezmoi diff
stdout ^/dev/null\s${WORK@R}/.*/\.dir$

chhome home4/user

# test that chezmoi diff uses textconv when an external diff tool is used
exec chezmoi diff
cmp stdout golden/external-diff

-- bin/external-diff --
#!/bin/sh

echo old/$(basename $1):
cat $1
echo
echo new/$(basename $2):
cat $2
-- golden/external-diff --
old/file.txt:
# OLD CONTENTS OF .DIR/FILE.TXT

new/file.txt:
# NEW CONTENTS OF .DIR/FILE.TXT
-- home/user/.config/chezmoi/chezmoi.toml --
[diff]
command = "cat"
Expand All @@ -34,3 +56,16 @@ diff:
[diff]
command = "echo"
-- home3/user/.local/share/chezmoi/dot_dir/.keep --
-- home4/user/.config/chezmoi/chezmoi.yaml --
diff:
command: external-diff
textconv:
- pattern: '**/*.txt'
command: tr
args:
- a-z
- A-Z
-- home4/user/.dir/file.txt --
# old contents of .dir/file.txt
-- home4/user/.local/share/chezmoi/dot_dir/file.txt --
# new contents of .dir/file.txt
12 changes: 8 additions & 4 deletions internal/cmd/textconv.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ type textConvElement struct {

type textConv []*textConvElement

func (t textConv) convert(path string, data []byte) ([]byte, error) {
func (t textConv) convert(path string, data []byte) ([]byte, bool, error) {
var longestPatternElement *textConvElement
for _, command := range t {
ok, err := doublestar.Match(command.Pattern, path)
if err != nil {
return nil, err
return nil, false, err
}
if !ok {
continue
Expand All @@ -34,11 +34,15 @@ func (t textConv) convert(path string, data []byte) ([]byte, error) {
}
}
if longestPatternElement == nil {
return data, nil
return data, false, nil
}

cmd := exec.Command(longestPatternElement.Command, longestPatternElement.Args...)
cmd.Stdin = bytes.NewReader(data)
cmd.Stderr = os.Stderr
return chezmoilog.LogCmdOutput(slog.Default(), cmd)
convertedData, err := chezmoilog.LogCmdOutput(slog.Default(), cmd)
if err != nil {
return nil, false, err
}
return convertedData, true, nil
}

0 comments on commit 3558b71

Please sign in to comment.