Files

3474 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":
destAbsPath := c.DestDirAbsPath.Join(targetRelPath)
if actualEntryState.Type == chezmoi.EntryStateTypeRemove {
destAbsPath = chezmoi.DevNullAbsPath
}
if err := c.diffFile(
targetRelPath,
destAbsPath, 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, &notInAbsDirError) {
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
}
}