3470 lines
113 KiB
Go
3470 lines
113 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"cmp"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log/slog"
|
|
"maps"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/Masterminds/sprig/v3"
|
|
"github.com/bartventer/httpcache"
|
|
"github.com/betterleaks/betterleaks/detect"
|
|
"github.com/coreos/go-semver/semver"
|
|
"github.com/dustin/go-humanize/english"
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/plumbing/format/diff"
|
|
"github.com/go-viper/mapstructure/v2"
|
|
"github.com/muesli/termenv"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
"github.com/twpayne/go-shell"
|
|
"github.com/twpayne/go-vfs/v5"
|
|
"github.com/twpayne/go-xdg/v6"
|
|
"golang.org/x/term"
|
|
"mvdan.cc/sh/v3/expand"
|
|
"mvdan.cc/sh/v3/syntax"
|
|
|
|
"chezmoi.io/chezmoi/v2/assets/templates"
|
|
"chezmoi.io/chezmoi/v2/internal/chezmoi"
|
|
"chezmoi.io/chezmoi/v2/internal/chezmoierrors"
|
|
"chezmoi.io/chezmoi/v2/internal/chezmoigit"
|
|
"chezmoi.io/chezmoi/v2/internal/chezmoilog"
|
|
"chezmoi.io/chezmoi/v2/internal/chezmoiset"
|
|
)
|
|
|
|
// defaultSentinel is a string value used to indicate that the default value
|
|
// should be used. It is a string unlikely to be an actual value set by the
|
|
// user.
|
|
const defaultSentinel = "\x00"
|
|
|
|
const (
|
|
logComponentKey = "component"
|
|
logComponentValueEncryption = "encryption"
|
|
logComponentValuePersistentState = "persistentState"
|
|
logComponentValueSourceState = "sourceState"
|
|
logComponentValueSystem = "system"
|
|
)
|
|
|
|
const (
|
|
groupIDAdvanced = "advanced"
|
|
groupIDDaily = "daily"
|
|
groupIDDocumentation = "documentation"
|
|
groupIDEncryption = "encryption"
|
|
groupIDInternal = "internal"
|
|
groupIDMigration = "migration"
|
|
groupIDRemote = "remote"
|
|
groupIDTemplate = "template"
|
|
)
|
|
|
|
var groups = []*cobra.Group{
|
|
{ID: groupIDDocumentation, Title: "Documentation commands:"},
|
|
{ID: groupIDDaily, Title: "Daily commands:"},
|
|
{ID: groupIDTemplate, Title: "Template commands:"},
|
|
{ID: groupIDAdvanced, Title: "Advanced commands:"},
|
|
{ID: groupIDEncryption, Title: "Encryption commands:"},
|
|
{ID: groupIDRemote, Title: "Remote commands:"},
|
|
{ID: groupIDMigration, Title: "Migration commands:"},
|
|
{ID: groupIDInternal, Title: "Internal commands:"},
|
|
}
|
|
|
|
type doPurgeOptions struct {
|
|
binary bool
|
|
cache bool
|
|
config bool
|
|
persistentState bool
|
|
sourceDir bool
|
|
workingTree bool
|
|
}
|
|
|
|
type commandConfig struct {
|
|
Command string `json:"command" mapstructure:"command" yaml:"command"`
|
|
Script string `json:"script" mapstructure:"script" yaml:"script"`
|
|
Args []string `json:"args" mapstructure:"args" yaml:"args"`
|
|
}
|
|
|
|
type hookConfig struct {
|
|
Pre commandConfig `json:"pre" mapstructure:"pre" yaml:"pre"`
|
|
Post commandConfig `json:"post" mapstructure:"post" yaml:"post"`
|
|
}
|
|
|
|
type templateConfig struct {
|
|
Options []string `json:"options" mapstructure:"options" yaml:"options"`
|
|
}
|
|
|
|
type warningsConfig struct {
|
|
ConfigFileTemplateHasChanged bool `json:"configFileTemplateHasChanged" mapstructure:"configFileTemplateHasChanged" yaml:"configFileTemplateHasChanged"`
|
|
}
|
|
|
|
// ConfigFile contains all data settable in the config file.
|
|
type ConfigFile struct {
|
|
// Global configuration.
|
|
CacheDirAbsPath chezmoi.AbsPath `json:"cacheDir" mapstructure:"cacheDir" yaml:"cacheDir"`
|
|
Color autoBool `json:"color" mapstructure:"color" yaml:"color"`
|
|
Data map[string]any `json:"data" mapstructure:"data" yaml:"data"`
|
|
Env map[string]string `json:"env" mapstructure:"env" yaml:"env"`
|
|
Format *choiceFlag `json:"format" mapstructure:"format" yaml:"format"`
|
|
DestDirAbsPath chezmoi.AbsPath `json:"destDir" mapstructure:"destDir" yaml:"destDir"`
|
|
GitHub gitHubConfig `json:"gitHub" mapstructure:"gitHub" yaml:"gitHub"`
|
|
Hooks map[string]hookConfig `json:"hooks" mapstructure:"hooks" yaml:"hooks"`
|
|
Interactive bool `json:"interactive" mapstructure:"interactive" yaml:"interactive"`
|
|
Interpreters map[string]chezmoi.Interpreter `json:"interpreters" mapstructure:"interpreters" yaml:"interpreters"`
|
|
LessInteractive bool `json:"lessInteractive" mapstructure:"lessInteractive" yaml:"lessInteractive"`
|
|
Mode chezmoi.Mode `json:"mode" mapstructure:"mode" yaml:"mode"`
|
|
Pager string `json:"pager" mapstructure:"pager" yaml:"pager"`
|
|
PagerArgs []string `json:"pagerArgs" mapstructure:"pagerArgs" yaml:"pagerArgs"`
|
|
PersistentStateAbsPath chezmoi.AbsPath `json:"persistentState" mapstructure:"persistentState" yaml:"persistentState"`
|
|
PINEntry pinEntryConfig `json:"pinentry" mapstructure:"pinentry" yaml:"pinentry"`
|
|
Progress autoBool `json:"progress" mapstructure:"progress" yaml:"progress"`
|
|
Safe bool `json:"safe" mapstructure:"safe" yaml:"safe"`
|
|
ScriptEnv map[string]string `json:"scriptEnv" mapstructure:"scriptEnv" yaml:"scriptEnv"`
|
|
ScriptTempDir chezmoi.AbsPath `json:"scriptTempDir" mapstructure:"scriptTempDir" yaml:"scriptTempDir"`
|
|
SourceDirAbsPath chezmoi.AbsPath `json:"sourceDir" mapstructure:"sourceDir" yaml:"sourceDir"`
|
|
TempDir chezmoi.AbsPath `json:"tempDir" mapstructure:"tempDir" yaml:"tempDir"`
|
|
Template templateConfig `json:"template" mapstructure:"template" yaml:"template"`
|
|
TextConv textConv `json:"textConv" mapstructure:"textConv" yaml:"textConv"`
|
|
Umask fs.FileMode `json:"umask" mapstructure:"umask" yaml:"umask"`
|
|
UseBuiltinAge autoBool `json:"useBuiltinAge" mapstructure:"useBuiltinAge" yaml:"useBuiltinAge"`
|
|
UseBuiltinGit autoBool `json:"useBuiltinGit" mapstructure:"useBuiltinGit" yaml:"useBuiltinGit"`
|
|
Verbose bool `json:"verbose" mapstructure:"verbose" yaml:"verbose"`
|
|
Warnings warningsConfig `json:"warnings" mapstructure:"warnings" yaml:"warnings"`
|
|
WorkingTreeAbsPath chezmoi.AbsPath `json:"workingTree" mapstructure:"workingTree" yaml:"workingTree"`
|
|
|
|
// Password manager configurations.
|
|
AWSSecretsManager awsSecretsManagerConfig `json:"awsSecretsManager" mapstructure:"awsSecretsManager" yaml:"awsSecretsManager"`
|
|
AzureKeyVault azureKeyVaultConfig `json:"azureKeyVault" mapstructure:"azureKeyVault" yaml:"azureKeyVault"`
|
|
Bitwarden bitwardenConfig `json:"bitwarden" mapstructure:"bitwarden" yaml:"bitwarden"`
|
|
BitwardenSecrets bitwardenSecretsConfig `json:"bitwardenSecrets" mapstructure:"bitwardenSecrets" yaml:"bitwardenSecrets"`
|
|
Dashlane dashlaneConfig `json:"dashlane" mapstructure:"dashlane" yaml:"dashlane"`
|
|
Doppler dopplerConfig `json:"doppler" mapstructure:"doppler" yaml:"doppler"`
|
|
Ejson ejsonConfig `json:"ejson" mapstructure:"ejson" yaml:"ejson"`
|
|
Gopass gopassConfig `json:"gopass" mapstructure:"gopass" yaml:"gopass"`
|
|
Keepassxc keepassxcConfig `json:"keepassxc" mapstructure:"keepassxc" yaml:"keepassxc"`
|
|
Keeper keeperConfig `json:"keeper" mapstructure:"keeper" yaml:"keeper"`
|
|
Lastpass lastpassConfig `json:"lastpass" mapstructure:"lastpass" yaml:"lastpass"`
|
|
Onepassword onepasswordConfig `json:"onepassword" mapstructure:"onepassword" yaml:"onepassword"`
|
|
Pass passConfig `json:"pass" mapstructure:"pass" yaml:"pass"`
|
|
Passhole passholeConfig `json:"passhole" mapstructure:"passhole" yaml:"passhole"`
|
|
ProtonPass protonPassConfig `json:"protonPass" mapstructure:"protonPass" yaml:"protonPass"`
|
|
RBW rbwConfig `json:"rbw" mapstructure:"rbw" yaml:"rbw"`
|
|
Secret secretConfig `json:"secret" mapstructure:"secret" yaml:"secret"`
|
|
Vault vaultConfig `json:"vault" mapstructure:"vault" yaml:"vault"`
|
|
|
|
// Encryption configurations.
|
|
Encryption string `json:"encryption" mapstructure:"encryption" yaml:"encryption"`
|
|
Age chezmoi.AgeEncryption `json:"age" mapstructure:"age" yaml:"age"`
|
|
GPG chezmoi.GPGEncryption `json:"gpg" mapstructure:"gpg" yaml:"gpg"`
|
|
|
|
// Command configurations.
|
|
Add addCmdConfig `json:"add" mapstructure:"add" yaml:"add"`
|
|
CD cdCmdConfig `json:"cd" mapstructure:"cd" yaml:"cd"`
|
|
Completion completionCmdConfig `json:"completion" mapstructure:"completion" yaml:"completion"`
|
|
Docker dockerCmdConfig `json:"docker" mapstructure:"docker" yaml:"docker"`
|
|
Diff diffCmdConfig `json:"diff" mapstructure:"diff" yaml:"diff"`
|
|
Edit editCmdConfig `json:"edit" mapstructure:"edit" yaml:"edit"`
|
|
Git gitCmdConfig `json:"git" mapstructure:"git" yaml:"git"`
|
|
Merge mergeCmdConfig `json:"merge" mapstructure:"merge" yaml:"merge"`
|
|
Status statusCmdConfig `json:"status" mapstructure:"status" yaml:"status"`
|
|
Update updateCmdConfig `json:"update" mapstructure:"update" yaml:"update"`
|
|
Verify verifyCmdConfig `json:"verify" mapstructure:"verify" yaml:"verify"`
|
|
}
|
|
|
|
// A Config represents a configuration.
|
|
type Config struct {
|
|
ConfigFile
|
|
|
|
// Global configuration.
|
|
ageRecipient string
|
|
ageRecipientFile string
|
|
configFormat *choiceFlag
|
|
debug bool
|
|
dryRun bool
|
|
force bool
|
|
homeDir string
|
|
keepGoing bool
|
|
noPager bool
|
|
noTTY bool
|
|
outputAbsPath chezmoi.AbsPath
|
|
refreshExternals chezmoi.RefreshExternals
|
|
skipSecrets bool
|
|
sourcePath bool
|
|
errorOnConflict bool
|
|
templateFuncs template.FuncMap
|
|
useBuiltinDiff bool
|
|
|
|
// Password manager data.
|
|
gitHub gitHubData
|
|
keyring keyringData
|
|
|
|
// Command configurations, not settable in the config file.
|
|
age ageCmdConfig
|
|
ageKeygen ageKeygenCmdConfig
|
|
apply applyCmdConfig
|
|
archive archiveCmdConfig
|
|
chattr chattrCmdConfig
|
|
data dataCmdConfig
|
|
destroy destroyCmdConfig
|
|
doctor doctorCmdConfig
|
|
dump dumpCmdConfig
|
|
dumpConfig dumpConfigCmdConfig
|
|
executeTemplate executeTemplateCmdConfig
|
|
generate generateCmdConfig
|
|
ignored ignoredCmdConfig
|
|
_import importCmdConfig
|
|
init initCmdConfig
|
|
managed managedCmdConfig
|
|
mergeAll mergeAllCmdConfig
|
|
ssh sshCmdConfig
|
|
purge purgeCmdConfig
|
|
reAdd reAddCmdConfig
|
|
secret secretCmdConfig
|
|
state stateCmdConfig
|
|
unmanaged unmanagedCmdConfig
|
|
upgrade upgradeCmdConfig
|
|
|
|
// Common configuration.
|
|
interactiveTemplateFuncs interactiveTemplateFuncsConfig
|
|
overrideData string
|
|
overrideDataFileAbsPath chezmoi.AbsPath
|
|
|
|
// Version information.
|
|
version semver.Version
|
|
versionInfo VersionInfo
|
|
versionStr string
|
|
|
|
// Configuration.
|
|
fileSystem vfs.FS
|
|
bds *xdg.BaseDirectorySpecification
|
|
defaultConfigFileAbsPath chezmoi.AbsPath
|
|
defaultConfigFileAbsPathErr error
|
|
customConfigFileAbsPath chezmoi.AbsPath
|
|
baseSystem chezmoi.System
|
|
sourceSystem chezmoi.System
|
|
destSystem chezmoi.System
|
|
persistentState chezmoi.PersistentState
|
|
httpClient *http.Client
|
|
logger *slog.Logger
|
|
|
|
// Computed configuration.
|
|
commandDirAbsPath chezmoi.AbsPath
|
|
homeDirAbsPath chezmoi.AbsPath
|
|
encryption chezmoi.Encryption
|
|
sourceDirAbsPath chezmoi.AbsPath
|
|
sourceDirAbsPathErr error
|
|
sourceState *chezmoi.SourceState
|
|
sourceStateErr error
|
|
templateData *templateData
|
|
betterleaksDetector *detect.Detector
|
|
betterleaksDetectorErr error
|
|
|
|
stdin io.Reader
|
|
stdout io.Writer
|
|
stderr io.Writer
|
|
bufioReader *bufio.Reader
|
|
diffPagerCmdStdin io.WriteCloser
|
|
diffPagerCmd *exec.Cmd
|
|
|
|
tempDirs map[string]chezmoi.AbsPath
|
|
|
|
ioregData ioregData
|
|
|
|
restoreWindowsConsole func() error
|
|
}
|
|
|
|
type templateDataFlags struct {
|
|
dryRun bool
|
|
force bool
|
|
keepGoing bool
|
|
noPager bool
|
|
noTTY bool
|
|
}
|
|
|
|
type templateData struct {
|
|
arch string
|
|
args []string
|
|
cacheDir string
|
|
command string
|
|
commandDir string
|
|
config map[string]any
|
|
configFile string
|
|
destDir string
|
|
executable string
|
|
flags templateDataFlags
|
|
fqdnHostname string
|
|
gid string
|
|
group string
|
|
homeDir string
|
|
hostname string
|
|
kernel map[string]any
|
|
os string
|
|
osRelease map[string]any
|
|
pathListSeparator string
|
|
pathSeparator string
|
|
rawHomeDir string
|
|
sourceDir string
|
|
uid string
|
|
username string
|
|
version map[string]any
|
|
windowsVersion map[string]any
|
|
workingTree string
|
|
}
|
|
|
|
// A configOption sets and option on a Config.
|
|
type configOption func(*Config) error
|
|
|
|
type configState struct {
|
|
ConfigTemplateContentsSHA256 chezmoi.HexBytes `json:"configTemplateContentsSHA256" yaml:"configTemplateContentsSHA256"` //nolint:tagliatelle
|
|
}
|
|
|
|
var (
|
|
chezmoiRelPath = chezmoi.NewRelPath("chezmoi")
|
|
persistentStateFileRelPath = chezmoi.NewRelPath("chezmoistate.boltdb")
|
|
|
|
configStateKey = []byte("configState")
|
|
|
|
defaultAgeEncryptionConfig = chezmoi.AgeEncryption{
|
|
Command: "age",
|
|
Suffix: ".age",
|
|
}
|
|
defaultGPGEncryptionConfig = chezmoi.GPGEncryption{
|
|
Command: "gpg",
|
|
Suffix: ".asc",
|
|
}
|
|
|
|
whitespaceRx = regexp.MustCompile(`\s+`)
|
|
|
|
commonFlagCompletionFuncs = map[string]func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective){
|
|
"exclude": chezmoi.EntryTypeSetFlagCompletionFunc,
|
|
"include": chezmoi.EntryTypeSetFlagCompletionFunc,
|
|
}
|
|
)
|
|
|
|
// newConfig creates a new Config with the given options.
|
|
func newConfig(options ...configOption) (*Config, error) {
|
|
userHomeDir, err := chezmoi.UserHomeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
homeDirAbsPath, err := chezmoi.NormalizePath(userHomeDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bds, err := xdg.NewBaseDirectorySpecification()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger := slog.Default()
|
|
|
|
c := &Config{
|
|
ConfigFile: newConfigFile(bds),
|
|
|
|
// Global configuration.
|
|
configFormat: newChoiceFlag("", readDataFormatValues),
|
|
homeDir: userHomeDir,
|
|
templateFuncs: sprig.TxtFuncMap(),
|
|
|
|
// Command configurations.
|
|
apply: applyCmdConfig{
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
recursive: true,
|
|
},
|
|
archive: archiveCmdConfig{
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
format: newChoiceFlag("tar.gz", archiveFormatValues),
|
|
recursive: true,
|
|
},
|
|
data: dataCmdConfig{
|
|
format: newChoiceFlag("", writeDataFormatValues),
|
|
},
|
|
dump: dumpCmdConfig{
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
format: newChoiceFlag("", writeDataFormatValues),
|
|
recursive: true,
|
|
},
|
|
dumpConfig: dumpConfigCmdConfig{
|
|
format: newChoiceFlag("", writeDataFormatValues),
|
|
},
|
|
executeTemplate: executeTemplateCmdConfig{
|
|
stdinIsATTY: true,
|
|
},
|
|
generate: generateCmdConfig{
|
|
installInitShellSh: generateInstallInitShellShCmdConfig{
|
|
interactive: true,
|
|
shell: true,
|
|
},
|
|
},
|
|
_import: importCmdConfig{
|
|
destination: homeDirAbsPath,
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
},
|
|
init: initCmdConfig{
|
|
data: true,
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
guessRepoURL: true,
|
|
recurseSubmodules: true,
|
|
},
|
|
managed: managedCmdConfig{
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
format: newChoiceFlag(formatJSON, writeDataFormatValues),
|
|
pathStyle: newChoiceFlag(pathStyleRelative, sourceOrTargetPathStyleValues),
|
|
},
|
|
mergeAll: mergeAllCmdConfig{
|
|
recursive: true,
|
|
},
|
|
reAdd: reAddCmdConfig{
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
recursive: true,
|
|
},
|
|
ssh: sshCmdConfig{
|
|
shell: true,
|
|
},
|
|
state: stateCmdConfig{
|
|
data: stateDataCmdConfig{
|
|
format: newChoiceFlag(formatJSON, writeDataFormatValues),
|
|
},
|
|
dump: stateDumpCmdConfig{
|
|
format: newChoiceFlag(formatJSON, writeDataFormatValues),
|
|
},
|
|
getBucket: stateGetBucketCmdConfig{
|
|
format: newChoiceFlag(formatJSON, writeDataFormatValues),
|
|
},
|
|
},
|
|
unmanaged: unmanagedCmdConfig{
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
pathStyle: newChoiceFlag(pathStyleRelative, targetPathStyleValues),
|
|
},
|
|
|
|
// Configuration.
|
|
fileSystem: vfs.OSFS,
|
|
bds: bds,
|
|
logger: logger,
|
|
|
|
// Computed configuration.
|
|
homeDirAbsPath: homeDirAbsPath,
|
|
|
|
tempDirs: make(map[string]chezmoi.AbsPath),
|
|
|
|
stdin: os.Stdin,
|
|
stdout: os.Stdout,
|
|
stderr: os.Stderr,
|
|
}
|
|
|
|
// Override sprig template functions. Delete them from the template function
|
|
// map first to avoid a duplicate function panic.
|
|
for _, templateFunc := range []string{
|
|
"fromJson",
|
|
"quote",
|
|
"splitList",
|
|
"squote",
|
|
"toPrettyJson",
|
|
"toString",
|
|
"toStrings",
|
|
} {
|
|
if _, ok := c.templateFuncs[templateFunc]; !ok {
|
|
panic(templateFunc + ": deleting non-existent template function")
|
|
}
|
|
delete(c.templateFuncs, templateFunc)
|
|
}
|
|
|
|
// The completion template function is added in persistentPreRunRootE as
|
|
// it needs a *cobra.Command, which we don't yet have.
|
|
for key, value := range map[string]any{
|
|
"abortEmpty": chezmoi.AbortEmptyTemplateFunc,
|
|
"awsSecretsManager": c.awsSecretsManagerTemplateFunc,
|
|
"awsSecretsManagerRaw": c.awsSecretsManagerRawTemplateFunc,
|
|
"azureKeyVault": c.azureKeyVaultTemplateFunc,
|
|
"bitwarden": c.bitwardenTemplateFunc,
|
|
"bitwardenAttachment": c.bitwardenAttachmentTemplateFunc,
|
|
"bitwardenAttachmentByRef": c.bitwardenAttachmentByRefTemplateFunc,
|
|
"bitwardenFields": c.bitwardenFieldsTemplateFunc,
|
|
"bitwardenSecrets": c.bitwardenSecretsTemplateFunc,
|
|
"comment": c.commentTemplateFunc,
|
|
"dashlaneNote": c.dashlaneNoteTemplateFunc,
|
|
"dashlanePassword": c.dashlanePasswordTemplateFunc,
|
|
"debugf": c.debugfTemplateFunc,
|
|
"decrypt": c.decryptTemplateFunc,
|
|
"deleteValueAtPath": c.deleteValueAtPathTemplateFunc,
|
|
"doppler": c.dopplerTemplateFunc,
|
|
"dopplerProjectJson": c.dopplerProjectJSONTemplateFunc,
|
|
"ejsonDecrypt": c.ejsonDecryptTemplateFunc,
|
|
"ejsonDecryptWithKey": c.ejsonDecryptWithKeyTemplateFunc,
|
|
"encrypt": c.encryptTemplateFunc,
|
|
"ensureLinePrefix": c.ensureLinePrefixTemplateFunc,
|
|
"eqFold": c.eqFoldTemplateFunc,
|
|
"exec": c.execTemplateFunc,
|
|
"findExecutable": c.findExecutableTemplateFunc,
|
|
"findOneExecutable": c.findOneExecutableTemplateFunc,
|
|
"fromIni": c.fromIniTemplateFunc,
|
|
"fromJson": c.fromJsonTemplateFunc,
|
|
"fromJsonc": c.fromJsoncTemplateFunc,
|
|
"fromToml": c.fromTomlTemplateFunc,
|
|
"fromYaml": c.fromYamlTemplateFunc,
|
|
"getRedirectedURL": c.getRedirectedURLTemplateFunc,
|
|
"gitHubKeys": c.gitHubKeysTemplateFunc,
|
|
"gitHubLatestRelease": c.gitHubLatestReleaseTemplateFunc,
|
|
"gitHubLatestReleaseAssetURL": c.gitHubLatestReleaseAssetURLTemplateFunc,
|
|
"gitHubLatestTag": c.gitHubLatestTagTemplateFunc,
|
|
"gitHubRelease": c.gitHubReleaseTemplateFunc,
|
|
"gitHubReleaseAssetURL": c.gitHubReleaseAssetURLTemplateFunc,
|
|
"gitHubReleases": c.gitHubReleasesTemplateFunc,
|
|
"gitHubTags": c.gitHubTagsTemplateFunc,
|
|
"glob": c.globTemplateFunc,
|
|
"globCaseInsensitive": c.globCaseInsensitiveTemplateFunc,
|
|
"gopass": c.gopassTemplateFunc,
|
|
"gopassCat": c.gopassCatTemplateFunc,
|
|
"gopassRaw": c.gopassRawTemplateFunc,
|
|
"hexDecode": c.hexDecodeTemplateFunc,
|
|
"hexEncode": c.hexEncodeTemplateFunc,
|
|
"include": c.includeTemplateFunc,
|
|
"includeTemplate": c.includeTemplateTemplateFunc,
|
|
"ioreg": c.ioregTemplateFunc,
|
|
"isExecutable": c.isExecutableTemplateFunc,
|
|
"joinPath": c.joinPathTemplateFunc,
|
|
"jq": c.jqTemplateFunc,
|
|
"keepassxc": c.keepassxcTemplateFunc,
|
|
"keepassxcAttachment": c.keepassxcAttachmentTemplateFunc,
|
|
"keepassxcAttribute": c.keepassxcAttributeTemplateFunc,
|
|
"keeper": c.keeperTemplateFunc,
|
|
"keeperDataFields": c.keeperDataFieldsTemplateFunc,
|
|
"keeperFindPassword": c.keeperFindPasswordTemplateFunc,
|
|
"keyring": c.keyringTemplateFunc,
|
|
"lastpass": c.lastpassTemplateFunc,
|
|
"lastpassRaw": c.lastpassRawTemplateFunc,
|
|
"lookPath": c.lookPathTemplateFunc,
|
|
"lstat": c.lstatTemplateFunc,
|
|
"mozillaInstallHash": c.mozillaInstallHashTemplateFunc,
|
|
"onepassword": c.onepasswordTemplateFunc,
|
|
"onepasswordDetailsFields": c.onepasswordDetailsFieldsTemplateFunc,
|
|
"onepasswordDocument": c.onepasswordDocumentTemplateFunc,
|
|
"onepasswordItemFields": c.onepasswordItemFieldsTemplateFunc,
|
|
"onepasswordRead": c.onepasswordReadTemplateFunc,
|
|
"output": c.outputTemplateFunc,
|
|
"outputList": c.outputListTemplateFunc,
|
|
"pass": c.passTemplateFunc,
|
|
"passFields": c.passFieldsTemplateFunc,
|
|
"passRaw": c.passRawTemplateFunc,
|
|
"passhole": c.passholeTemplateFunc,
|
|
"protonPass": c.protonPassTemplateFunc,
|
|
"protonPassAttachment": c.protonPassAttachmentTemplateFunc,
|
|
"protonPassJSON": c.protonPassJSONTemplateFunc,
|
|
"pruneEmptyDicts": c.pruneEmptyDictsTemplateFunc,
|
|
"quote": c.quoteTemplateFunc,
|
|
"quoteList": c.quoteListTemplateFunc,
|
|
"rbw": c.rbwTemplateFunc,
|
|
"rbwFields": c.rbwFieldsTemplateFunc,
|
|
"replaceAllRegex": c.replaceAllRegexTemplateFunc,
|
|
"secret": c.secretTemplateFunc,
|
|
"secretJSON": c.secretJSONTemplateFunc,
|
|
"setValueAtPath": c.setValueAtPathTemplateFunc,
|
|
"shellQuote": c.shellQuoteTemplateFunc,
|
|
"shellQuoteList": c.shellQuoteListTemplateFunc,
|
|
"splitList": c.splitListTemplateFunc,
|
|
"squote": c.squoteTemplateFunc,
|
|
"stat": c.statTemplateFunc,
|
|
"stdinIsATTY": c.stdinIsATTYTemplateFunc,
|
|
"toIni": c.toIniTemplateFunc,
|
|
"toPrettyJson": c.toPrettyJsonTemplateFunc,
|
|
"toString": c.toStringTemplateFunc,
|
|
"toStrings": c.toStringsTemplateFunc,
|
|
"toToml": c.toTomlTemplateFunc,
|
|
"toYaml": c.toYamlTemplateFunc,
|
|
"vault": c.vaultTemplateFunc,
|
|
"warnf": c.warnfTemplateFunc,
|
|
} {
|
|
c.addTemplateFunc(key, value)
|
|
}
|
|
|
|
for _, option := range options {
|
|
if err := option(c); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.commandDirAbsPath, err = chezmoi.NormalizePath(wd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.homeDirAbsPath, err = chezmoi.NormalizePath(c.homeDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.defaultConfigFileAbsPath, c.defaultConfigFileAbsPathErr = c.defaultConfigFile(c.fileSystem, c.bds)
|
|
c.SourceDirAbsPath, err = c.defaultSourceDir(c.fileSystem, c.bds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.DestDirAbsPath = c.homeDirAbsPath
|
|
c._import.destination = c.homeDirAbsPath
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// Close closes resources associated with c.
|
|
func (c *Config) Close() error {
|
|
errs := make([]error, 0, len(c.tempDirs))
|
|
for _, tempDirAbsPath := range c.tempDirs {
|
|
err := os.RemoveAll(tempDirAbsPath.String())
|
|
chezmoilog.InfoOrError(c.logger, "RemoveAll", err,
|
|
chezmoilog.Stringer("tempDir", tempDirAbsPath),
|
|
)
|
|
errs = append(errs, err)
|
|
}
|
|
return chezmoierrors.Combine(errs...)
|
|
}
|
|
|
|
// addTemplateFunc adds the template function with the given key and value
|
|
// to c. It panics if there is already an existing template function with the
|
|
// same key.
|
|
func (c *Config) addTemplateFunc(key string, value any) {
|
|
if _, ok := c.templateFuncs[key]; ok {
|
|
panic(key + ": already defined")
|
|
}
|
|
c.templateFuncs[key] = value
|
|
}
|
|
|
|
type applyArgsOptions struct {
|
|
cmd *cobra.Command
|
|
filter *chezmoi.EntryTypeFilter
|
|
init bool
|
|
parentDirs bool
|
|
recursive bool
|
|
umask fs.FileMode
|
|
preApplyFunc chezmoi.PreApplyFunc
|
|
}
|
|
|
|
// applyArgs is the core of all commands that make changes to a target system.
|
|
// It checks config file freshness, reads the source state, and then applies the
|
|
// source state for each target entry in args. If args is empty then the source
|
|
// state is applied to all target entries.
|
|
func (c *Config) applyArgs(
|
|
ctx context.Context,
|
|
targetSystem chezmoi.System,
|
|
targetDirAbsPath chezmoi.AbsPath,
|
|
args []string,
|
|
options applyArgsOptions,
|
|
) error {
|
|
if options.init {
|
|
if err := c.createAndReloadConfigFile(options.cmd); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var currentConfigTemplateContentsSHA256 []byte
|
|
configTemplate, err := c.findConfigTemplate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if configTemplate != nil {
|
|
currentConfigTemplateContentsSHA256 = sha256Sum(configTemplate.contents)
|
|
}
|
|
var previousConfigTemplateContentsSHA256 []byte
|
|
if configStateData, err := c.persistentState.Get(chezmoi.ConfigStateBucket, configStateKey); err != nil {
|
|
return err
|
|
} else if configStateData != nil {
|
|
var configState configState
|
|
if err := json.Unmarshal(configStateData, &configState); err != nil {
|
|
return err
|
|
}
|
|
previousConfigTemplateContentsSHA256 = []byte(configState.ConfigTemplateContentsSHA256)
|
|
}
|
|
configTemplatesEmpty := currentConfigTemplateContentsSHA256 == nil && previousConfigTemplateContentsSHA256 == nil
|
|
configTemplateContentsUnchanged := configTemplatesEmpty ||
|
|
bytes.Equal(currentConfigTemplateContentsSHA256, previousConfigTemplateContentsSHA256)
|
|
if !configTemplateContentsUnchanged {
|
|
if c.force {
|
|
if configTemplate == nil {
|
|
if err := c.persistentState.Delete(chezmoi.ConfigStateBucket, configStateKey); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
configStateValue, err := chezmoi.FormatJSON.Marshal(configState{
|
|
ConfigTemplateContentsSHA256: chezmoi.HexBytes(currentConfigTemplateContentsSHA256),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := c.persistentState.Set(chezmoi.ConfigStateBucket, configStateKey, configStateValue); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if c.Warnings.ConfigFileTemplateHasChanged {
|
|
c.errorf("warning: config file template has changed, run chezmoi init to regenerate config file\n")
|
|
}
|
|
}
|
|
|
|
sourceState, err := c.getSourceState(ctx, options.cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var targetRelPaths []chezmoi.RelPath
|
|
switch {
|
|
case len(args) == 0:
|
|
targetRelPaths = sourceState.TargetRelPaths()
|
|
case c.sourcePath:
|
|
targetRelPaths, err = c.targetRelPathsBySourcePath(sourceState, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
targetRelPaths, err = c.targetRelPaths(sourceState, args, targetRelPathsOptions{
|
|
recursive: options.recursive,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if options.parentDirs {
|
|
targetRelPaths = prependParentRelPaths(targetRelPaths)
|
|
}
|
|
|
|
applyOptions := chezmoi.ApplyOptions{
|
|
Filter: options.filter,
|
|
PreApplyFunc: options.preApplyFunc,
|
|
Umask: options.umask,
|
|
}
|
|
|
|
keptGoingAfterErr := false
|
|
for _, targetRelPath := range targetRelPaths {
|
|
switch err := sourceState.Apply(targetSystem, c.destSystem, c.persistentState, targetDirAbsPath, targetRelPath, applyOptions); {
|
|
case errors.Is(err, fs.SkipDir):
|
|
continue
|
|
case err != nil:
|
|
err = fmt.Errorf("%s: %w", targetRelPath, err)
|
|
if !c.keepGoing {
|
|
return err
|
|
}
|
|
c.errorf("%v\n", err)
|
|
keptGoingAfterErr = true
|
|
}
|
|
}
|
|
|
|
switch err := sourceState.PostApply(targetSystem, c.persistentState, targetDirAbsPath, targetRelPaths); {
|
|
case err != nil && c.keepGoing:
|
|
c.errorf("%v\n", err)
|
|
keptGoingAfterErr = true
|
|
case err != nil:
|
|
return err
|
|
}
|
|
|
|
if keptGoingAfterErr {
|
|
return chezmoi.ExitCodeError(1)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// builtinDiffFile outputs the diff between fromData and fromMode and toData and
|
|
// toMode at path.
|
|
func (c *Config) builtinDiffFile(
|
|
relPath chezmoi.RelPath,
|
|
fromData []byte, fromMode fs.FileMode,
|
|
toData []byte, toMode fs.FileMode,
|
|
) error {
|
|
builder := strings.Builder{}
|
|
unifiedEncoder := diff.NewUnifiedEncoder(&builder, diff.DefaultContextLines)
|
|
color := c.Color.Value(c.colorAutoFunc)
|
|
if color {
|
|
unifiedEncoder.SetColor(diff.NewColorConfig())
|
|
}
|
|
if fromMode.IsRegular() {
|
|
var err error
|
|
fromData, _, err = c.TextConv.convert(relPath.String(), fromData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if toMode.IsRegular() {
|
|
var err error
|
|
toData, _, err = c.TextConv.convert(relPath.String(), toData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
diffPatch, err := chezmoi.DiffPatch(relPath, fromData, fromMode, toData, toMode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := unifiedEncoder.Encode(diffPatch); err != nil {
|
|
return err
|
|
}
|
|
return c.pageDiffOutput(builder.String())
|
|
}
|
|
|
|
// checkVersion checks that chezmoi is at least the required version for the
|
|
// source state.
|
|
func (c *Config) checkVersion() error {
|
|
versionAbsPath := c.SourceDirAbsPath.JoinString(chezmoi.VersionName)
|
|
switch data, err := c.baseSystem.ReadFile(versionAbsPath); {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
case err != nil:
|
|
return err
|
|
default:
|
|
minVersion, err := semver.NewVersion(strings.TrimSpace(string(data)))
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %q: %w", versionAbsPath, data, err)
|
|
}
|
|
var zeroVersion semver.Version
|
|
if c.version != zeroVersion && c.version.LessThan(*minVersion) {
|
|
return &chezmoi.TooOldError{
|
|
Need: *minVersion,
|
|
Have: c.version,
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// cmdOutput returns the of running the command name with args in dirAbsPath.
|
|
func (c *Config) cmdOutput(dirAbsPath chezmoi.AbsPath, name string, args []string) ([]byte, error) {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Stderr = os.Stderr
|
|
if !dirAbsPath.IsEmpty() {
|
|
dirRawAbsPath, err := c.baseSystem.RawPath(dirAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd.Dir = dirRawAbsPath.String()
|
|
}
|
|
return chezmoilog.LogCmdOutput(slog.Default(), cmd)
|
|
}
|
|
|
|
// colorAutoFunc detects whether color should be used.
|
|
func (c *Config) colorAutoFunc() bool {
|
|
if _, ok := os.LookupEnv("NO_COLOR"); ok {
|
|
return false
|
|
}
|
|
if stdout, ok := c.stdout.(*os.File); ok {
|
|
return term.IsTerminal(int(stdout.Fd()))
|
|
}
|
|
return false
|
|
}
|
|
|
|
// createAndReloadConfigFile creates a config file if it there is a config file
|
|
// template and reloads it.
|
|
func (c *Config) createAndReloadConfigFile(cmd *cobra.Command) error {
|
|
// Refresh the source directory, as there might be a .chezmoiroot file and
|
|
// the template data is set before .chezmoiroot is read.
|
|
sourceDirAbsPath, err := c.getSourceDirAbsPath(&getSourceDirAbsPathOptions{
|
|
refresh: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.templateData.sourceDir = sourceDirAbsPath.String()
|
|
if err := os.Setenv("CHEZMOI_SOURCE_DIR", sourceDirAbsPath.String()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find config template, execute it, and create config file.
|
|
configTemplate, err := c.findConfigTemplate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if configTemplate == nil {
|
|
return c.persistentState.Delete(chezmoi.ConfigStateBucket, configStateKey)
|
|
}
|
|
|
|
configFileContents, err := c.createConfigFileContents(configTemplate.targetRelPath, configTemplate.contents, cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate the config file.
|
|
var configFile ConfigFile
|
|
if err := c.decodeConfigContents(configTemplate.format, configFileContents, &configFile); err != nil {
|
|
return fmt.Errorf("%s: %w", configTemplate.sourceAbsPath, err)
|
|
}
|
|
|
|
// Write the config.
|
|
configPath := c.init.configPath
|
|
if c.init.configPath.IsEmpty() {
|
|
if c.customConfigFileAbsPath.IsEmpty() {
|
|
configPath = chezmoi.NewAbsPath(c.bds.ConfigHome).Join(chezmoiRelPath, configTemplate.targetRelPath)
|
|
} else {
|
|
configPath = c.customConfigFileAbsPath
|
|
}
|
|
}
|
|
if err := chezmoi.MkdirAll(c.destSystem, configPath.Dir(), fs.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
if err := c.destSystem.WriteFile(configPath, configFileContents, 0o600); err != nil {
|
|
return err
|
|
}
|
|
|
|
configStateValue, err := chezmoi.FormatJSON.Marshal(configState{
|
|
ConfigTemplateContentsSHA256: chezmoi.HexBytes(sha256Sum(configTemplate.contents)),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := c.persistentState.Set(chezmoi.ConfigStateBucket, configStateKey, configStateValue); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Reload the config.
|
|
if err := c.decodeConfigContents(configTemplate.format, configFileContents, &c.ConfigFile); err != nil {
|
|
return fmt.Errorf("%s: %w", configTemplate.sourceAbsPath, err)
|
|
}
|
|
|
|
if err := c.setEncryption(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.setEnvironmentVariables()
|
|
}
|
|
|
|
// createConfigFileContents creates a config file using a template and returns
|
|
// its contents.
|
|
func (c *Config) createConfigFileContents(filename chezmoi.RelPath, data []byte, cmd *cobra.Command) ([]byte, error) {
|
|
// Clone funcMap and restore it after creating the config.
|
|
// This ensures that the init template functions
|
|
// are removed before "normal" template parsing.
|
|
funcMap := make(template.FuncMap)
|
|
chezmoi.RecursiveMerge(funcMap, c.templateFuncs)
|
|
defer func() {
|
|
c.templateFuncs = funcMap
|
|
}()
|
|
|
|
initTemplateFuncs := map[string]any{
|
|
"exit": c.exitInitTemplateFunc,
|
|
"promptBool": c.promptBoolInteractiveTemplateFunc,
|
|
"promptBoolOnce": c.promptBoolOnceInteractiveTemplateFunc,
|
|
"promptChoice": c.promptChoiceInteractiveTemplateFunc,
|
|
"promptChoiceOnce": c.promptChoiceOnceInteractiveTemplateFunc,
|
|
"promptInt": c.promptIntInteractiveTemplateFunc,
|
|
"promptIntOnce": c.promptIntOnceInteractiveTemplateFunc,
|
|
"promptMultichoice": c.promptMultichoiceInteractiveTemplateFunc,
|
|
"promptMultichoiceOnce": c.promptMultichoiceOnceInteractiveTemplateFunc,
|
|
"promptString": c.promptStringInteractiveTemplateFunc,
|
|
"promptStringOnce": c.promptStringOnceInteractiveTemplateFunc,
|
|
"writeToStdout": c.writeToStdout,
|
|
}
|
|
chezmoi.RecursiveMerge(c.templateFuncs, initTemplateFuncs)
|
|
|
|
tmpl, err := chezmoi.ParseTemplate(filename.String(), data, chezmoi.TemplateOptions{
|
|
Funcs: c.templateFuncs,
|
|
Options: slices.Clone(c.Template.Options),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
templateData := c.getTemplateDataMap(cmd)
|
|
if c.init.data {
|
|
chezmoi.RecursiveMerge(templateData, c.Data)
|
|
}
|
|
return tmpl.Execute(templateData)
|
|
}
|
|
|
|
// decodeConfigContents decodes data in format into configFile.
|
|
func (c *Config) decodeConfigContents(format chezmoi.Format, contents []byte, configFile *ConfigFile) error {
|
|
var configMap map[string]any
|
|
if err := format.Unmarshal(contents, &configMap); err != nil {
|
|
return err
|
|
}
|
|
return c.decodeConfigMap(configMap, configFile)
|
|
}
|
|
|
|
// decodeConfigFile decodes the config file at configFileAbsPath into
|
|
// configFile.
|
|
func (c *Config) decodeConfigFile(configFileAbsPath chezmoi.AbsPath, configFile *ConfigFile) error {
|
|
var format chezmoi.Format
|
|
switch formatStr := c.configFormat.String(); formatStr {
|
|
case "":
|
|
var err error
|
|
format, err = chezmoi.FormatFromAbsPath(configFileAbsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case formatJSON:
|
|
format = chezmoi.FormatJSON
|
|
case formatTOML:
|
|
format = chezmoi.FormatTOML
|
|
case formatYAML:
|
|
format = chezmoi.FormatYAML
|
|
default:
|
|
return fmt.Errorf("%s: invalid format", formatStr)
|
|
}
|
|
|
|
configFileContents, err := c.fileSystem.ReadFile(configFileAbsPath.String())
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", configFileAbsPath, err)
|
|
}
|
|
|
|
if err := c.decodeConfigContents(format, configFileContents, configFile); err != nil {
|
|
return fmt.Errorf("%s: %w", configFileAbsPath, err)
|
|
}
|
|
|
|
if configFile.Git.CommitMessageTemplate != "" && configFile.Git.CommitMessageTemplateFile != "" {
|
|
return fmt.Errorf(
|
|
"%s: cannot specify both git.commitMessageTemplate and git.commitMessageTemplateFile",
|
|
configFileAbsPath,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// decodeConfigMap decodes configMap into configFile.
|
|
func (c *Config) decodeConfigMap(configMap map[string]any, configFile *ConfigFile) error {
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
mapstructure.StringToSliceHookFunc(","),
|
|
chezmoi.StringSliceToEntryTypeSetHookFunc(),
|
|
chezmoi.StringToAbsPathHookFunc(),
|
|
StringOrBoolToAutoBoolHookFunc(),
|
|
StringToChoiceFlagHookFunc(),
|
|
),
|
|
Result: configFile,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decoder.Decode(configMap)
|
|
}
|
|
|
|
// defaultConfigFile returns the default config file according to the XDG Base
|
|
// Directory Specification.
|
|
func (c *Config) defaultConfigFile(fileSystem vfs.FS, bds *xdg.BaseDirectorySpecification) (chezmoi.AbsPath, error) {
|
|
// Search XDG Base Directory Specification config directories first.
|
|
CONFIG_DIR:
|
|
for _, configDir := range bds.ConfigDirs {
|
|
configDirAbsPath, err := chezmoi.NewAbsPathFromExtPath(configDir, c.homeDirAbsPath)
|
|
if err != nil {
|
|
return chezmoi.EmptyAbsPath, err
|
|
}
|
|
|
|
dirEntries, err := fileSystem.ReadDir(configDirAbsPath.JoinString("chezmoi").String())
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
continue CONFIG_DIR
|
|
case err != nil:
|
|
return chezmoi.EmptyAbsPath, err
|
|
}
|
|
|
|
dirEntryNames := chezmoiset.NewWithCapacity[string](len(dirEntries))
|
|
for _, dirEntry := range dirEntries {
|
|
dirEntryNames.Add(dirEntry.Name())
|
|
}
|
|
|
|
var names []string
|
|
for _, extension := range chezmoi.FormatExtensions {
|
|
name := "chezmoi." + extension
|
|
if dirEntryNames.Contains(name) {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
|
|
switch len(names) {
|
|
case 0:
|
|
// Do nothing.
|
|
case 1:
|
|
return configDirAbsPath.JoinString("chezmoi", names[0]), nil
|
|
default:
|
|
configFileAbsPathStrs := make([]string, 0, len(names))
|
|
for _, name := range names {
|
|
configFileAbsPathStr := configDirAbsPath.JoinString("chezmoi", name)
|
|
configFileAbsPathStrs = append(configFileAbsPathStrs, configFileAbsPathStr.String())
|
|
}
|
|
return chezmoi.EmptyAbsPath, fmt.Errorf("multiple config files: %s", english.OxfordWordSeries(configFileAbsPathStrs, "and"))
|
|
}
|
|
}
|
|
|
|
// Fallback to XDG Base Directory Specification default.
|
|
configHomeAbsPath, err := chezmoi.NewAbsPathFromExtPath(bds.ConfigHome, c.homeDirAbsPath)
|
|
if err != nil {
|
|
return chezmoi.EmptyAbsPath, err
|
|
}
|
|
return configHomeAbsPath.JoinString("chezmoi", "chezmoi.toml"), nil
|
|
}
|
|
|
|
// defaultPreApplyFunc is the default pre-apply function. If the target entry
|
|
// has changed since chezmoi last wrote it then it prompts the user for the
|
|
// action to take.
|
|
func (c *Config) defaultPreApplyFunc(
|
|
targetRelPath chezmoi.RelPath,
|
|
targetEntryState, lastWrittenEntryState, actualEntryState *chezmoi.EntryState,
|
|
) error {
|
|
c.logger.Info("defaultPreApplyFunc",
|
|
chezmoilog.Stringer("targetRelPath", targetRelPath),
|
|
slog.Any("targetEntryState", targetEntryState),
|
|
slog.Any("lastWrittenEntryState", lastWrittenEntryState),
|
|
slog.Any("actualEntryState", actualEntryState),
|
|
)
|
|
|
|
switch {
|
|
case c.force:
|
|
return nil
|
|
case targetEntryState.Equivalent(actualEntryState):
|
|
return nil
|
|
}
|
|
|
|
// Prepare decision for which kind of prompt we need (if any)
|
|
type promptMode int
|
|
const (
|
|
promptNone promptMode = iota
|
|
promptYesNoAll // yes/no/all/quit (just ask, don't indicate if there is a conflict)
|
|
promptConflict // overwrite/all-overwrite/skip/quit (conflict-specific prompt)
|
|
)
|
|
mode := promptNone
|
|
|
|
targetDirty := lastWrittenEntryState != nil && !lastWrittenEntryState.Equivalent(actualEntryState)
|
|
targetPreExisting := lastWrittenEntryState == nil && actualEntryState.Type != chezmoi.EntryStateTypeRemove
|
|
|
|
// Select prompt mode based on command line flag
|
|
switch {
|
|
case c.Interactive: // Prompt no matter what
|
|
mode = promptYesNoAll
|
|
case c.LessInteractive: // Prompt if target is dirty or pre-existing (i.e., only overwrite what chezmoi has written)
|
|
if targetDirty || targetPreExisting {
|
|
mode = promptConflict
|
|
}
|
|
default: // Prompt in *some* cases of a dirty target:
|
|
switch {
|
|
case targetEntryState.Overwrite():
|
|
mode = promptNone
|
|
case targetEntryState.Type == chezmoi.EntryStateTypeScript:
|
|
mode = promptNone
|
|
case lastWrittenEntryState == nil:
|
|
mode = promptNone
|
|
case lastWrittenEntryState.Equivalent(actualEntryState):
|
|
mode = promptNone
|
|
case targetDirty:
|
|
mode = promptConflict
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case mode == promptNone:
|
|
return nil
|
|
case mode == promptConflict && c.errorOnConflict:
|
|
return chezmoi.ExitCodeError(1)
|
|
}
|
|
|
|
// Now prompt based on choice made above
|
|
actualContents := actualEntryState.Contents()
|
|
targetContents := targetEntryState.Contents()
|
|
var choices []string
|
|
if actualContents != nil || targetContents != nil {
|
|
choices = append(choices, "diff")
|
|
}
|
|
var promptText string
|
|
if mode == promptYesNoAll {
|
|
choices = append(choices, choicesYesNoAllQuit...)
|
|
promptText = fmt.Sprintf("Apply %s", targetRelPath)
|
|
} else {
|
|
choices = append(choices, choicesOverwrite...)
|
|
if targetDirty {
|
|
promptText = fmt.Sprintf("%s has changed since chezmoi last wrote it", targetRelPath)
|
|
} else {
|
|
promptText = fmt.Sprintf("%s already exists", targetRelPath)
|
|
}
|
|
}
|
|
|
|
for {
|
|
switch choice, err := c.promptChoice(promptText, choices); {
|
|
case err != nil:
|
|
return err
|
|
case choice == "diff":
|
|
if err := c.diffFile(
|
|
targetRelPath,
|
|
c.DestDirAbsPath.Join(targetRelPath), actualContents, actualEntryState.Mode,
|
|
chezmoi.EmptyAbsPath, targetContents, targetEntryState.Mode,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
case choice == "yes":
|
|
return nil
|
|
case choice == "no":
|
|
return fs.SkipDir
|
|
case choice == "all":
|
|
// Delicate difference to all-overwrite (mainly for backwards compatibility): Disabling --interactive means
|
|
// we still prompt for dirty files, whereas all-overwrite adds --force to really prompt no more.
|
|
c.Interactive = false
|
|
return nil
|
|
case choice == "overwrite":
|
|
return nil
|
|
case choice == "all-overwrite":
|
|
c.force = true
|
|
return nil
|
|
case choice == "skip":
|
|
return fs.SkipDir
|
|
case choice == "quit":
|
|
return chezmoi.ExitCodeError(0)
|
|
default:
|
|
panic(choice + ": unexpected choice")
|
|
}
|
|
}
|
|
}
|
|
|
|
// defaultSourceDir returns the default source directory according to the XDG
|
|
// Base Directory Specification.
|
|
func (c *Config) defaultSourceDir(fileSystem vfs.Stater, bds *xdg.BaseDirectorySpecification) (chezmoi.AbsPath, error) {
|
|
// Check for XDG Base Directory Specification data directories first.
|
|
for _, dataDir := range bds.DataDirs {
|
|
dataDirAbsPath, err := chezmoi.NewAbsPathFromExtPath(dataDir, c.homeDirAbsPath)
|
|
if err != nil {
|
|
return chezmoi.EmptyAbsPath, err
|
|
}
|
|
sourceDirAbsPath := dataDirAbsPath.Join(chezmoiRelPath)
|
|
if _, err := fileSystem.Stat(sourceDirAbsPath.String()); err == nil {
|
|
return sourceDirAbsPath, nil
|
|
}
|
|
}
|
|
// Fallback to XDG Base Directory Specification default.
|
|
dataHomeAbsPath, err := chezmoi.NewAbsPathFromExtPath(bds.DataHome, c.homeDirAbsPath)
|
|
if err != nil {
|
|
return chezmoi.EmptyAbsPath, err
|
|
}
|
|
return dataHomeAbsPath.Join(chezmoiRelPath), nil
|
|
}
|
|
|
|
type onNotExist int
|
|
|
|
const (
|
|
onNotExistError onNotExist = iota
|
|
onNotExistIgnore
|
|
onNotExistAdd
|
|
)
|
|
|
|
type destAbsPathInfosOptions struct {
|
|
follow bool
|
|
onIgnoreFunc func(chezmoi.RelPath)
|
|
onNotExist onNotExist
|
|
recursive bool
|
|
}
|
|
|
|
// destAbsPathInfos returns the os/fs.FileInfos for each destination entry in
|
|
// args, recursing into subdirectories and following symlinks if configured in
|
|
// options.
|
|
func (c *Config) destAbsPathInfos(
|
|
sourceState *chezmoi.SourceState,
|
|
args []string,
|
|
options destAbsPathInfosOptions,
|
|
) (map[chezmoi.AbsPath]fs.FileInfo, error) {
|
|
destAbsPathInfos := make(map[chezmoi.AbsPath]fs.FileInfo)
|
|
for _, arg := range args {
|
|
arg = filepath.Clean(arg)
|
|
destAbsPath, err := chezmoi.NewAbsPathFromExtPath(arg, c.homeDirAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targetRelPath, err := c.targetRelPath(destAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sourceState.Ignore(targetRelPath) {
|
|
options.onIgnoreFunc(targetRelPath)
|
|
continue
|
|
}
|
|
if options.recursive {
|
|
walkFunc := func(destAbsPath chezmoi.AbsPath, fileInfo fs.FileInfo, err error) error {
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
switch options.onNotExist {
|
|
case onNotExistError:
|
|
return err
|
|
case onNotExistIgnore:
|
|
case onNotExistAdd:
|
|
if _, ok := destAbsPathInfos[destAbsPath]; !ok {
|
|
destAbsPathInfos[destAbsPath] = nil
|
|
}
|
|
}
|
|
return nil
|
|
case err != nil:
|
|
return err
|
|
}
|
|
|
|
targetRelPath, err := c.targetRelPath(destAbsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sourceState.Ignore(targetRelPath) {
|
|
options.onIgnoreFunc(targetRelPath)
|
|
if fileInfo.IsDir() {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if options.follow && fileInfo.Mode().Type() == fs.ModeSymlink {
|
|
fileInfo, err = c.destSystem.Stat(destAbsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return sourceState.AddDestAbsPathInfos(destAbsPathInfos, c.destSystem, destAbsPath, fileInfo)
|
|
}
|
|
if err := chezmoi.Walk(c.destSystem, destAbsPath, walkFunc); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
var fileInfo fs.FileInfo
|
|
if options.follow {
|
|
fileInfo, err = c.destSystem.Stat(destAbsPath)
|
|
} else {
|
|
fileInfo, err = c.destSystem.Lstat(destAbsPath)
|
|
}
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
switch options.onNotExist {
|
|
case onNotExistError:
|
|
return nil, err
|
|
case onNotExistIgnore:
|
|
case onNotExistAdd:
|
|
if _, ok := destAbsPathInfos[destAbsPath]; !ok {
|
|
destAbsPathInfos[destAbsPath] = nil
|
|
}
|
|
}
|
|
continue
|
|
case err != nil:
|
|
return nil, err
|
|
}
|
|
if err := sourceState.AddDestAbsPathInfos(destAbsPathInfos, c.destSystem, destAbsPath, fileInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
return destAbsPathInfos, nil
|
|
}
|
|
|
|
// diffFile outputs the diff between fromAbsPath, fromData and fromMode and
|
|
// toAbsPath, toData and toMode at relPath.
|
|
func (c *Config) diffFile(
|
|
relPath chezmoi.RelPath,
|
|
fromAbsPath chezmoi.AbsPath, fromData []byte, fromMode fs.FileMode,
|
|
toAbsPath chezmoi.AbsPath, toData []byte, toMode fs.FileMode,
|
|
) error {
|
|
if c.useBuiltinDiff || c.Diff.Command == "" {
|
|
return c.builtinDiffFile(relPath, fromData, fromMode, toData, toMode)
|
|
}
|
|
return c.externalDiffFile(
|
|
relPath,
|
|
fromAbsPath, fromData, fromMode,
|
|
toAbsPath, toData, toMode,
|
|
)
|
|
}
|
|
|
|
// editor returns the path to the user's editor and any extra arguments.
|
|
func (c *Config) editor(args []string) (string, []string, error) {
|
|
editCommand := c.Edit.Command
|
|
editArgs := c.Edit.Args
|
|
|
|
// If the user has set an edit command then use it.
|
|
if editCommand != "" {
|
|
return editCommand, append(editArgs, args...), nil
|
|
}
|
|
|
|
// Prefer $VISUAL over $EDITOR and fallback to the OS's default editor.
|
|
editCommand = cmp.Or(os.Getenv("VISUAL"), os.Getenv("EDITOR"), defaultEditor)
|
|
|
|
return parseCommand(editCommand, append(editArgs, args...))
|
|
}
|
|
|
|
// errorf writes an error to stderr.
|
|
func (c *Config) errorf(format string, args ...any) {
|
|
_, _ = fmt.Fprintf(c.stderr, "chezmoi: "+format, args...)
|
|
}
|
|
|
|
// execute creates a new root command and executes it with args.
|
|
func (c *Config) execute(args []string) error {
|
|
rootCmd, err := c.newRootCmd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer c.finalize()
|
|
|
|
rootCmd.SetArgs(args)
|
|
|
|
return rootCmd.Execute()
|
|
}
|
|
|
|
func (c *Config) externalDiffFile(
|
|
relPath chezmoi.RelPath,
|
|
fromAbsPath chezmoi.AbsPath, fromData []byte, fromMode fs.FileMode,
|
|
toAbsPath chezmoi.AbsPath, toData []byte, toMode fs.FileMode,
|
|
) error {
|
|
if fromAbsPath.IsEmpty() {
|
|
fromTempDir, err := c.tempDir("chezmoi-diff-from")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fromAbsPath = fromTempDir.Join(relPath)
|
|
if err := os.MkdirAll(fromAbsPath.Dir().String(), 0o700); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(fromAbsPath.String(), fromData, fromMode); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if toAbsPath.IsEmpty() {
|
|
toTempDir, err := c.tempDir("chezmoi-diff-to")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
toAbsPath = toTempDir.Join(relPath)
|
|
if err := os.MkdirAll(toAbsPath.Dir().String(), 0o700); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(toAbsPath.String(), toData, toMode); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
externalDiffSystem := c.newExternalDiffSystem(c.baseSystem)
|
|
return externalDiffSystem.RunDiffCommand(fromAbsPath, toAbsPath)
|
|
}
|
|
|
|
// filterInput reads from args (or the standard input if args is empty),
|
|
// transforms it with f, and writes the output.
|
|
func (c *Config) filterInput(args []string, f func([]byte) ([]byte, error)) error {
|
|
if len(args) == 0 {
|
|
input, err := io.ReadAll(c.stdin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
output, err := f(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.writeOutput(output, 0o666)
|
|
}
|
|
|
|
for _, arg := range args {
|
|
argAbsPath, err := chezmoi.NewAbsPathFromExtPath(arg, c.homeDirAbsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
input, err := c.baseSystem.ReadFile(argAbsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
output, err := f(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := c.writeOutput(output, 0o666); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// finalize cleans up.
|
|
func (c *Config) finalize() {
|
|
if c.persistentState != nil {
|
|
if err := c.persistentState.Close(); err != nil {
|
|
c.errorf("error: failed to close persistent state: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// Wait for any diff pager process to terminate.
|
|
if c.diffPagerCmd != nil {
|
|
if err := c.diffPagerCmdStdin.Close(); err != nil {
|
|
c.errorf("error: failed to close diff pager stdin: %v\n", err)
|
|
}
|
|
if c.diffPagerCmd.Process != nil {
|
|
if err := chezmoilog.LogCmdWait(c.logger, c.diffPagerCmd); err != nil {
|
|
c.errorf("error: failed to wait for diff pager to close: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if c.restoreWindowsConsole != nil {
|
|
if err := c.restoreWindowsConsole(); err != nil {
|
|
c.errorf("error: failed to restore console: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// Lock Bitwarden CLI.
|
|
if err := c.bitwardenLock(); err != nil {
|
|
c.errorf("error: failed to lock bitwarden-cli: %v\n", err)
|
|
}
|
|
|
|
// Close any connection to keepassxc-cli.
|
|
if err := c.keepassxcClose(); err != nil {
|
|
c.errorf("error: failed to close connection to keepassxc-cli: %v\n", err)
|
|
}
|
|
}
|
|
|
|
type configTemplate struct {
|
|
sourceAbsPath chezmoi.AbsPath
|
|
format chezmoi.Format
|
|
targetRelPath chezmoi.RelPath
|
|
contents []byte
|
|
}
|
|
|
|
// findConfigTemplate searches for a config template, returning the path,
|
|
// format, and contents. It returns an error if multiple config file templates
|
|
// are found.
|
|
func (c *Config) findConfigTemplate() (*configTemplate, error) {
|
|
sourceDirAbsPath, err := c.getSourceDirAbsPath(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dirEntries, err := c.baseSystem.ReadDir(sourceDirAbsPath)
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
return nil, nil
|
|
case err != nil:
|
|
return nil, err
|
|
}
|
|
|
|
dirEntryNames := chezmoiset.NewWithCapacity[chezmoi.RelPath](len(dirEntries))
|
|
for _, dirEntry := range dirEntries {
|
|
dirEntryNames.Add(chezmoi.NewRelPath(dirEntry.Name()))
|
|
}
|
|
|
|
var configTemplates []*configTemplate //nolint:prealloc
|
|
for _, extension := range chezmoi.FormatExtensions {
|
|
relPath := chezmoi.NewRelPath(chezmoi.Prefix + "." + extension + chezmoi.TemplateSuffix)
|
|
if !dirEntryNames.Contains(relPath) {
|
|
continue
|
|
}
|
|
absPath := sourceDirAbsPath.Join(relPath)
|
|
contents, err := c.baseSystem.ReadFile(absPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configTemplate := &configTemplate{
|
|
targetRelPath: chezmoi.NewRelPath("chezmoi." + extension),
|
|
format: chezmoi.FormatsByExtension[extension],
|
|
sourceAbsPath: absPath,
|
|
contents: contents,
|
|
}
|
|
configTemplates = append(configTemplates, configTemplate)
|
|
}
|
|
|
|
switch len(configTemplates) {
|
|
case 0:
|
|
return nil, nil
|
|
case 1:
|
|
return configTemplates[0], nil
|
|
default:
|
|
sourceAbsPathStrs := make([]string, len(configTemplates))
|
|
for i, configTemplate := range configTemplates {
|
|
sourceAbsPathStr := configTemplate.sourceAbsPath.String()
|
|
sourceAbsPathStrs[i] = sourceAbsPathStr
|
|
}
|
|
return nil, fmt.Errorf("multiple config file templates: %s ", english.OxfordWordSeries(sourceAbsPathStrs, "and"))
|
|
}
|
|
}
|
|
|
|
func (c *Config) getBetterleaksDetector() (*detect.Detector, error) {
|
|
if c.betterleaksDetector == nil && c.betterleaksDetectorErr == nil {
|
|
c.betterleaksDetector, c.betterleaksDetectorErr = detect.NewDetectorDefaultConfig()
|
|
}
|
|
return c.betterleaksDetector, c.betterleaksDetectorErr
|
|
}
|
|
|
|
func (c *Config) getConfigFileAbsPath() (chezmoi.AbsPath, error) {
|
|
if c.customConfigFileAbsPath.IsEmpty() {
|
|
return c.defaultConfigFileAbsPath, c.defaultConfigFileAbsPathErr
|
|
}
|
|
return c.customConfigFileAbsPath, nil
|
|
}
|
|
|
|
// getDiffPager returns the pager for diff output.
|
|
func (c *Config) getDiffPager() (command string, args []string) {
|
|
switch {
|
|
case c.noPager:
|
|
return "", nil
|
|
case c.Diff.Pager != defaultSentinel:
|
|
return c.Diff.Pager, c.Diff.PagerArgs
|
|
default:
|
|
return c.Pager, c.PagerArgs
|
|
}
|
|
}
|
|
|
|
// getDiffPagerCmd returns a command to run the diff pager, or nil if there is
|
|
// no diff pager configured.
|
|
func (c *Config) getDiffPagerCmd() (*exec.Cmd, error) {
|
|
pager, pagerArgs := c.getDiffPager()
|
|
if pager == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
// If we're not on Windows and the pager command contains any spaces, assume
|
|
// that it is a full shell command to be executed via the user's shell.
|
|
// Otherwise, execute it directly.
|
|
var pagerCmd *exec.Cmd
|
|
if runtime.GOOS != "windows" && whitespaceRx.MatchString(pager) {
|
|
shellCommand, _ := shell.CurrentUserShell()
|
|
shellCommand, shellArgs, err := parseCommand(shellCommand, []string{"-c", pager})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pagerCmd = exec.Command(shellCommand, slices.Concat(shellArgs, pagerArgs)...)
|
|
} else {
|
|
pagerCmd = exec.Command(pager, pagerArgs...)
|
|
}
|
|
pagerCmd.Stdout = c.stdout
|
|
pagerCmd.Stderr = c.stderr
|
|
// If the LESS or LV environment variables are not set, set them to sensible
|
|
// defaults the same way the git does. See
|
|
// https://git-scm.com/docs/git-config#Documentation/git-config.txt-corepager.
|
|
if _, ok := os.LookupEnv("LESS"); !ok {
|
|
pagerCmd.Env = append(pagerCmd.Environ(), "LESS=FRX")
|
|
}
|
|
if _, ok := os.LookupEnv("LV"); !ok {
|
|
pagerCmd.Env = append(pagerCmd.Environ(), "LV=-c")
|
|
}
|
|
return pagerCmd, nil
|
|
}
|
|
|
|
// A modifyHTTPRequestFunc is a function that modifies a [net/http.Request]
|
|
// before it is sent.
|
|
type modifyHTTPRequestFunc func(*http.Request) (*http.Request, error)
|
|
|
|
// A modifyHTTPRequestRoundTripper is a [net/http.Transport] that modifies the
|
|
// request before it is sent.
|
|
type modifyHTTPRequestRoundTripper struct {
|
|
modifyHTTPRequestFunc modifyHTTPRequestFunc
|
|
httpRoundTripper http.RoundTripper
|
|
}
|
|
|
|
func newModifyHTTPRequestRoundTripper(f modifyHTTPRequestFunc, t http.RoundTripper) modifyHTTPRequestRoundTripper {
|
|
return modifyHTTPRequestRoundTripper{
|
|
modifyHTTPRequestFunc: f,
|
|
httpRoundTripper: t,
|
|
}
|
|
}
|
|
|
|
// RoundTrip implements [net/http.Transport.RoundTrip].
|
|
func (m modifyHTTPRequestRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
modifiedRequest, err := m.modifyHTTPRequestFunc(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.httpRoundTripper.RoundTrip(modifiedRequest)
|
|
}
|
|
|
|
func (c *Config) getHTTPClient() (*http.Client, error) {
|
|
if c.httpClient != nil {
|
|
return c.httpClient, nil
|
|
}
|
|
|
|
cacheBasePath, err := c.baseSystem.RawPath(c.CacheDirAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.httpClient = &http.Client{
|
|
Transport: httpcache.NewTransport(
|
|
httpCacheScheme+"://"+cacheBasePath.String(),
|
|
httpcache.WithLogger(c.logger),
|
|
httpcache.WithUpstream(newModifyHTTPRequestRoundTripper(
|
|
func(req *http.Request) (*http.Request, error) {
|
|
req = req.Clone(req.Context())
|
|
req.Header.Add("User-Agent", "chezmoi.io/"+c.version.String())
|
|
return req, nil
|
|
},
|
|
http.DefaultTransport,
|
|
)),
|
|
),
|
|
}
|
|
|
|
return c.httpClient, nil
|
|
}
|
|
|
|
type getSourceDirAbsPathOptions struct {
|
|
refresh bool
|
|
}
|
|
|
|
// getSourceDirAbsPath returns the source directory, using .chezmoiroot if it
|
|
// exists.
|
|
func (c *Config) getSourceDirAbsPath(options *getSourceDirAbsPathOptions) (chezmoi.AbsPath, error) {
|
|
if options == nil || !options.refresh {
|
|
if !c.sourceDirAbsPath.IsEmpty() || c.sourceDirAbsPathErr != nil {
|
|
return c.sourceDirAbsPath, c.sourceDirAbsPathErr
|
|
}
|
|
}
|
|
|
|
switch data, err := c.sourceSystem.ReadFile(c.SourceDirAbsPath.JoinString(chezmoi.RootName)); {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
c.sourceDirAbsPath = c.SourceDirAbsPath
|
|
case err != nil:
|
|
c.sourceDirAbsPathErr = err
|
|
default:
|
|
rootRelPath, err := chezmoi.NewUntrustedRelPath(string(bytes.TrimSpace(data)))
|
|
if err != nil {
|
|
return chezmoi.EmptyAbsPath, fmt.Errorf("%s: %w", chezmoi.RootName, err)
|
|
}
|
|
c.sourceDirAbsPath = c.SourceDirAbsPath.Join(rootRelPath)
|
|
}
|
|
|
|
return c.sourceDirAbsPath, c.sourceDirAbsPathErr
|
|
}
|
|
|
|
func (c *Config) getSourceState(ctx context.Context, cmd *cobra.Command) (*chezmoi.SourceState, error) {
|
|
if c.sourceState != nil || c.sourceStateErr != nil {
|
|
return c.sourceState, c.sourceStateErr
|
|
}
|
|
c.sourceState, c.sourceStateErr = c.newSourceState(ctx, cmd)
|
|
return c.sourceState, c.sourceStateErr
|
|
}
|
|
|
|
// getTemplateData returns the default template data.
|
|
func (c *Config) getTemplateData(cmd *cobra.Command) *templateData {
|
|
if c.templateData == nil {
|
|
c.templateData = c.newTemplateData(cmd)
|
|
}
|
|
return c.templateData
|
|
}
|
|
|
|
// getTemplateDataMap returns the template data as a map.
|
|
func (c *Config) getTemplateDataMap(cmd *cobra.Command) map[string]any {
|
|
templateData := c.getTemplateData(cmd)
|
|
|
|
return map[string]any{
|
|
"chezmoi": map[string]any{
|
|
"arch": templateData.arch,
|
|
"args": templateData.args,
|
|
"cacheDir": templateData.cacheDir,
|
|
"command": templateData.command,
|
|
"commandDir": templateData.commandDir,
|
|
"config": templateData.config,
|
|
"configFile": templateData.configFile,
|
|
"destDir": templateData.destDir,
|
|
"executable": templateData.executable,
|
|
"flags": map[string]any{
|
|
"dryRun": templateData.flags.dryRun,
|
|
"force": templateData.flags.force,
|
|
"keepGoing": templateData.flags.keepGoing,
|
|
"noPager": templateData.flags.noPager,
|
|
"noTTY": templateData.flags.noTTY,
|
|
},
|
|
"fqdnHostname": templateData.fqdnHostname,
|
|
"gid": templateData.gid,
|
|
"group": templateData.group,
|
|
"homeDir": templateData.homeDir,
|
|
"hostname": templateData.hostname,
|
|
"kernel": templateData.kernel,
|
|
"os": templateData.os,
|
|
"osRelease": templateData.osRelease,
|
|
"pathListSeparator": templateData.pathListSeparator,
|
|
"pathSeparator": templateData.pathSeparator,
|
|
"rawHomeDir": templateData.rawHomeDir,
|
|
"sourceDir": templateData.sourceDir,
|
|
"uid": templateData.uid,
|
|
"username": templateData.username,
|
|
"version": templateData.version,
|
|
"windowsVersion": templateData.windowsVersion,
|
|
"workingTree": templateData.workingTree,
|
|
},
|
|
}
|
|
}
|
|
|
|
// gitAdd adds all changes to the git index and returns the new git status.
|
|
func (c *Config) gitAdd() (*chezmoigit.Status, error) {
|
|
if err := c.run(c.SourceDirAbsPath, c.Git.Command, []string{"add", "."}); err != nil {
|
|
return nil, err
|
|
}
|
|
output, err := c.cmdOutput(c.SourceDirAbsPath, c.Git.Command, []string{"status", "--porcelain=v2"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return chezmoigit.ParseStatusPorcelainV2(output)
|
|
}
|
|
|
|
// gitCommit commits all changes in the git index, including generating a
|
|
// commit message from status.
|
|
func (c *Config) gitCommit(cmd *cobra.Command, status *chezmoigit.Status) error {
|
|
if status.IsEmpty() {
|
|
return nil
|
|
}
|
|
if err := c.runHookPre("git-auto-commit"); err != nil {
|
|
return err
|
|
}
|
|
commitMessage, err := c.gitCommitMessage(cmd, status)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := c.run(c.SourceDirAbsPath, c.Git.Command, []string{"commit", "--message", string(commitMessage)}); err != nil {
|
|
return err
|
|
}
|
|
return c.runHookPost("git-auto-commit")
|
|
}
|
|
|
|
// gitCommitMessage returns the git commit message for the given status.
|
|
func (c *Config) gitCommitMessage(cmd *cobra.Command, status *chezmoigit.Status) ([]byte, error) {
|
|
templateFuncs := maps.Clone(c.templateFuncs)
|
|
maps.Copy(templateFuncs, map[string]any{
|
|
"promptBool": c.promptBoolInteractiveTemplateFunc,
|
|
"promptChoice": c.promptChoiceInteractiveTemplateFunc,
|
|
"promptInt": c.promptIntInteractiveTemplateFunc,
|
|
"promptMultichoice": c.promptMultichoiceInteractiveTemplateFunc,
|
|
"promptString": c.promptStringInteractiveTemplateFunc,
|
|
"targetRelPath": func(source string) string {
|
|
return mustValue(chezmoi.NewSourceRelPath(source).TargetRelPath(c.encryption.EncryptedSuffix())).String()
|
|
},
|
|
})
|
|
var name string
|
|
var commitMessageTemplateData []byte
|
|
switch {
|
|
case c.Git.CommitMessageTemplate != "":
|
|
name = "git.commitMessageTemplate"
|
|
commitMessageTemplateData = []byte(c.Git.CommitMessageTemplate)
|
|
case c.Git.CommitMessageTemplateFile != "":
|
|
if c.sourceDirAbsPathErr != nil {
|
|
return nil, c.sourceDirAbsPathErr
|
|
}
|
|
commitMessageTemplateFileAbsPath := c.sourceDirAbsPath.JoinString(c.Git.CommitMessageTemplateFile)
|
|
name = c.sourceDirAbsPath.String()
|
|
var err error
|
|
commitMessageTemplateData, err = c.baseSystem.ReadFile(commitMessageTemplateFileAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
name = "COMMIT_MESSAGE"
|
|
commitMessageTemplateData = []byte(templates.CommitMessageTmpl)
|
|
}
|
|
commitMessageTmpl, err := chezmoi.ParseTemplate(name, commitMessageTemplateData, chezmoi.TemplateOptions{
|
|
Funcs: templateFuncs,
|
|
Options: slices.Clone(c.Template.Options),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceState, err := c.getSourceState(cmd.Context(), cmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
templateDataMap := sourceState.TemplateData()
|
|
templateDataMap["chezmoi"].(map[string]any)["status"] = status //nolint:forcetypeassert
|
|
return commitMessageTmpl.Execute(templateDataMap)
|
|
}
|
|
|
|
// gitPush pushes all changes to the remote if status is not empty.
|
|
func (c *Config) gitPush(status *chezmoigit.Status) error {
|
|
if status.IsEmpty() {
|
|
return nil
|
|
}
|
|
if err := c.runHookPre("git-auto-push"); err != nil {
|
|
return err
|
|
}
|
|
if err := c.run(c.SourceDirAbsPath, c.Git.Command, []string{"push"}); err != nil {
|
|
return err
|
|
}
|
|
return c.runHookPost("git-auto-push")
|
|
}
|
|
|
|
// makeRunEWithSourceState returns a function for
|
|
// github.com/spf13/cobra.Command.RunE that includes reading the source state.
|
|
func (c *Config) makeRunEWithSourceState(
|
|
runE func(*cobra.Command, []string, *chezmoi.SourceState) error,
|
|
) func(*cobra.Command, []string) error {
|
|
return func(cmd *cobra.Command, args []string) error {
|
|
sourceState, err := c.getSourceState(cmd.Context(), cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return runE(cmd, args, sourceState)
|
|
}
|
|
}
|
|
|
|
// marshal formats data in dataFormat and writes it to the standard output.
|
|
func (c *Config) marshal(dataFormat string, data any) error {
|
|
var format chezmoi.Format
|
|
switch dataFormat {
|
|
case formatJSON:
|
|
format = chezmoi.FormatJSON
|
|
case formatYAML:
|
|
format = chezmoi.FormatYAML
|
|
default:
|
|
return fmt.Errorf("%s: invalid format", dataFormat)
|
|
}
|
|
marshaledData, err := format.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.writeOutput(marshaledData, 0o666)
|
|
}
|
|
|
|
// newBuiltinDifSystem returns a new builtin diff system.
|
|
func (c *Config) newBuiltinDiffSystem(s chezmoi.System, w io.Writer, dirAbsPath chezmoi.AbsPath) *chezmoi.GitDiffSystem {
|
|
options := &chezmoi.GitDiffSystemOptions{
|
|
Color: c.Color.Value(c.colorAutoFunc),
|
|
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.NewGitDiffSystem(s, w, dirAbsPath, options)
|
|
}
|
|
|
|
// newDiffSystem returns a system that logs all changes to s to w using
|
|
// diff.command if set or the builtin git diff otherwise.
|
|
func (c *Config) newDiffSystem(s chezmoi.System, w io.Writer, dirAbsPath chezmoi.AbsPath) chezmoi.System {
|
|
if c.useBuiltinDiff || c.Diff.Command == "" {
|
|
return c.newBuiltinDiffSystem(s, w, dirAbsPath)
|
|
}
|
|
return c.newExternalDiffSystem(s)
|
|
}
|
|
|
|
// newExternalDiffSystem returns a new external diff system.
|
|
func (c *Config) newExternalDiffSystem(s chezmoi.System) *chezmoi.ExternalDiffSystem {
|
|
options := &chezmoi.ExternalDiffSystemOptions{
|
|
Filter: chezmoi.NewEntryTypeFilter(c.Diff.include.Bits(), c.Diff.Exclude.Bits()),
|
|
Reverse: c.Diff.Reverse,
|
|
ScriptContents: c.Diff.ScriptContents,
|
|
TemplateOptions: chezmoi.TemplateOptions{
|
|
Funcs: c.templateFuncs,
|
|
Options: c.Template.Options,
|
|
},
|
|
TextConvFunc: c.TextConv.convert,
|
|
}
|
|
return chezmoi.NewExternalDiffSystem(s, c.Diff.Command, c.Diff.Args, c.DestDirAbsPath, c.getDiffPagerCmd, options)
|
|
}
|
|
|
|
// newRootCmd returns a new root github.com/spf13/cobra.Command.
|
|
func (c *Config) newRootCmd() (*cobra.Command, error) {
|
|
rootCmd := &cobra.Command{
|
|
Use: "chezmoi",
|
|
Short: "Manage your dotfiles across multiple diverse machines, securely",
|
|
Version: c.versionStr,
|
|
PersistentPreRunE: c.persistentPreRunRootE,
|
|
PersistentPostRunE: c.persistentPostRunRootE,
|
|
SilenceErrors: true,
|
|
SilenceUsage: true,
|
|
}
|
|
rootCmd.AddGroup(groups...)
|
|
|
|
persistentFlags := rootCmd.PersistentFlags()
|
|
|
|
persistentFlags.StringVar(&c.ageRecipient, "age-recipient", c.ageRecipient, "Override age recipient")
|
|
persistentFlags.StringVar(&c.ageRecipientFile, "age-recipient-file", c.ageRecipient, "Override age recipient")
|
|
persistentFlags.Var(&c.CacheDirAbsPath, "cache", "Set cache directory")
|
|
persistentFlags.Var(&c.Color, "color", "Colorize output")
|
|
persistentFlags.VarP(&c.DestDirAbsPath, "destination", "D", "Set destination directory")
|
|
persistentFlags.BoolVar(&c.Interactive, "interactive", c.Interactive, "Prompt for all changes")
|
|
persistentFlags.BoolVar(
|
|
&c.LessInteractive,
|
|
"less-interactive",
|
|
c.LessInteractive,
|
|
"Prompt for changed or pre-existing targets",
|
|
)
|
|
persistentFlags.Var(&c.Mode, "mode", "Mode")
|
|
persistentFlags.Var(&c.PersistentStateAbsPath, "persistent-state", "Set persistent state file")
|
|
persistentFlags.Var(&c.Progress, "progress", "Display progress bars")
|
|
persistentFlags.BoolVar(&c.Safe, "safe", c.Safe, "Safely replace files and symlinks")
|
|
persistentFlags.VarP(&c.SourceDirAbsPath, "source", "S", "Set source directory")
|
|
persistentFlags.Var(&c.UseBuiltinAge, "use-builtin-age", "Use builtin age")
|
|
persistentFlags.Var(&c.UseBuiltinGit, "use-builtin-git", "Use builtin git")
|
|
persistentFlags.BoolVarP(&c.Verbose, "verbose", "v", c.Verbose, "Make output more verbose")
|
|
persistentFlags.VarP(&c.WorkingTreeAbsPath, "working-tree", "W", "Set working tree directory")
|
|
|
|
persistentFlags.VarP(&c.customConfigFileAbsPath, "config", "c", "Set config file")
|
|
persistentFlags.Var(c.configFormat, "config-format", "Set config file format")
|
|
persistentFlags.BoolVar(&c.debug, "debug", c.debug, "Include debug information in output")
|
|
persistentFlags.BoolVarP(&c.dryRun, "dry-run", "n", c.dryRun, "Do not make any modifications to the destination directory")
|
|
persistentFlags.BoolVar(&c.force, "force", c.force, "Make all changes without prompting")
|
|
persistentFlags.BoolVarP(&c.keepGoing, "keep-going", "k", c.keepGoing, "Keep going as far as possible after an error")
|
|
persistentFlags.BoolVar(&c.noPager, "no-pager", c.noPager, "Do not use the pager")
|
|
persistentFlags.BoolVar(&c.noTTY, "no-tty", c.noTTY, "Do not attempt to get a TTY for prompts")
|
|
persistentFlags.VarP(&c.outputAbsPath, "output", "o", "Write output to path instead of stdout")
|
|
persistentFlags.StringVar(&c.overrideData, "override-data", c.overrideData, "Override data")
|
|
persistentFlags.Var(&c.overrideDataFileAbsPath, "override-data-file", "Override data with file")
|
|
persistentFlags.VarP(&c.refreshExternals, "refresh-externals", "R", "Refresh external cache")
|
|
persistentFlags.Lookup("refresh-externals").NoOptDefVal = chezmoi.RefreshExternalsAlways.String()
|
|
persistentFlags.BoolVar(&c.skipSecrets, "skip-secrets", c.skipSecrets, "Skip all templates containing secrets")
|
|
persistentFlags.BoolVar(&c.sourcePath, "source-path", c.sourcePath, "Specify targets by source path")
|
|
persistentFlags.BoolVar(&c.errorOnConflict, "error-on-conflict", c.errorOnConflict, "Error on conflict")
|
|
persistentFlags.BoolVarP(&c.useBuiltinDiff, "use-builtin-diff", "", c.useBuiltinDiff, "Use builtin diff")
|
|
|
|
if err := chezmoierrors.Combine(
|
|
rootCmd.MarkPersistentFlagFilename("config"),
|
|
rootCmd.MarkPersistentFlagDirname("destination"),
|
|
rootCmd.MarkPersistentFlagFilename("output"),
|
|
persistentFlags.MarkHidden("safe"),
|
|
rootCmd.MarkPersistentFlagDirname("source"),
|
|
rootCmd.RegisterFlagCompletionFunc("color", autoBoolFlagCompletionFunc),
|
|
rootCmd.RegisterFlagCompletionFunc("config-format", c.configFormat.FlagCompletionFunc()),
|
|
rootCmd.RegisterFlagCompletionFunc("mode", chezmoi.ModeFlagCompletionFunc),
|
|
rootCmd.RegisterFlagCompletionFunc("progress", autoBoolFlagCompletionFunc),
|
|
rootCmd.RegisterFlagCompletionFunc("refresh-externals", chezmoi.RefreshExternalsFlagCompletionFunc),
|
|
rootCmd.RegisterFlagCompletionFunc("use-builtin-age", autoBoolFlagCompletionFunc),
|
|
rootCmd.RegisterFlagCompletionFunc("use-builtin-git", autoBoolFlagCompletionFunc),
|
|
rootCmd.MarkPersistentFlagDirname("working-tree"),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rootCmd.SetHelpCommand(c.newHelpCmd())
|
|
for _, cmd := range []*cobra.Command{
|
|
c.newAddCmd(),
|
|
c.newAgeCmd(),
|
|
c.newAgeKeygenCmd(),
|
|
c.newApplyCmd(),
|
|
c.newArchiveCmd(),
|
|
c.newCatCmd(),
|
|
c.newCatConfigCmd(),
|
|
c.newCDCmd(),
|
|
c.newChattrCmd(),
|
|
c.newCompletionCmd(),
|
|
c.newDataCmd(),
|
|
c.newDecryptCommand(),
|
|
c.newDestroyCmd(),
|
|
c.newDiffCmd(),
|
|
c.newDockerCmd(),
|
|
c.newDoctorCmd(),
|
|
c.newDumpCmd(),
|
|
c.newDumpConfigCmd(),
|
|
c.newEditCmd(),
|
|
c.newEditConfigCmd(),
|
|
c.newEditConfigTemplateCmd(),
|
|
c.newEditEncryptedCmd(),
|
|
c.newEncryptCmd(),
|
|
c.newExecuteTemplateCmd(),
|
|
c.newForgetCmd(),
|
|
c.newGenerateCmd(),
|
|
c.newGitCmd(),
|
|
c.newIgnoredCmd(),
|
|
c.newImportCmd(),
|
|
c.newInitCmd(),
|
|
c.newInternalTestCmd(),
|
|
c.newLicenseCmd(),
|
|
c.newMackupCmd(),
|
|
c.newManagedCmd(),
|
|
c.newMergeCmd(),
|
|
c.newMergeAllCmd(),
|
|
c.newPurgeCmd(),
|
|
c.newPushCmd(),
|
|
c.newReAddCmd(),
|
|
c.newRemoveCmd(),
|
|
c.newSSHCmd(),
|
|
c.newSecretCmd(),
|
|
c.newSourcePathCmd(),
|
|
c.newStateCmd(),
|
|
c.newStatusCmd(),
|
|
c.newTargetPathCmd(),
|
|
c.newUnmanagedCmd(),
|
|
c.newUpdateCmd(),
|
|
c.newUpgradeCmd(),
|
|
c.newVerifyCmd(),
|
|
} {
|
|
if cmd != nil {
|
|
ensureHasGroupID(cmd)
|
|
ensureAllFlagsDocumented(cmd, persistentFlags)
|
|
registerCommonFlagCompletionFuncs(cmd)
|
|
rootCmd.AddCommand(cmd)
|
|
}
|
|
}
|
|
|
|
for _, cmd := range rootCmd.Commands() {
|
|
annotations := getAnnotations(cmd)
|
|
if len(annotations) == 0 {
|
|
panic(cmd.Name() + ": no annotations")
|
|
}
|
|
if annotations.persistentStateMode() == "" {
|
|
panic(cmd.Name() + ": persistent state mode not set")
|
|
}
|
|
}
|
|
|
|
return rootCmd, nil
|
|
}
|
|
|
|
// newSourceState returns a new SourceState with options.
|
|
func (c *Config) newSourceState(
|
|
ctx context.Context,
|
|
cmd *cobra.Command,
|
|
options ...chezmoi.SourceStateOption,
|
|
) (*chezmoi.SourceState, error) {
|
|
if err := c.checkVersion(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
httpClient, err := c.getHTTPClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sourceStateLogger := c.logger.With(slog.String(logComponentKey, logComponentValueSourceState))
|
|
|
|
c.SourceDirAbsPath, err = c.getSourceDirAbsPath(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := c.runHookPre(readSourceStateHookName); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
priorityTemplateData := c.Data
|
|
if !c.overrideDataFileAbsPath.IsEmpty() {
|
|
var overrideData map[string]any
|
|
data, err := c.baseSystem.ReadFile(c.overrideDataFileAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := chezmoi.UnmarshalFileData(c.overrideDataFileAbsPath, data, &overrideData); err != nil {
|
|
return nil, err
|
|
}
|
|
chezmoi.RecursiveMerge(priorityTemplateData, overrideData)
|
|
}
|
|
if c.overrideData != "" {
|
|
var overrideData map[string]any
|
|
if err := json.Unmarshal([]byte(c.overrideData), &overrideData); err != nil {
|
|
return nil, err
|
|
}
|
|
chezmoi.RecursiveMerge(priorityTemplateData, overrideData)
|
|
}
|
|
|
|
sourceState := chezmoi.NewSourceState(append([]chezmoi.SourceStateOption{
|
|
chezmoi.WithBaseSystem(c.baseSystem),
|
|
chezmoi.WithCacheDir(c.CacheDirAbsPath),
|
|
chezmoi.WithDefaultTemplateDataFunc(func() map[string]any {
|
|
return c.getTemplateDataMap(cmd)
|
|
}),
|
|
chezmoi.WithDestDir(c.DestDirAbsPath),
|
|
chezmoi.WithEncryption(c.encryption),
|
|
chezmoi.WithHTTPClient(httpClient),
|
|
chezmoi.WithInterpreters(c.Interpreters),
|
|
chezmoi.WithLogger(sourceStateLogger),
|
|
chezmoi.WithMode(c.Mode),
|
|
chezmoi.WithPriorityTemplateData(priorityTemplateData),
|
|
chezmoi.WithScriptTempDir(c.ScriptTempDir),
|
|
chezmoi.WithSourceDir(c.SourceDirAbsPath),
|
|
chezmoi.WithSystem(c.sourceSystem),
|
|
chezmoi.WithTemplateFuncs(c.templateFuncs),
|
|
chezmoi.WithTemplateOptions(c.Template.Options),
|
|
chezmoi.WithUmask(c.Umask),
|
|
chezmoi.WithVersion(c.version),
|
|
chezmoi.WithWarnFunc(c.errorf),
|
|
}, options...)...)
|
|
|
|
if err := sourceState.Read(ctx, &chezmoi.ReadOptions{
|
|
RefreshExternals: c.refreshExternals,
|
|
ReadHTTPResponse: c.readHTTPResponse,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := c.runHookPost(readSourceStateHookName); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return sourceState, nil
|
|
}
|
|
|
|
func (c *Config) newTemplateData(cmd *cobra.Command) *templateData {
|
|
// Determine the user's username and group, if possible.
|
|
//
|
|
// os/user.Current and os/user.LookupGroupId in Go's standard library are
|
|
// generally unreliable, so work around errors if possible, or ignore them.
|
|
//
|
|
// On Android, user.Current always fails. Instead, use $LOGNAME (as this is
|
|
// set by Termux), or $USER if $LOGNAME is not set.
|
|
//
|
|
// If CGO is disabled, then the Go standard library falls back to parsing
|
|
// /etc/passwd and /etc/group, which will return incorrect results without
|
|
// error if the system uses an alternative password database such as NIS or
|
|
// LDAP.
|
|
//
|
|
// If CGO is enabled then os/user.Current and os/user.LookupGroupId will use
|
|
// the underlying libc functions, namely getpwuid_r and getgrnam_r. If
|
|
// linked with glibc this will return the correct result. If linked with
|
|
// musl then they will use musl's implementation which, like Go's non-CGO
|
|
// implementation, also only parses /etc/passwd and /etc/group and so also
|
|
// returns incorrect results without error if NIS or LDAP are being used.
|
|
//
|
|
// On Windows, the user's group ID returned by os/user.Current() is an SID
|
|
// and no further useful lookup is possible with Go's standard library.
|
|
//
|
|
// If os/user.Current fails, then fallback to $USER.
|
|
//
|
|
// Since neither the username nor the group are likely widely used in
|
|
// templates, leave these variables unset if their values cannot be
|
|
// determined. Unset variables will trigger template errors if used,
|
|
// alerting the user to the problem and allowing them to find alternative
|
|
// solutions.
|
|
var gid, group, uid, username string
|
|
if runtime.GOOS == "android" {
|
|
username = cmp.Or(os.Getenv("LOGNAME"), os.Getenv("USER"))
|
|
} else if currentUser, err := user.Current(); err == nil {
|
|
gid = currentUser.Gid
|
|
uid = currentUser.Uid
|
|
username = currentUser.Username
|
|
if runtime.GOOS != "windows" {
|
|
if rawGroup, err := user.LookupGroupId(currentUser.Gid); err == nil {
|
|
group = rawGroup.Name
|
|
} else {
|
|
c.logger.Info("user.LookupGroupId", slog.Any("err", err), slog.String("gid", currentUser.Gid))
|
|
}
|
|
}
|
|
} else {
|
|
c.logger.Error("user.Current", slog.Any("err", err))
|
|
var ok bool
|
|
username, ok = os.LookupEnv("USER")
|
|
if !ok {
|
|
c.logger.Info("os.LookupEnv", slog.String("key", "USER"), slog.Bool("ok", ok))
|
|
}
|
|
}
|
|
|
|
fqdnHostname, err := chezmoi.FQDNHostname(c.fileSystem)
|
|
if err != nil {
|
|
c.logger.Info("chezmoi.FQDNHostname", slog.Any("err", err))
|
|
}
|
|
hostname, _, _ := strings.Cut(fqdnHostname, ".")
|
|
|
|
kernel, err := chezmoi.Kernel(c.fileSystem)
|
|
if err != nil {
|
|
c.logger.Info("chezmoi.Kernel", slog.Any("err", err))
|
|
}
|
|
|
|
var osRelease map[string]any
|
|
switch runtime.GOOS {
|
|
case "openbsd", "windows":
|
|
// Don't populate osRelease on OSes where /etc/os-release does not
|
|
// exist.
|
|
default:
|
|
if rawOSRelease, err := chezmoi.OSRelease(c.fileSystem); err == nil {
|
|
osRelease = upperSnakeCaseToCamelCaseMap(rawOSRelease)
|
|
} else {
|
|
c.logger.Info("chezmoi.OSRelease", slog.Any("err", err))
|
|
}
|
|
}
|
|
|
|
rawHomeDir, _ := os.UserHomeDir()
|
|
configFileAbsPath, _ := c.getConfigFileAbsPath()
|
|
executable, _ := os.Executable()
|
|
windowsVersion, _ := windowsVersion()
|
|
sourceDirAbsPath, _ := c.getSourceDirAbsPath(nil)
|
|
|
|
return &templateData{
|
|
arch: runtime.GOARCH,
|
|
args: os.Args,
|
|
cacheDir: c.CacheDirAbsPath.String(),
|
|
command: cmd.Name(),
|
|
commandDir: c.commandDirAbsPath.String(),
|
|
config: c.toMap(),
|
|
configFile: configFileAbsPath.String(),
|
|
destDir: c.DestDirAbsPath.String(),
|
|
executable: executable,
|
|
flags: templateDataFlags{
|
|
dryRun: c.dryRun,
|
|
force: c.force,
|
|
keepGoing: c.keepGoing,
|
|
noPager: c.noPager,
|
|
noTTY: c.noTTY,
|
|
},
|
|
fqdnHostname: fqdnHostname,
|
|
gid: gid,
|
|
group: group,
|
|
homeDir: c.homeDirAbsPath.String(),
|
|
hostname: hostname,
|
|
kernel: kernel,
|
|
os: runtime.GOOS,
|
|
osRelease: osRelease,
|
|
pathListSeparator: string(os.PathListSeparator),
|
|
pathSeparator: string(os.PathSeparator),
|
|
rawHomeDir: rawHomeDir,
|
|
sourceDir: sourceDirAbsPath.String(),
|
|
uid: uid,
|
|
username: username,
|
|
version: map[string]any{
|
|
"builtBy": c.versionInfo.BuiltBy,
|
|
"commit": c.versionInfo.Commit,
|
|
"date": c.versionInfo.Date,
|
|
"version": c.versionInfo.Version,
|
|
},
|
|
windowsVersion: windowsVersion,
|
|
workingTree: c.WorkingTreeAbsPath.String(),
|
|
}
|
|
}
|
|
|
|
// pageDiffOutput pages the diff output to stdout.
|
|
func (c *Config) pageDiffOutput(output string) error {
|
|
switch pagerCmd, err := c.getDiffPagerCmd(); {
|
|
case err != nil:
|
|
return err
|
|
case pagerCmd == nil:
|
|
return c.writeOutputString(output, 0o666)
|
|
default:
|
|
pagerCmd.Stdin = bytes.NewBufferString(output)
|
|
return chezmoilog.LogCmdRun(c.logger, pagerCmd)
|
|
}
|
|
}
|
|
|
|
// persistentPostRunRootE performs post-run actions for the root command.
|
|
func (c *Config) persistentPostRunRootE(cmd *cobra.Command, args []string) error {
|
|
annotations := getAnnotations(cmd)
|
|
|
|
// Verify modified config.
|
|
if annotations.hasTag(modifiesConfigFile) {
|
|
configFileAbsPath, err := c.getConfigFileAbsPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
configFileContents, err := c.baseSystem.ReadFile(configFileAbsPath)
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
err = nil
|
|
case err != nil:
|
|
// err is already set, do nothing.
|
|
default:
|
|
var format chezmoi.Format
|
|
if format, err = chezmoi.FormatFromAbsPath(configFileAbsPath); err == nil {
|
|
var config map[string]any
|
|
if err = format.Unmarshal(configFileContents, &config); err != nil {
|
|
// err is already set, do nothing.
|
|
} else {
|
|
err = c.decodeConfigMap(config, &ConfigFile{})
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
c.errorf("warning: %s: %v\n", configFileAbsPath, err)
|
|
}
|
|
}
|
|
|
|
// Perform auto git commands.
|
|
if annotations.hasTag(modifiesSourceDirectory) {
|
|
var status *chezmoigit.Status
|
|
if c.Git.AutoAdd || c.Git.AutoCommit || c.Git.AutoPush {
|
|
var err error
|
|
status, err = c.gitAdd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if c.Git.AutoCommit || c.Git.AutoPush {
|
|
if err := c.gitCommit(cmd, status); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if c.Git.AutoPush {
|
|
if err := c.gitPush(status); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return c.runHookPost(cmd.Name())
|
|
}
|
|
|
|
// persistentPreRunRootE performs pre-run actions for the root command.
|
|
func (c *Config) persistentPreRunRootE(cmd *cobra.Command, args []string) error {
|
|
annotations := getAnnotations(cmd)
|
|
|
|
// Add the completion template function. This needs cmd, so we can't do it
|
|
// in newConfig.
|
|
c.addTemplateFunc("completion", func(shell string) string {
|
|
return mustValue(completion(cmd, shell))
|
|
})
|
|
|
|
if runtime.GOOS == "windows" {
|
|
var err error
|
|
c.restoreWindowsConsole, err = termenv.EnableVirtualTerminalProcessing(termenv.DefaultOutput())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Save flags that were set on the command line. Skip some types as
|
|
// spf13/pflag does not round trip them correctly.
|
|
changedFlags := make(map[pflag.Value]string)
|
|
brokenFlagTypes := map[string]bool{
|
|
"stringToInt": true,
|
|
"stringToInt64": true,
|
|
"stringToString": true,
|
|
}
|
|
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
|
if flag.Changed && !brokenFlagTypes[flag.Value.Type()] {
|
|
changedFlags[flag.Value] = flag.Value.String()
|
|
}
|
|
})
|
|
|
|
// Read the config file.
|
|
if annotations.hasTag(doesNotRequireValidConfig) {
|
|
if configFileAbsPath, err := c.getConfigFileAbsPath(); err == nil {
|
|
_ = c.readConfig(configFileAbsPath)
|
|
}
|
|
} else {
|
|
configFileAbsPath, err := c.getConfigFileAbsPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := c.readConfig(configFileAbsPath); err != nil {
|
|
return fmt.Errorf("invalid config: %s: %w", configFileAbsPath, err)
|
|
}
|
|
}
|
|
|
|
// Restore flags that were set on the command line.
|
|
for value, original := range changedFlags {
|
|
if err := value.Set(original); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if c.force && c.Interactive {
|
|
return errors.New("the --force and --interactive flags are mutually exclusive")
|
|
}
|
|
|
|
// Configure the logger.
|
|
var handler slog.Handler
|
|
if c.debug {
|
|
handler = slog.NewTextHandler(c.stderr, nil)
|
|
} else {
|
|
handler = chezmoilog.NullHandler{}
|
|
}
|
|
c.logger = slog.New(handler)
|
|
slog.SetDefault(c.logger)
|
|
|
|
// Log basic information.
|
|
c.logger.Info("persistentPreRunRootE",
|
|
slog.Any("version", c.versionInfo),
|
|
slog.Any("args", os.Args),
|
|
slog.String("goVersion", runtime.Version()),
|
|
)
|
|
realSystem := chezmoi.NewRealSystem(c.fileSystem,
|
|
chezmoi.RealSystemWithSafe(c.Safe),
|
|
chezmoi.RealSystemWithScriptTempDir(c.ScriptTempDir),
|
|
)
|
|
c.baseSystem = realSystem
|
|
if c.debug {
|
|
systemLogger := c.logger.With(slog.String(logComponentKey, logComponentValueSystem))
|
|
c.baseSystem = chezmoi.NewDebugSystem(c.baseSystem, systemLogger)
|
|
}
|
|
|
|
// Set up the persistent state.
|
|
switch persistentStateMode := annotations.persistentStateMode(); {
|
|
case persistentStateMode == persistentStateModeEmpty:
|
|
c.persistentState = chezmoi.NewMockPersistentState()
|
|
case persistentStateMode == persistentStateModeReadOnly:
|
|
persistentStateFileAbsPath, err := c.persistentStateFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.persistentState, err = chezmoi.NewBoltPersistentState(
|
|
c.baseSystem,
|
|
persistentStateFileAbsPath,
|
|
chezmoi.BoltPersistentStateReadOnly,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case persistentStateMode == persistentStateModeReadMockWrite:
|
|
fallthrough
|
|
case persistentStateMode == persistentStateModeReadWrite && c.dryRun:
|
|
persistentStateFileAbsPath, err := c.persistentStateFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
persistentState, err := chezmoi.NewBoltPersistentState(
|
|
c.baseSystem,
|
|
persistentStateFileAbsPath,
|
|
chezmoi.BoltPersistentStateReadOnly,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dryRunPersistentState := chezmoi.NewMockPersistentState()
|
|
if err := persistentState.CopyTo(dryRunPersistentState); err != nil {
|
|
return err
|
|
}
|
|
if err := persistentState.Close(); err != nil {
|
|
return err
|
|
}
|
|
c.persistentState = dryRunPersistentState
|
|
case persistentStateMode == persistentStateModeReadWrite:
|
|
persistentStateFileAbsPath, err := c.persistentStateFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.persistentState, err = chezmoi.NewBoltPersistentState(
|
|
c.baseSystem,
|
|
persistentStateFileAbsPath,
|
|
chezmoi.BoltPersistentStateReadWrite,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
c.persistentState = chezmoi.NullPersistentState{}
|
|
}
|
|
if c.debug && c.persistentState != nil {
|
|
persistentStateLogger := c.logger.With(slog.String(logComponentKey, logComponentValuePersistentState))
|
|
c.persistentState = chezmoi.NewDebugPersistentState(c.persistentState, persistentStateLogger)
|
|
}
|
|
|
|
// Set up the source and destination systems.
|
|
c.sourceSystem = c.baseSystem
|
|
c.destSystem = c.baseSystem
|
|
if !annotations.hasTag(modifiesDestinationDirectory) {
|
|
c.destSystem = chezmoi.NewReadOnlySystem(c.destSystem)
|
|
}
|
|
if !annotations.hasTag(modifiesSourceDirectory) {
|
|
c.sourceSystem = chezmoi.NewReadOnlySystem(c.sourceSystem)
|
|
}
|
|
if c.dryRun || annotations.hasTag(dryRun) {
|
|
c.sourceSystem = chezmoi.NewDryRunSystem(c.sourceSystem)
|
|
c.destSystem = chezmoi.NewDryRunSystem(c.destSystem)
|
|
}
|
|
if annotations.hasTag(outputsDiff) ||
|
|
c.Verbose && (annotations.hasTag(modifiesDestinationDirectory) || annotations.hasTag(modifiesSourceDirectory)) {
|
|
// If the user has configured a diff pager, then start it as a process.
|
|
// Otherwise, write the diff output directly to stdout.
|
|
var writer io.Writer
|
|
switch pagerCmd, err := c.getDiffPagerCmd(); {
|
|
case err != nil:
|
|
return err
|
|
case pagerCmd == nil:
|
|
writer = c.stdout
|
|
default:
|
|
pipeReader, pipeWriter := io.Pipe()
|
|
pagerCmd.Stdin = pipeReader
|
|
lazyWriter := newLazyWriter(func() (io.WriteCloser, error) {
|
|
if err := chezmoilog.LogCmdStart(c.logger, pagerCmd); err != nil {
|
|
return nil, err
|
|
}
|
|
return pipeWriter, nil
|
|
})
|
|
writer = lazyWriter
|
|
c.diffPagerCmd = pagerCmd
|
|
c.diffPagerCmdStdin = lazyWriter
|
|
}
|
|
c.sourceSystem = c.newDiffSystem(c.sourceSystem, writer, c.SourceDirAbsPath)
|
|
c.destSystem = c.newDiffSystem(c.destSystem, writer, c.DestDirAbsPath)
|
|
}
|
|
|
|
if err := c.setEncryption(); err != nil && !annotations.hasTag(doesNotRequireValidConfig) {
|
|
return err
|
|
}
|
|
|
|
// Create the config directory if needed.
|
|
if annotations.hasTag(requiresConfigDirectory) {
|
|
configFileAbsPath, err := c.getConfigFileAbsPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := chezmoi.MkdirAll(c.baseSystem, configFileAbsPath.Dir(), fs.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create the source directory if needed.
|
|
if annotations.hasTag(createSourceDirectoryIfNeeded) {
|
|
if err := chezmoi.MkdirAll(c.baseSystem, c.SourceDirAbsPath, fs.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Verify that the source directory exists and is a directory, if needed.
|
|
if annotations.hasTag(requiresSourceDirectory) {
|
|
switch fileInfo, err := c.baseSystem.Stat(c.SourceDirAbsPath); {
|
|
case err == nil && fileInfo.IsDir():
|
|
// Do nothing.
|
|
case err == nil:
|
|
return fmt.Errorf("%s: not a directory", c.SourceDirAbsPath)
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create the runtime directory if needed.
|
|
if annotations.hasTag(runsCommands) {
|
|
if runtime.GOOS == "linux" && c.bds.RuntimeDir != "" {
|
|
// Snap sets the $XDG_RUNTIME_DIR environment variable to
|
|
// /run/user/$uid/snap.$snap_name, but does not create this
|
|
// directory. Consequently, any spawned processes that need
|
|
// $XDG_DATA_DIR will fail. As a work-around, create the directory
|
|
// if it does not exist. See
|
|
// https://forum.snapcraft.io/t/wayland-dconf-and-xdg-runtime-dir/186/13.
|
|
if err := chezmoi.MkdirAll(c.baseSystem, chezmoi.NewAbsPath(c.bds.RuntimeDir), 0o700); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine the working tree directory if it is not configured.
|
|
if c.WorkingTreeAbsPath.IsEmpty() {
|
|
workingTreeAbsPath := c.SourceDirAbsPath
|
|
for {
|
|
gitDirAbsPath := workingTreeAbsPath.JoinString(git.GitDirName)
|
|
if _, err := c.baseSystem.Stat(gitDirAbsPath); err == nil {
|
|
c.WorkingTreeAbsPath = workingTreeAbsPath
|
|
break
|
|
}
|
|
prevWorkingTreeDirAbsPath := workingTreeAbsPath
|
|
workingTreeAbsPath = workingTreeAbsPath.Dir()
|
|
if workingTreeAbsPath == c.homeDirAbsPath || workingTreeAbsPath.Len() >= prevWorkingTreeDirAbsPath.Len() {
|
|
c.WorkingTreeAbsPath = c.SourceDirAbsPath
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create the working tree directory if needed.
|
|
if annotations.hasTag(requiresWorkingTree) {
|
|
if _, err := c.SourceDirAbsPath.TrimDirPrefix(c.WorkingTreeAbsPath); err != nil {
|
|
return err
|
|
}
|
|
if err := chezmoi.MkdirAll(c.baseSystem, c.WorkingTreeAbsPath, fs.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
templateData := c.getTemplateData(cmd)
|
|
if err := os.Setenv("CHEZMOI", "1"); err != nil {
|
|
return err
|
|
}
|
|
for key, value := range map[string]string{
|
|
"ARCH": templateData.arch,
|
|
"ARGS": strings.Join(templateData.args, " "),
|
|
"CACHE_DIR": templateData.cacheDir,
|
|
"COMMAND": templateData.command,
|
|
"COMMAND_DIR": templateData.commandDir,
|
|
"CONFIG_FILE": templateData.configFile,
|
|
"DEST_DIR": templateData.destDir,
|
|
"EXECUTABLE": templateData.executable,
|
|
"FQDN_HOSTNAME": templateData.fqdnHostname,
|
|
"GID": templateData.gid,
|
|
"GROUP": templateData.group,
|
|
"HOME_DIR": templateData.homeDir,
|
|
"HOSTNAME": templateData.hostname,
|
|
"OS": templateData.os,
|
|
"RAW_HOME_DIR": templateData.rawHomeDir,
|
|
"SOURCE_DIR": templateData.sourceDir,
|
|
"UID": templateData.uid,
|
|
"USERNAME": templateData.username,
|
|
"WORKING_TREE": templateData.workingTree,
|
|
} {
|
|
if err := os.Setenv("CHEZMOI_"+key, value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if c.Verbose {
|
|
if err := os.Setenv("CHEZMOI_VERBOSE", "1"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for groupKey, group := range map[string]map[string]any{
|
|
"KERNEL": templateData.kernel,
|
|
"OS_RELEASE": templateData.osRelease,
|
|
"VERSION": templateData.version,
|
|
"WINDOWS_VERSION": templateData.windowsVersion,
|
|
} {
|
|
for key, value := range group {
|
|
key := "CHEZMOI_" + groupKey + "_" + camelCaseToUpperSnakeCase(key)
|
|
var valueStr string
|
|
switch value := value.(type) {
|
|
case string:
|
|
valueStr = value
|
|
case uint64:
|
|
valueStr = strconv.FormatUint(value, 10)
|
|
default:
|
|
panic(fmt.Errorf("%s has unexpected type %T", key, value))
|
|
}
|
|
if err := os.Setenv(key, valueStr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := c.setEnvironmentVariables(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.runHookPre(cmd.Name())
|
|
}
|
|
|
|
// persistentStateFile returns the absolute path to the persistent state file,
|
|
// returning the first persistent file found, and returning the default path if
|
|
// none are found.
|
|
func (c *Config) persistentStateFile() (chezmoi.AbsPath, error) {
|
|
if !c.PersistentStateAbsPath.IsEmpty() {
|
|
return c.PersistentStateAbsPath, nil
|
|
}
|
|
configFileAbsPath, err := c.getConfigFileAbsPath()
|
|
if err != nil {
|
|
return chezmoi.EmptyAbsPath, err
|
|
}
|
|
return configFileAbsPath.Dir().Join(persistentStateFileRelPath), nil
|
|
}
|
|
|
|
// progressAutoFunc detects whether progress bars should be displayed.
|
|
func (c *Config) progressAutoFunc() bool {
|
|
if stdout, ok := c.stdout.(*os.File); ok {
|
|
return term.IsTerminal(int(stdout.Fd()))
|
|
}
|
|
return false
|
|
}
|
|
|
|
// readConfig reads the config file, if it exists.
|
|
func (c *Config) readConfig(configFileAbsPath chezmoi.AbsPath) error {
|
|
switch err := c.decodeConfigFile(configFileAbsPath, &c.ConfigFile); {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
return nil
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// resetSourceState clears the cached source state, if any.
|
|
func (c *Config) resetSourceState() {
|
|
c.sourceState = nil
|
|
c.sourceStateErr = nil
|
|
}
|
|
|
|
// run runs name with args in dir.
|
|
func (c *Config) run(dir chezmoi.AbsPath, name string, args []string) error {
|
|
cmd := exec.Command(name, args...)
|
|
if !dir.IsEmpty() {
|
|
dirRawAbsPath, err := c.baseSystem.RawPath(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Dir = dirRawAbsPath.String()
|
|
}
|
|
cmd.Stdin = c.stdin
|
|
cmd.Stdout = c.stdout
|
|
cmd.Stderr = c.stderr
|
|
if err := chezmoilog.LogCmdRun(c.logger, cmd); err != nil {
|
|
return fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// runEditor runs the configured editor with args.
|
|
func (c *Config) runEditor(args []string) error {
|
|
if err := c.persistentState.Close(); err != nil {
|
|
return err
|
|
}
|
|
editor, editorArgs, err := c.editor(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
start := time.Now()
|
|
err = c.run(chezmoi.EmptyAbsPath, editor, editorArgs)
|
|
if runtime.GOOS != "windows" && c.Edit.MinDuration != 0 {
|
|
if duration := time.Since(start); duration < c.Edit.MinDuration {
|
|
c.errorf("warning: %s: returned in less than %s\n", shellQuoteCommand(editor, editorArgs), c.Edit.MinDuration)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// runHook runs a command or script hook.
|
|
func (c *Config) runHook(command commandConfig) error {
|
|
var name string
|
|
var args []string
|
|
switch {
|
|
case command.Command != "" && command.Script != "":
|
|
return errors.New("cannot specify both command and script")
|
|
case command.Command != "":
|
|
name = command.Command
|
|
args = command.Args
|
|
case command.Script != "":
|
|
extension := strings.TrimPrefix(strings.ToLower(path.Ext(command.Script)), ".")
|
|
if interpreter, ok := c.Interpreters[extension]; ok {
|
|
name = interpreter.Command
|
|
args = slices.Concat(interpreter.Args, []string{command.Script}, command.Args)
|
|
} else {
|
|
name = command.Script
|
|
args = command.Args
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
return c.run(c.homeDirAbsPath, name, args)
|
|
}
|
|
|
|
// runHookPost runs the hook's post command, if it is set.
|
|
func (c *Config) runHookPost(hook string) error {
|
|
if err := c.runHook(c.Hooks[hook].Post); err != nil {
|
|
return fmt.Errorf("%s: post: %w", hook, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// runHookPre runs the hook's pre command, if it is set.
|
|
func (c *Config) runHookPre(hook string) error {
|
|
if err := c.runHook(c.Hooks[hook].Pre); err != nil {
|
|
return fmt.Errorf("%s: pre: %w", hook, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type runInstallInitShellOptions struct {
|
|
args []string
|
|
interactive bool
|
|
packageManager string
|
|
shell bool
|
|
}
|
|
|
|
func (c *Config) runInstallInitShellSh(
|
|
sourceState *chezmoi.SourceState,
|
|
command string,
|
|
commandArgs []string,
|
|
options runInstallInitShellOptions,
|
|
) error {
|
|
script, err := sourceState.ExecuteTemplateData(chezmoi.ExecuteTemplateDataOptions{
|
|
NameRelPath: chezmoi.NewRelPath("install-init-shell.sh.tmpl"),
|
|
Data: templates.InstallInitShellShTmpl,
|
|
ExtraData: map[string]any{
|
|
"args": options.args,
|
|
"interactive": options.interactive,
|
|
"packageManager": options.packageManager,
|
|
"shell": options.shell,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allArgs := slices.Concat(
|
|
commandArgs,
|
|
[]string{"sh", "-c", string(script)},
|
|
)
|
|
return c.run(chezmoi.EmptyAbsPath, command, allArgs)
|
|
}
|
|
|
|
// setEncryption configures c's encryption.
|
|
func (c *Config) setEncryption() error {
|
|
// Override the age recipients for encryption if --age-recipient or
|
|
// --age-recipient-file is set.
|
|
switch {
|
|
case c.ageRecipient != "" && c.ageRecipientFile != "":
|
|
return errors.New("--age-recipient and --age-recipient-file cannot both be set")
|
|
case c.ageRecipient != "":
|
|
c.Age.Recipient = c.ageRecipient
|
|
c.Age.Recipients = nil
|
|
c.Age.RecipientsFile = chezmoi.EmptyAbsPath
|
|
c.Age.RecipientsFiles = nil
|
|
case c.ageRecipientFile != "":
|
|
c.Age.Recipient = ""
|
|
c.Age.Recipients = nil
|
|
c.Age.RecipientsFile = chezmoi.NewAbsPath(c.ageRecipientFile)
|
|
c.Age.RecipientsFiles = nil
|
|
}
|
|
|
|
switch c.Encryption {
|
|
case "age":
|
|
c.Age.UseBuiltin = c.UseBuiltinAge.Value(c.useBuiltinAgeAutoFunc)
|
|
c.encryption = &c.Age
|
|
case "gpg":
|
|
c.encryption = &c.GPG
|
|
case "transparent":
|
|
c.encryption = chezmoi.TransparentEncryption{}
|
|
case "":
|
|
// Detect encryption if any non-default configuration is set, preferring
|
|
// gpg for backwards compatibility.
|
|
switch {
|
|
case !reflect.DeepEqual(c.GPG, defaultGPGEncryptionConfig):
|
|
c.errorf(
|
|
"warning: 'encryption' not set, using gpg configuration. " +
|
|
"Check if 'encryption' is correctly set as the top-level key.\n",
|
|
)
|
|
c.encryption = &c.GPG
|
|
case !reflect.DeepEqual(c.Age, defaultAgeEncryptionConfig):
|
|
c.errorf(
|
|
"warning: 'encryption' not set, using age configuration. " +
|
|
"Check if 'encryption' is correctly set as the top-level key.\n",
|
|
)
|
|
c.Age.UseBuiltin = c.UseBuiltinAge.Value(c.useBuiltinAgeAutoFunc)
|
|
c.encryption = &c.Age
|
|
default:
|
|
c.encryption = chezmoi.NoEncryption{}
|
|
}
|
|
default:
|
|
return fmt.Errorf("%s: unknown encryption", c.Encryption)
|
|
}
|
|
|
|
if c.debug {
|
|
encryptionLogger := c.logger.With(slog.String(logComponentKey, logComponentValueEncryption))
|
|
c.encryption = chezmoi.NewDebugEncryption(c.encryption, encryptionLogger)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setEnvironmentVariables sets all environment variables defined in c.
|
|
func (c *Config) setEnvironmentVariables() error {
|
|
var env map[string]string
|
|
switch {
|
|
case len(c.Env) != 0 && len(c.ScriptEnv) != 0:
|
|
return errors.New("only one of env or scriptEnv may be set")
|
|
case len(c.Env) != 0:
|
|
env = c.Env
|
|
case len(c.ScriptEnv) != 0:
|
|
env = c.ScriptEnv
|
|
}
|
|
for key, value := range env {
|
|
if strings.HasPrefix(key, "CHEZMOI_") {
|
|
c.errorf("warning: %s: overriding reserved environment variable\n", key)
|
|
}
|
|
if err := os.Setenv(key, value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sourceAbsPaths returns the source absolute paths for each target path in
|
|
// args.
|
|
func (c *Config) sourceAbsPaths(sourceState *chezmoi.SourceState, args []string) ([]chezmoi.AbsPath, error) {
|
|
targetRelPaths, err := c.targetRelPaths(sourceState, args, targetRelPathsOptions{
|
|
mustBeInSourceState: true,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceAbsPaths := make([]chezmoi.AbsPath, 0, len(targetRelPaths))
|
|
for _, targetRelPath := range targetRelPaths {
|
|
sourceAbsPath := c.SourceDirAbsPath.Join(sourceState.MustEntry(targetRelPath).SourceRelPath().RelPath())
|
|
sourceAbsPaths = append(sourceAbsPaths, sourceAbsPath)
|
|
}
|
|
return sourceAbsPaths, nil
|
|
}
|
|
|
|
func (c *Config) targetRelPath(absPath chezmoi.AbsPath) (chezmoi.RelPath, error) {
|
|
relPath, err := absPath.TrimDirPrefix(c.DestDirAbsPath)
|
|
if notInAbsDirError := (&chezmoi.NotInAbsDirError{}); errors.As(err, ¬InAbsDirError) {
|
|
return chezmoi.EmptyRelPath, fmt.Errorf("%s: not in destination directory (%s)", absPath, c.DestDirAbsPath)
|
|
}
|
|
return relPath, err
|
|
}
|
|
|
|
type targetRelPathsOptions struct {
|
|
mustBeInSourceState bool
|
|
mustNotBeExternal bool
|
|
recursive bool
|
|
}
|
|
|
|
// targetRelPaths returns the target relative paths for each target path in
|
|
// args. The returned paths are sorted and de-duplicated.
|
|
func (c *Config) targetRelPaths(
|
|
sourceState *chezmoi.SourceState,
|
|
args []string,
|
|
options targetRelPathsOptions,
|
|
) ([]chezmoi.RelPath, error) {
|
|
targetRelPaths := make([]chezmoi.RelPath, 0, len(args))
|
|
for _, arg := range args {
|
|
argAbsPath, err := chezmoi.NewAbsPathFromExtPath(arg, c.homeDirAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targetRelPath, err := c.targetRelPath(argAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceStateEntry := sourceState.Get(targetRelPath)
|
|
if sourceStateEntry == nil {
|
|
return nil, fmt.Errorf("%s: not managed", arg)
|
|
}
|
|
if options.mustBeInSourceState {
|
|
if _, ok := sourceStateEntry.(*chezmoi.SourceStateRemove); ok {
|
|
return nil, fmt.Errorf("%s: not in source state", arg)
|
|
}
|
|
}
|
|
if options.mustNotBeExternal {
|
|
targetStateEntry, err := sourceStateEntry.TargetStateEntry(c.destSystem, c.DestDirAbsPath.Join(targetRelPath))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if targetStateEntry.SourceAttr().External {
|
|
return nil, fmt.Errorf("%s: is an external", arg)
|
|
}
|
|
}
|
|
targetRelPaths = append(targetRelPaths, targetRelPath)
|
|
if options.recursive {
|
|
parentRelPath := targetRelPath
|
|
// FIXME we should not call s.TargetRelPaths() here - risk of
|
|
// accidentally quadratic
|
|
for _, targetRelPath := range sourceState.TargetRelPaths() {
|
|
if _, err := targetRelPath.TrimDirPrefix(parentRelPath); err == nil {
|
|
targetRelPaths = append(targetRelPaths, targetRelPath)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(targetRelPaths) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Sort and de-duplicate targetRelPaths in place.
|
|
slices.SortFunc(targetRelPaths, chezmoi.CompareRelPaths)
|
|
n := 1
|
|
for i := 1; i < len(targetRelPaths); i++ {
|
|
if targetRelPaths[i] != targetRelPaths[i-1] {
|
|
targetRelPaths[n] = targetRelPaths[i]
|
|
n++
|
|
}
|
|
}
|
|
return targetRelPaths[:n], nil
|
|
}
|
|
|
|
// targetRelPathsBySourcePath returns the target relative paths for each arg in
|
|
// args.
|
|
func (c *Config) targetRelPathsBySourcePath(sourceState *chezmoi.SourceState, args []string) ([]chezmoi.RelPath, error) {
|
|
targetRelPaths := make([]chezmoi.RelPath, len(args))
|
|
targetRelPathsBySourceRelPath := make(map[chezmoi.RelPath]chezmoi.RelPath)
|
|
_ = sourceState.ForEach(
|
|
func(targetRelPath chezmoi.RelPath, sourceStateEntry chezmoi.SourceStateEntry) error {
|
|
sourceRelPath := sourceStateEntry.SourceRelPath().RelPath()
|
|
targetRelPathsBySourceRelPath[sourceRelPath] = targetRelPath
|
|
return nil
|
|
},
|
|
)
|
|
for i, arg := range args {
|
|
argAbsPath, err := chezmoi.NewAbsPathFromExtPath(arg, c.homeDirAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceRelPath, err := argAbsPath.TrimDirPrefix(c.SourceDirAbsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targetRelPath, ok := targetRelPathsBySourceRelPath[sourceRelPath]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: not in source state", arg)
|
|
}
|
|
targetRelPaths[i] = targetRelPath
|
|
}
|
|
return targetRelPaths, nil
|
|
}
|
|
|
|
// targetValidArgs returns target completions for toComplete given args.
|
|
func (c *Config) targetValidArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if !c.Completion.Custom {
|
|
return nil, cobra.ShellCompDirectiveDefault
|
|
}
|
|
|
|
toCompleteAbsPath, err := chezmoi.NewAbsPathFromExtPath(toComplete, c.homeDirAbsPath)
|
|
if err != nil {
|
|
cobra.CompErrorln(err.Error())
|
|
return nil, cobra.ShellCompDirectiveError
|
|
}
|
|
|
|
sourceState, err := c.getSourceState(cmd.Context(), cmd)
|
|
if err != nil {
|
|
cobra.CompErrorln(err.Error())
|
|
return nil, cobra.ShellCompDirectiveError
|
|
}
|
|
|
|
var completions []string
|
|
if err := sourceState.ForEach(func(targetRelPath chezmoi.RelPath, sourceStateEntry chezmoi.SourceStateEntry) error {
|
|
completion := c.DestDirAbsPath.Join(targetRelPath).String()
|
|
if _, ok := sourceStateEntry.(*chezmoi.SourceStateDir); ok {
|
|
completion += "/"
|
|
}
|
|
if strings.HasPrefix(completion, toCompleteAbsPath.String()) {
|
|
completions = append(completions, completion)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
cobra.CompErrorln(err.Error())
|
|
return nil, cobra.ShellCompDirectiveError
|
|
}
|
|
|
|
if !filepath.IsAbs(toComplete) {
|
|
for i, completion := range completions {
|
|
completions[i] = strings.TrimPrefix(completion, c.commandDirAbsPath.String()+"/")
|
|
}
|
|
}
|
|
|
|
return completions, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
// tempDir returns the temporary directory for the given key, creating it if
|
|
// needed.
|
|
func (c *Config) tempDir(key string) (chezmoi.AbsPath, error) {
|
|
if tempDirAbsPath, ok := c.tempDirs[key]; ok {
|
|
return tempDirAbsPath, nil
|
|
}
|
|
tempDir, err := os.MkdirTemp(c.TempDir.String(), key)
|
|
chezmoilog.InfoOrError(c.logger, "MkdirTemp", err, slog.String("tempDir", tempDir))
|
|
if err != nil {
|
|
return chezmoi.EmptyAbsPath, err
|
|
}
|
|
tempDirAbsPath := chezmoi.NewAbsPath(tempDir)
|
|
c.tempDirs[key] = tempDirAbsPath
|
|
if runtime.GOOS != "windows" {
|
|
if err := os.Chmod(tempDir, 0o700); err != nil {
|
|
return chezmoi.EmptyAbsPath, err
|
|
}
|
|
}
|
|
return tempDirAbsPath, nil
|
|
}
|
|
|
|
// useBuiltinAgeAutoFunc detects whether the builtin age should be used.
|
|
func (c *Config) useBuiltinAgeAutoFunc() bool {
|
|
if _, err := chezmoi.LookPath(c.Age.Command); err == nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// useBuiltinGitAutoFunc detects whether the builtin git should be used.
|
|
func (c *Config) useBuiltinGitAutoFunc() bool {
|
|
if _, err := chezmoi.LookPath(c.Git.Command); err == nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// writeOutput writes data to the configured output.
|
|
func (c *Config) writeOutput(data []byte, perm fs.FileMode) error {
|
|
if c.outputAbsPath.IsEmpty() || c.outputAbsPath == chezmoi.NewAbsPath("-") {
|
|
_, err := c.stdout.Write(data)
|
|
return err
|
|
}
|
|
file, err := os.OpenFile(c.outputAbsPath.String(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
fileInfo, err := file.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fileInfo.Mode().Perm()&^perm != 0 {
|
|
if err := file.Chmod(fileInfo.Mode().Perm() & perm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = file.Write(data)
|
|
return err
|
|
}
|
|
|
|
// writeOutputString writes data to the configured output.
|
|
func (c *Config) writeOutputString(data string, perm fs.FileMode) error {
|
|
return c.writeOutput([]byte(data), perm)
|
|
}
|
|
|
|
type writePathsOptions struct {
|
|
nulPathSeparator bool
|
|
tree bool
|
|
}
|
|
|
|
func (c *Config) writePaths(paths []string, options writePathsOptions) error {
|
|
pathSeparator := byte('\n')
|
|
if options.nulPathSeparator {
|
|
pathSeparator = '\x00'
|
|
}
|
|
builder := strings.Builder{}
|
|
if options.tree {
|
|
newPathListTreeFromPathsSlice(paths).writeChildren(&builder, "", " ")
|
|
} else {
|
|
slices.Sort(paths)
|
|
for _, path := range paths {
|
|
builder.WriteString(path)
|
|
builder.WriteByte(pathSeparator)
|
|
}
|
|
}
|
|
return c.writeOutputString(builder.String(), 0o666)
|
|
}
|
|
|
|
func newConfigFile(bds *xdg.BaseDirectorySpecification) ConfigFile {
|
|
return ConfigFile{
|
|
// Global configuration.
|
|
CacheDirAbsPath: chezmoi.NewAbsPath(bds.CacheHome).Join(chezmoiRelPath),
|
|
Color: autoBool{
|
|
auto: true,
|
|
},
|
|
Data: make(map[string]any),
|
|
Interpreters: DefaultInterpreters,
|
|
Mode: chezmoi.ModeFile,
|
|
Pager: os.Getenv("PAGER"),
|
|
Progress: autoBool{
|
|
auto: true,
|
|
},
|
|
PINEntry: pinEntryConfig{
|
|
Options: pinEntryDefaultOptions,
|
|
},
|
|
Safe: true,
|
|
TempDir: chezmoi.NewAbsPath(os.TempDir()),
|
|
Template: templateConfig{
|
|
Options: chezmoi.DefaultTemplateOptions,
|
|
},
|
|
Umask: chezmoi.Umask,
|
|
UseBuiltinAge: autoBool{
|
|
auto: true,
|
|
},
|
|
UseBuiltinGit: autoBool{
|
|
auto: true,
|
|
},
|
|
Warnings: warningsConfig{
|
|
ConfigFileTemplateHasChanged: true,
|
|
},
|
|
|
|
// Password manager configurations.
|
|
Bitwarden: bitwardenConfig{
|
|
Command: "bw",
|
|
},
|
|
BitwardenSecrets: bitwardenSecretsConfig{
|
|
Command: "bws",
|
|
},
|
|
Dashlane: dashlaneConfig{
|
|
Command: "dcli",
|
|
},
|
|
Doppler: dopplerConfig{
|
|
Command: "doppler",
|
|
},
|
|
Ejson: ejsonConfig{
|
|
KeyDir: cmp.Or(os.Getenv("EJSON_KEYDIR"), "/opt/ejson/keys"),
|
|
},
|
|
Gopass: gopassConfig{
|
|
Command: "gopass",
|
|
},
|
|
Keepassxc: keepassxcConfig{
|
|
Command: "keepassxc-cli",
|
|
Prompt: true,
|
|
Mode: keepassxcModeCachePassword,
|
|
},
|
|
Keeper: keeperConfig{
|
|
Command: "keeper",
|
|
},
|
|
Lastpass: lastpassConfig{
|
|
Command: "lpass",
|
|
},
|
|
Onepassword: onepasswordConfig{
|
|
Command: "op",
|
|
Prompt: true,
|
|
Mode: onepasswordModeAccount,
|
|
},
|
|
Pass: passConfig{
|
|
Command: "pass",
|
|
},
|
|
Passhole: passholeConfig{
|
|
Command: "ph",
|
|
Prompt: true,
|
|
},
|
|
ProtonPass: protonPassConfig{
|
|
Command: "pass-cli",
|
|
},
|
|
RBW: rbwConfig{
|
|
Command: "rbw",
|
|
},
|
|
Vault: vaultConfig{
|
|
Command: "vault",
|
|
},
|
|
|
|
// Encryption configurations.
|
|
Age: defaultAgeEncryptionConfig,
|
|
GPG: defaultGPGEncryptionConfig,
|
|
|
|
// Command configurations.
|
|
Add: addCmdConfig{
|
|
Secrets: newChoiceFlag("warning", severityValues),
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
recursive: true,
|
|
},
|
|
Diff: diffCmdConfig{
|
|
Exclude: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesNone),
|
|
Pager: defaultSentinel,
|
|
ScriptContents: true,
|
|
include: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesAll),
|
|
},
|
|
Docker: dockerCmdConfig{
|
|
Command: "docker",
|
|
exec: dockerExecCmdConfig{
|
|
interactive: true,
|
|
shell: true,
|
|
},
|
|
},
|
|
Edit: editCmdConfig{
|
|
Hardlink: true,
|
|
MinDuration: 1 * time.Second,
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
},
|
|
Format: newChoiceFlag(formatJSON, readDataFormatValues),
|
|
Git: gitCmdConfig{
|
|
Command: "git",
|
|
},
|
|
GitHub: gitHubConfig{
|
|
RefreshPeriod: 1 * time.Minute,
|
|
},
|
|
Merge: mergeCmdConfig{
|
|
Command: "vimdiff",
|
|
},
|
|
Status: statusCmdConfig{
|
|
Exclude: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesNone),
|
|
PathStyle: newChoiceFlag(pathStyleRelative, targetPathStyleValues),
|
|
include: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesAll),
|
|
recursive: true,
|
|
},
|
|
Update: updateCmdConfig{
|
|
Apply: true,
|
|
RecurseSubmodules: true,
|
|
filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone),
|
|
recursive: true,
|
|
},
|
|
Verify: verifyCmdConfig{
|
|
Exclude: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesNone),
|
|
include: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesAll),
|
|
recursive: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (f *ConfigFile) toMap() map[string]any {
|
|
// Make a copy of f and replace any default sentinels with the empty string
|
|
// to ensure that there are no default sentinels in the result.
|
|
configFile := *f
|
|
if configFile.Diff.Pager == defaultSentinel {
|
|
configFile.Diff.Pager = ""
|
|
}
|
|
|
|
// This is a horrible hack. We want the returned map to contain only simple
|
|
// types because they are used with masterminds/sprig template functions
|
|
// which don't accept fmt.Stringers in place of strings. As a work-around,
|
|
// round-trip via JSON.
|
|
data, err := json.Marshal(configFile)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var result map[string]any
|
|
must(json.Unmarshal(data, &result))
|
|
return result
|
|
}
|
|
|
|
func parseCommand(command string, args []string) (string, []string, error) {
|
|
// If command is found, then return it.
|
|
if commandPath, err := chezmoi.LookPath(command); err == nil {
|
|
return commandPath, args, nil
|
|
}
|
|
|
|
// Otherwise, if the command contains spaces, parse it as a shell command.
|
|
if whitespaceRx.MatchString(command) {
|
|
var words []*syntax.Word
|
|
for word, err := range syntax.NewParser().WordsSeq(strings.NewReader(command)) {
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
words = append(words, word)
|
|
}
|
|
switch fields, err := expand.Fields(&expand.Config{
|
|
Env: expand.FuncEnviron(os.Getenv),
|
|
}, words...); {
|
|
case err != nil:
|
|
return "", nil, err
|
|
case len(fields) > 1:
|
|
return fields[0], append(fields[1:], args...), nil
|
|
case len(fields) == 1:
|
|
return fields[0], args, nil
|
|
}
|
|
}
|
|
|
|
// Fallback to the command only.
|
|
return command, args, nil
|
|
}
|
|
|
|
// prependParentRelPaths returns a new slice of RelPaths where the parents of
|
|
// each RelPath appear before each RelPath.
|
|
func prependParentRelPaths(relPaths []chezmoi.RelPath) []chezmoi.RelPath {
|
|
// For each target relative path, enumerate its parents from the root down
|
|
// and insert any parents which have not yet been seen.
|
|
result := make([]chezmoi.RelPath, 0, len(relPaths))
|
|
seenRelPaths := make(map[chezmoi.RelPath]struct{}, len(relPaths))
|
|
for _, relPath := range relPaths {
|
|
components := relPath.SplitAll()
|
|
for i := 1; i < len(components); i++ {
|
|
parentRelPath := chezmoi.EmptyRelPath.Join(components[:i]...)
|
|
if _, ok := seenRelPaths[parentRelPath]; !ok {
|
|
result = append(result, parentRelPath)
|
|
seenRelPaths[parentRelPath] = struct{}{}
|
|
}
|
|
}
|
|
result = append(result, relPath)
|
|
seenRelPaths[relPath] = struct{}{}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// registerCommonFlagCompletionFuncs registers completion functions for cmd's
|
|
// common flags, recursively. It panics on any error.
|
|
func registerCommonFlagCompletionFuncs(cmd *cobra.Command) {
|
|
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
|
if _, exist := cmd.GetFlagCompletionFunc(flag.Name); exist {
|
|
return
|
|
}
|
|
if flagCompletionFunc, ok := commonFlagCompletionFuncs[flag.Name]; ok {
|
|
must(cmd.RegisterFlagCompletionFunc(flag.Name, flagCompletionFunc))
|
|
}
|
|
})
|
|
for _, command := range cmd.Commands() {
|
|
registerCommonFlagCompletionFuncs(command)
|
|
}
|
|
}
|
|
|
|
// sha256Sum returns the SHA256 sum of data.
|
|
func sha256Sum(data []byte) []byte {
|
|
sha256SumArr := sha256.Sum256(data)
|
|
return sha256SumArr[:]
|
|
}
|
|
|
|
// withVersionInfo sets the version information.
|
|
func withVersionInfo(versionInfo VersionInfo) configOption {
|
|
return func(c *Config) error {
|
|
var version *semver.Version
|
|
var versionElems []string
|
|
if versionInfo.Version != "" {
|
|
var err error
|
|
version, err = semver.NewVersion(strings.TrimPrefix(versionInfo.Version, "v"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
versionElems = append(versionElems, "v"+version.String())
|
|
} else {
|
|
versionElems = append(versionElems, "dev")
|
|
}
|
|
if versionInfo.Commit != "" {
|
|
versionElems = append(versionElems, "commit "+versionInfo.Commit)
|
|
}
|
|
if versionInfo.Date != "" {
|
|
date := versionInfo.Date
|
|
if sec, err := strconv.ParseInt(date, 10, 64); err == nil {
|
|
date = time.Unix(sec, 0).UTC().Format(time.RFC3339)
|
|
}
|
|
versionElems = append(versionElems, "built at "+date)
|
|
}
|
|
if versionInfo.BuiltBy != "" {
|
|
versionElems = append(versionElems, "built by "+versionInfo.BuiltBy)
|
|
}
|
|
if version != nil {
|
|
c.version = *version
|
|
}
|
|
c.versionInfo = versionInfo
|
|
c.versionStr = strings.Join(versionElems, ", ")
|
|
return nil
|
|
}
|
|
}
|