Upstream twpayne/chezmoi state of the touched files before c6778d6771
This commit is contained in:
commit
ee28f348f2
5 files changed
+4195
No files matched your search
@@ -0,0 +1,462 @@
|
|||||||
|
package chezmoi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
vfs "github.com/twpayne/go-vfs/v5"
|
||||||
|
|
||||||
|
"chezmoi.io/chezmoi/v2/internal/chezmoilog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An ExternalDiffSystem is a DiffSystem that uses an external diff tool.
|
||||||
|
type ExternalDiffSystem struct {
|
||||||
|
system System
|
||||||
|
command string
|
||||||
|
args []string
|
||||||
|
destDirAbsPath AbsPath
|
||||||
|
tempDirAbsPath AbsPath
|
||||||
|
filter *EntryTypeFilter
|
||||||
|
pagerCmdFunc func() (*exec.Cmd, error)
|
||||||
|
reverse bool
|
||||||
|
scriptContents bool
|
||||||
|
templateOptions TemplateOptions
|
||||||
|
textConvFunc TextConvFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalDiffSystemOptions are options for NewExternalDiffSystem.
|
||||||
|
type ExternalDiffSystemOptions struct {
|
||||||
|
Filter *EntryTypeFilter
|
||||||
|
Reverse bool
|
||||||
|
ScriptContents bool
|
||||||
|
TextConvFunc TextConvFunc
|
||||||
|
TemplateOptions TemplateOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExternalDiffSystem creates a new ExternalDiffSystem.
|
||||||
|
func NewExternalDiffSystem(
|
||||||
|
system System,
|
||||||
|
command string,
|
||||||
|
args []string,
|
||||||
|
destDirAbsPath AbsPath,
|
||||||
|
pagerCmdFunc func() (*exec.Cmd, error),
|
||||||
|
options *ExternalDiffSystemOptions,
|
||||||
|
) *ExternalDiffSystem {
|
||||||
|
return &ExternalDiffSystem{
|
||||||
|
system: system,
|
||||||
|
command: command,
|
||||||
|
args: args,
|
||||||
|
destDirAbsPath: destDirAbsPath,
|
||||||
|
filter: options.Filter,
|
||||||
|
pagerCmdFunc: pagerCmdFunc,
|
||||||
|
reverse: options.Reverse,
|
||||||
|
scriptContents: options.ScriptContents,
|
||||||
|
templateOptions: options.TemplateOptions,
|
||||||
|
textConvFunc: options.TextConvFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chmod implements System.Chmod.
|
||||||
|
func (s *ExternalDiffSystem) Chmod(name AbsPath, mode fs.FileMode) error {
|
||||||
|
// FIXME generate suitable inputs for s.command
|
||||||
|
return s.system.Chmod(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chtimes implements System.Chtimes.
|
||||||
|
func (s *ExternalDiffSystem) Chtimes(name AbsPath, atime, mtime time.Time) error {
|
||||||
|
return s.system.Chtimes(name, atime, mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close frees all resources held by s.
|
||||||
|
func (s *ExternalDiffSystem) Close() error {
|
||||||
|
if !s.tempDirAbsPath.IsEmpty() {
|
||||||
|
if err := os.RemoveAll(s.tempDirAbsPath.String()); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.tempDirAbsPath = EmptyAbsPath
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glob implements System.Glob.
|
||||||
|
func (s *ExternalDiffSystem) Glob(pattern string) ([]string, error) {
|
||||||
|
return s.system.Glob(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link implements System.Link.
|
||||||
|
func (s *ExternalDiffSystem) Link(oldName, newName AbsPath) error {
|
||||||
|
// FIXME generate suitable inputs for s.command
|
||||||
|
return s.system.Link(oldName, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if s.filter.IncludeEntryTypeBits(EntryTypeDirs) {
|
||||||
|
targetRelPath, err := name.TrimDirPrefix(s.destDirAbsPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tempDirAbsPath, err := s.tempDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targetAbsPath := tempDirAbsPath.Join(targetRelPath)
|
||||||
|
if err := os.MkdirAll(targetAbsPath.String(), perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.RunDiffCommand(devNullAbsPath, targetAbsPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove implements System.Remove.
|
||||||
|
func (s *ExternalDiffSystem) Remove(name AbsPath) error {
|
||||||
|
if s.filter.IncludeEntryTypeBits(EntryTypeRemove) {
|
||||||
|
switch fileInfo, err := s.system.Lstat(name); {
|
||||||
|
case errors.Is(err, fs.ErrNotExist):
|
||||||
|
// Do nothing.
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
case s.filter.IncludeFileInfo(fileInfo):
|
||||||
|
if err := s.RunDiffCommand(name, devNullAbsPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.system.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAll implements System.RemoveAll.
|
||||||
|
func (s *ExternalDiffSystem) RemoveAll(name AbsPath) error {
|
||||||
|
if s.filter.IncludeEntryTypeBits(EntryTypeRemove) {
|
||||||
|
switch fileInfo, err := s.system.Lstat(name); {
|
||||||
|
case errors.Is(err, fs.ErrNotExist):
|
||||||
|
// Do nothing.
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
case s.filter.IncludeFileInfo(fileInfo):
|
||||||
|
if err := s.RunDiffCommand(name, devNullAbsPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunDiffCommand runs the external diff command.
|
||||||
|
func (s *ExternalDiffSystem) RunDiffCommand(destAbsPath, targetAbsPath AbsPath) error {
|
||||||
|
templateData := struct {
|
||||||
|
Destination string
|
||||||
|
Target string
|
||||||
|
}{
|
||||||
|
Destination: destAbsPath.String(),
|
||||||
|
Target: targetAbsPath.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.reverse {
|
||||||
|
templateData.Destination, templateData.Target = templateData.Target, templateData.Destination
|
||||||
|
}
|
||||||
|
|
||||||
|
args := make([]string, 0, len(s.args))
|
||||||
|
// Work around a regression introduced in 2.1.5
|
||||||
|
// (https://github.com/twpayne/chezmoi/pull/1328) in a user-friendly way.
|
||||||
|
//
|
||||||
|
// Prior to #1328, the diff.args config option was prepended to the default
|
||||||
|
// order of files to the diff command. Post #1328, the diff.args config
|
||||||
|
// option replaced all arguments to the diff command.
|
||||||
|
//
|
||||||
|
// Work around this by looking for any templates in diff.args. An arg is
|
||||||
|
// considered a template if, after execution as a template, it is not equal
|
||||||
|
// to the original arg.
|
||||||
|
anyTemplateArgs := false
|
||||||
|
for i, arg := range s.args {
|
||||||
|
tmpl, err := ParseTemplate("diff.args["+strconv.Itoa(i)+"]", []byte(arg), s.templateOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newArg, err := tmpl.ExecuteString(templateData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
args = append(args, newArg)
|
||||||
|
|
||||||
|
// Detect template arguments.
|
||||||
|
if arg != newArg {
|
||||||
|
anyTemplateArgs = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no template arguments, then append the destination and
|
||||||
|
// target paths as prior to #1328.
|
||||||
|
if !anyTemplateArgs {
|
||||||
|
args = append(args, templateData.Destination, templateData.Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
diffCmd := exec.Command(s.command, args...)
|
||||||
|
diffCmd.Stdin = os.Stdin
|
||||||
|
diffCmd.Stderr = os.Stderr
|
||||||
|
var diffCmdErr error
|
||||||
|
switch diffPagerCmd, err := s.pagerCmdFunc(); {
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
case diffPagerCmd == nil:
|
||||||
|
diffCmd.Stdout = os.Stdout
|
||||||
|
diffCmdErr = chezmoilog.LogCmdRun(slog.Default(), diffCmd)
|
||||||
|
default:
|
||||||
|
reader, writer, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = writer.Close() }()
|
||||||
|
diffPagerCmd.Stdin = reader
|
||||||
|
if err := chezmoilog.LogCmdStart(slog.Default(), diffPagerCmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
diffCmd.Stdout = writer
|
||||||
|
diffCmdErr = chezmoilog.LogCmdRun(slog.Default(), diffCmd)
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := diffPagerCmd.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swallow exit status 1 errors if the entries differ and there are actual
|
||||||
|
// differences between the entries as diff commands traditionally exit with
|
||||||
|
// code 1 in this case.
|
||||||
|
if exitError := (&exec.ExitError{}); errors.As(diffCmdErr, &exitError) && exitError.ExitCode() == 1 {
|
||||||
|
switch entriesDiffer, err := s.entriesDiffer(destAbsPath, targetAbsPath); {
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
case entriesDiffer:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diffCmdErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunScript implements System.RunScript.
|
||||||
|
func (s *ExternalDiffSystem) RunScript(scriptName RelPath, dir AbsPath, data []byte, options RunScriptOptions) error {
|
||||||
|
bits := EntryTypeScripts
|
||||||
|
if options.Condition == ScriptConditionAlways {
|
||||||
|
bits |= EntryTypeAlways
|
||||||
|
}
|
||||||
|
if s.filter.IncludeEntryTypeBits(bits) {
|
||||||
|
tempDirAbsPath, err := s.tempDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targetAbsPath := tempDirAbsPath.Join(scriptName)
|
||||||
|
if err := os.MkdirAll(targetAbsPath.Dir().String(), 0o700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
toData := data
|
||||||
|
if !s.scriptContents {
|
||||||
|
toData = nil
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(targetAbsPath.String(), toData, 0o700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.RunDiffCommand(devNullAbsPath, targetAbsPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.system.RunScript(scriptName, dir, data, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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.
|
||||||
|
// 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):
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(toAbsPath.Dir().String(), 0o700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(toAbsPath.String(), toData, perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the external diff command.
|
||||||
|
if err := s.RunDiffCommand(fromAbsPath, toAbsPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.system.WriteFile(filename, data, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// entriesDiffer returns whether the two given entries differ.
|
||||||
|
//
|
||||||
|
// This function employs negative logic, i.e. that the default is that the
|
||||||
|
// function returns that the entries DO differ unless it can prove otherwise.
|
||||||
|
func (s *ExternalDiffSystem) entriesDiffer(absPath1, absPath2 AbsPath) (bool, error) {
|
||||||
|
fileInfo1, err1 := s.Lstat(absPath1)
|
||||||
|
fileInfo2, err2 := s.Lstat(absPath2)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err1, fs.ErrNotExist) && errors.Is(err2, fs.ErrNotExist):
|
||||||
|
// If neither entry exists, then they do not differ.
|
||||||
|
return false, nil
|
||||||
|
case err1 != nil || err2 != nil:
|
||||||
|
// If lstating either entry returned an error, then combine both errors.
|
||||||
|
return true, errors.Join(err1, err2)
|
||||||
|
case fileInfo1.Mode() != fileInfo2.Mode():
|
||||||
|
// If the modes are not equal, then the entries are not equal. This
|
||||||
|
// covers the case where the entries are of different types, or that
|
||||||
|
// their permissions differ.
|
||||||
|
return true, nil
|
||||||
|
case fileInfo1.Mode()&fs.ModeType == 0:
|
||||||
|
// If both entries are files, then their contents must be equal.
|
||||||
|
contents1, err1 := s.ReadFile(absPath1)
|
||||||
|
if err1 != nil {
|
||||||
|
return true, err1
|
||||||
|
}
|
||||||
|
contents2, err2 := s.ReadFile(absPath2)
|
||||||
|
if err2 != nil {
|
||||||
|
return true, err2
|
||||||
|
}
|
||||||
|
return !bytes.Equal(contents1, contents2), nil
|
||||||
|
case fileInfo1.Mode()&fs.ModeType == fs.ModeSymlink:
|
||||||
|
// If both entries are symlinks, then their targets must be equal.
|
||||||
|
linkname1, err1 := s.Readlink(absPath1)
|
||||||
|
if err1 != nil {
|
||||||
|
return true, err1
|
||||||
|
}
|
||||||
|
linkname2, err2 := s.Readlink(absPath2)
|
||||||
|
if err2 != nil {
|
||||||
|
return true, err2
|
||||||
|
}
|
||||||
|
return linkname1 != linkname2, nil
|
||||||
|
default:
|
||||||
|
// Otherwise return that the entries DO differ. This code should not be
|
||||||
|
// reached. This does not cover the case where both entries are special
|
||||||
|
// files (e.g. device nodes with the same device numbers or UNIX domain
|
||||||
|
// sockets) but chezmoi does not manage these types of entries.
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.IsEmpty() {
|
||||||
|
tempDir, err := os.MkdirTemp("", "chezmoi-diff")
|
||||||
|
if err != nil {
|
||||||
|
return EmptyAbsPath, err
|
||||||
|
}
|
||||||
|
s.tempDirAbsPath = NewAbsPath(tempDir)
|
||||||
|
}
|
||||||
|
return s.tempDirAbsPath, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package chezmoi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var devNullAbsPath = NewAbsPath("/dev/null")
|
||||||
|
|
||||||
|
// NewAbsPathFromExtPath returns a new AbsPath by converting extPath to use
|
||||||
|
// slashes, performing tilde expansion, and making the path absolute.
|
||||||
|
func NewAbsPathFromExtPath(extPath string, homeDirAbsPath AbsPath) (AbsPath, error) {
|
||||||
|
extPath = filepath.Clean(extPath)
|
||||||
|
switch {
|
||||||
|
case extPath == "~":
|
||||||
|
return homeDirAbsPath, nil
|
||||||
|
case strings.HasPrefix(extPath, "~/"):
|
||||||
|
return homeDirAbsPath.JoinString(extPath[2:]), nil
|
||||||
|
case filepath.IsAbs(extPath):
|
||||||
|
return NewAbsPath(extPath), nil
|
||||||
|
default:
|
||||||
|
absPath, err := filepath.Abs(extPath)
|
||||||
|
if err != nil {
|
||||||
|
return EmptyAbsPath, err
|
||||||
|
}
|
||||||
|
return NewAbsPath(absPath), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizePath returns path normalized. On non-Windows systems, normalized
|
||||||
|
// paths are absolute paths.
|
||||||
|
func NormalizePath(path string) (AbsPath, error) {
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return EmptyAbsPath, err
|
||||||
|
}
|
||||||
|
return NewAbsPath(absPath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeLinkname returns linkname normalized. On non-Windows systems, it
|
||||||
|
// returns linkname unchanged.
|
||||||
|
func normalizeLinkname(linkname string) string {
|
||||||
|
return linkname
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package chezmoi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var devNullAbsPath = NewAbsPath("NUL:")
|
||||||
|
|
||||||
|
// NewAbsPathFromExtPath returns a new AbsPath by converting extPath to use
|
||||||
|
// slashes, performing tilde expansion, making the path absolute, and converting
|
||||||
|
// the volume name to uppercase.
|
||||||
|
func NewAbsPathFromExtPath(extPath string, homeDirAbsPath AbsPath) (AbsPath, error) {
|
||||||
|
extPath = filepath.Clean(extPath)
|
||||||
|
switch {
|
||||||
|
case extPath == "~":
|
||||||
|
return homeDirAbsPath, nil
|
||||||
|
case len(extPath) >= 2 && extPath[0] == '~' && isSlash(extPath[1]):
|
||||||
|
return homeDirAbsPath.JoinString(filepath.ToSlash(extPath[2:])), nil
|
||||||
|
case filepath.IsAbs(extPath):
|
||||||
|
return NewAbsPath(volumeNameToUpper(extPath)).ToSlash(), nil
|
||||||
|
default:
|
||||||
|
extPath, err := filepath.Abs(extPath)
|
||||||
|
if err != nil {
|
||||||
|
return EmptyAbsPath, err
|
||||||
|
}
|
||||||
|
return NewAbsPath(volumeNameToUpper(extPath)).ToSlash(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizePath returns path normalized. On Windows, normalized paths are
|
||||||
|
// absolute paths with a uppercase volume name and forward slashes.
|
||||||
|
func NormalizePath(path string) (AbsPath, error) {
|
||||||
|
var err error
|
||||||
|
path, err = filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return EmptyAbsPath, err
|
||||||
|
}
|
||||||
|
if n := volumeNameLen(path); n > 0 {
|
||||||
|
path = strings.ToUpper(path[:n]) + path[n:]
|
||||||
|
}
|
||||||
|
return NewAbsPath(path).ToSlash(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeLinkname returns linkname normalized. On Windows, backslashes are
|
||||||
|
// converted to forward slashes and if linkname is an absolute path then the
|
||||||
|
// volume name is converted to uppercase.
|
||||||
|
func normalizeLinkname(linkname string) string {
|
||||||
|
if filepath.IsAbs(linkname) {
|
||||||
|
return filepath.ToSlash(volumeNameToUpper(linkname))
|
||||||
|
}
|
||||||
|
return filepath.ToSlash(linkname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// volumeNameLen returns length of the leading volume name on Windows. It
|
||||||
|
// returns 0 elsewhere.
|
||||||
|
func volumeNameLen(path string) int {
|
||||||
|
if len(path) < 2 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// with drive letter
|
||||||
|
c := path[0]
|
||||||
|
if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
// is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
|
||||||
|
if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) && !isSlash(path[2]) && path[2] != '.' {
|
||||||
|
// first, leading `\\` and next shouldn't be `\`. its server name.
|
||||||
|
for n := 3; n < l-1; n++ {
|
||||||
|
// second, next '\' shouldn't be repeated.
|
||||||
|
if isSlash(path[n]) {
|
||||||
|
n++
|
||||||
|
// third, following something characters. its share name.
|
||||||
|
if !isSlash(path[n]) {
|
||||||
|
if path[n] == '.' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for ; n < l; n++ {
|
||||||
|
if isSlash(path[n]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// volumeNameToUpper returns path with the volume name converted to uppercase.
|
||||||
|
func volumeNameToUpper(path string) string {
|
||||||
|
if n := volumeNameLen(path); n > 0 {
|
||||||
|
return strings.ToUpper(path[:n]) + path[n:]
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,120 @@
|
|||||||
|
[windows] skip 'UNIX only'
|
||||||
|
|
||||||
|
chmod 755 bin/diff-pager
|
||||||
|
|
||||||
|
# test that chezmoi diff invokes diff.command when configured
|
||||||
|
exec chezmoi diff
|
||||||
|
stdout ^${HOME@R}/\.file\s+${WORK@R}/.*/\.file$
|
||||||
|
|
||||||
|
# test that chezmoi diff --use-builtin-diff uses the builtin diff even if diff.command is configured
|
||||||
|
exec chezmoi diff --use-builtin-diff
|
||||||
|
[umask:002] cmp stdout golden/diff-umask-002.diff
|
||||||
|
[umask:022] cmp stdout golden/diff-umask-022.diff
|
||||||
|
|
||||||
|
# test that chezmoi diff --reverse reverses the order of arguments
|
||||||
|
exec chezmoi diff --reverse
|
||||||
|
stdout ^${WORK@R}/.*/\.file\s+${HOME@R}/\.file$
|
||||||
|
|
||||||
|
# test that chezmoi apply --verbose uses diff.command
|
||||||
|
exec chezmoi apply --verbose
|
||||||
|
stdout ^${HOME@R}/\.file\s+${WORK@R}/.*/\.file$
|
||||||
|
|
||||||
|
chhome home2/user
|
||||||
|
|
||||||
|
# test that chezmoi diff appends the destination and target paths if diff.args does not contain any templates
|
||||||
|
exec chezmoi diff
|
||||||
|
stdout ^arg\s+${HOME@R}/\.file\s+${WORK@R}/.*/\.file$
|
||||||
|
|
||||||
|
# test that chezmoi apply --verbose uses diff.command
|
||||||
|
exec chezmoi apply --verbose
|
||||||
|
stdout ^arg\s+${HOME@R}/\.file\s+${WORK@R}/.*/\.file$
|
||||||
|
exists $HOME/.file
|
||||||
|
|
||||||
|
chhome home3/user
|
||||||
|
|
||||||
|
# test that chezmoi diff ignores exit code 1 from diff.command if the files differ
|
||||||
|
exec chezmoi diff
|
||||||
|
|
||||||
|
chhome home4/user
|
||||||
|
|
||||||
|
# test that chezmoi diff does not ignore exit code 2 from diff.command
|
||||||
|
! exec chezmoi diff
|
||||||
|
stderr 'exit status 2'
|
||||||
|
|
||||||
|
chhome home5/user
|
||||||
|
|
||||||
|
# test that chezmoi diff does not invoke diff.command for directories if directories are excluded
|
||||||
|
exec chezmoi diff
|
||||||
|
stdout \.file
|
||||||
|
! stdout \.dir
|
||||||
|
|
||||||
|
chhome home6/user
|
||||||
|
|
||||||
|
# test that chezmoi diff does not invoke the diff pager when there is no diff
|
||||||
|
exec chezmoi diff
|
||||||
|
! stdout .
|
||||||
|
|
||||||
|
# test that chezmoi diff does invoke the diff pager when there is a diff
|
||||||
|
cp golden/dot_file $CHEZMOISOURCEDIR
|
||||||
|
exec chezmoi diff
|
||||||
|
stdout diff-pager
|
||||||
|
|
||||||
|
-- bin/diff-pager --
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo diff-pager
|
||||||
|
-- golden/diff-umask-002.diff --
|
||||||
|
diff --git a/.file b/.file
|
||||||
|
index bd729e8ee3cc005444c67dc77eed60016886b5e0..b508963510528ab709627ec448026a10a64c72ef 100664
|
||||||
|
--- a/.file
|
||||||
|
+++ b/.file
|
||||||
|
@@ -1 +1 @@
|
||||||
|
-# destination contents of .file
|
||||||
|
+# target contents of .file
|
||||||
|
-- golden/diff-umask-022.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
|
||||||
|
-- golden/dot_file --
|
||||||
|
# contents of .file
|
||||||
|
-- home/user/.config/chezmoi/chezmoi.toml --
|
||||||
|
[diff]
|
||||||
|
command = "echo"
|
||||||
|
-- home/user/.file --
|
||||||
|
# destination contents of .file
|
||||||
|
-- home/user/.local/share/chezmoi/dot_file --
|
||||||
|
# target contents of .file
|
||||||
|
-- home2/user/.config/chezmoi/chezmoi.toml --
|
||||||
|
[diff]
|
||||||
|
command = "echo"
|
||||||
|
args = ["arg"]
|
||||||
|
-- home2/user/.file --
|
||||||
|
# destination contents of .file
|
||||||
|
-- home2/user/.local/share/chezmoi/dot_file --
|
||||||
|
# target contents of .file
|
||||||
|
-- home3/user/.config/chezmoi/chezmoi.toml --
|
||||||
|
[diff]
|
||||||
|
command = "false"
|
||||||
|
-- home3/user/.local/share/chezmoi/dot_file --
|
||||||
|
# target contents of .file
|
||||||
|
-- home4/user/.config/chezmoi/chezmoi.toml --
|
||||||
|
[diff]
|
||||||
|
command = "sh"
|
||||||
|
args = ["-c", "exit 2"]
|
||||||
|
-- home4/user/.local/share/chezmoi/dot_file --
|
||||||
|
# target contents of .file
|
||||||
|
-- home5/user/.config/chezmoi/chezmoi.toml --
|
||||||
|
[diff]
|
||||||
|
command = "echo"
|
||||||
|
exclude = ["dirs"]
|
||||||
|
-- home5/user/.local/share/chezmoi/dot_dir/.keep --
|
||||||
|
-- home5/user/.local/share/chezmoi/dot_file --
|
||||||
|
# contents of .file
|
||||||
|
-- home6/user/.config/chezmoi/chezmoi.toml --
|
||||||
|
[diff]
|
||||||
|
pager = "diff-pager"
|
||||||
|
-- home6/user/.local/share/chezmoi/.keep --
|
||||||
Reference in new issue
Block a user