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

feat: Use textconv with external diff commands #4136

Merged
merged 1 commit into from
Dec 14, 2024
Merged
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
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
}
Loading