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/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", englishList(configFileAbsPathStrs))
		}
	}

	// Fallback to XDG Base Directory Specification default.
	configHomeAbsPath, err := chezmoi.NewAbsPathFromExtPath(bds.ConfigHome, c.homeDirAbsPath)
	if err != nil {
		return chezmoi.EmptyAbsPath, err
	}
	return configHomeAbsPath.JoinString("chezmoi", "chezmoi.toml"), nil
}

// defaultPreApplyFunc is the default pre-apply function. If the target entry
// has changed since chezmoi last wrote it then it prompts the user for the
// action to take.
func (c *Config) defaultPreApplyFunc(
	targetRelPath chezmoi.RelPath,
	targetEntryState, lastWrittenEntryState, actualEntryState *chezmoi.EntryState,
) error {
	c.logger.Info("defaultPreApplyFunc",
		chezmoilog.Stringer("targetRelPath", targetRelPath),
		slog.Any("targetEntryState", targetEntryState),
		slog.Any("lastWrittenEntryState", lastWrittenEntryState),
		slog.Any("actualEntryState", actualEntryState),
	)

	switch {
	case c.force:
		return nil
	case targetEntryState.Equivalent(actualEntryState):
		return nil
	}

	// Prepare decision for which kind of prompt we need (if any)
	type promptMode int
	const (
		promptNone     promptMode = iota
		promptYesNoAll            // yes/no/all/quit (just ask, don't indicate if there is a conflict)
		promptConflict            // overwrite/all-overwrite/skip/quit (conflict-specific prompt)
	)
	mode := promptNone

	targetDirty := lastWrittenEntryState != nil && !lastWrittenEntryState.Equivalent(actualEntryState)
	targetPreExisting := lastWrittenEntryState == nil && actualEntryState.Type != chezmoi.EntryStateTypeRemove

	// Select prompt mode based on command line flag
	switch {
	case c.Interactive: // Prompt no matter what
		mode = promptYesNoAll
	case c.LessInteractive: // Prompt if target is dirty or pre-existing (i.e., only overwrite what chezmoi has written)
		if targetDirty || targetPreExisting {
			mode = promptConflict
		}
	default: // Prompt in *some* cases of a dirty target:
		switch {
		case targetEntryState.Overwrite():
			mode = promptNone
		case targetEntryState.Type == chezmoi.EntryStateTypeScript:
			mode = promptNone
		case lastWrittenEntryState == nil:
			mode = promptNone
		case lastWrittenEntryState.Equivalent(actualEntryState):
			mode = promptNone
		case targetDirty:
			mode = promptConflict
		}
	}

	switch {
	case mode == promptNone:
		return nil
	case mode == promptConflict && c.errorOnConflict:
		return chezmoi.ExitCodeError(1)
	}

	// Now prompt based on choice made above
	actualContents := actualEntryState.Contents()
	targetContents := targetEntryState.Contents()
	var choices []string
	if actualContents != nil || targetContents != nil {
		choices = append(choices, "diff")
	}
	var promptText string
	if mode == promptYesNoAll {
		choices = append(choices, choicesYesNoAllQuit...)
		promptText = fmt.Sprintf("Apply %s", targetRelPath)
	} else {
		choices = append(choices, choicesOverwrite...)
		if targetDirty {
			promptText = fmt.Sprintf("%s has changed since chezmoi last wrote it", targetRelPath)
		} else {
			promptText = fmt.Sprintf("%s already exists", targetRelPath)
		}
	}

	for {
		switch choice, err := c.promptChoice(promptText, choices); {
		case err != nil:
			return err
		case choice == "diff":
			if err := c.diffFile(
				targetRelPath,
				c.DestDirAbsPath.Join(targetRelPath), actualContents, actualEntryState.Mode,
				chezmoi.EmptyAbsPath, targetContents, targetEntryState.Mode,
			); err != nil {
				return err
			}
		case choice == "yes":
			return nil
		case choice == "no":
			return fs.SkipDir
		case choice == "all":
			// Delicate difference to all-overwrite (mainly for backwards compatibility): Disabling --interactive means
			// we still prompt for dirty files, whereas all-overwrite adds --force to really prompt no more.
			c.Interactive = false
			return nil
		case choice == "overwrite":
			return nil
		case choice == "all-overwrite":
			c.force = true
			return nil
		case choice == "skip":
			return fs.SkipDir
		case choice == "quit":
			return chezmoi.ExitCodeError(0)
		default:
			panic(choice + ": unexpected choice")
		}
	}
}

// defaultSourceDir returns the default source directory according to the XDG
// Base Directory Specification.
func (c *Config) defaultSourceDir(fileSystem vfs.Stater, bds *xdg.BaseDirectorySpecification) (chezmoi.AbsPath, error) {
	// Check for XDG Base Directory Specification data directories first.
	for _, dataDir := range bds.DataDirs {
		dataDirAbsPath, err := chezmoi.NewAbsPathFromExtPath(dataDir, c.homeDirAbsPath)
		if err != nil {
			return chezmoi.EmptyAbsPath, err
		}
		sourceDirAbsPath := dataDirAbsPath.Join(chezmoiRelPath)
		if _, err := fileSystem.Stat(sourceDirAbsPath.String()); err == nil {
			return sourceDirAbsPath, nil
		}
	}
	// Fallback to XDG Base Directory Specification default.
	dataHomeAbsPath, err := chezmoi.NewAbsPathFromExtPath(bds.DataHome, c.homeDirAbsPath)
	if err != nil {
		return chezmoi.EmptyAbsPath, err
	}
	return dataHomeAbsPath.Join(chezmoiRelPath), nil
}

type onNotExist int

const (
	onNotExistError onNotExist = iota
	onNotExistIgnore
	onNotExistAdd
)

type destAbsPathInfosOptions struct {
	follow       bool
	onIgnoreFunc func(chezmoi.RelPath)
	onNotExist   onNotExist
	recursive    bool
}

// destAbsPathInfos returns the os/fs.FileInfos for each destination entry in
// args, recursing into subdirectories and following symlinks if configured in
// options.
func (c *Config) destAbsPathInfos(
	sourceState *chezmoi.SourceState,
	args []string,
	options destAbsPathInfosOptions,
) (map[chezmoi.AbsPath]fs.FileInfo, error) {
	destAbsPathInfos := make(map[chezmoi.AbsPath]fs.FileInfo)
	for _, arg := range args {
		arg = filepath.Clean(arg)
		destAbsPath, err := chezmoi.NewAbsPathFromExtPath(arg, c.homeDirAbsPath)
		if err != nil {
			return nil, err
		}
		targetRelPath, err := c.targetRelPath(destAbsPath)
		if err != nil {
			return nil, err
		}
		if sourceState.Ignore(targetRelPath) {
			options.onIgnoreFunc(targetRelPath)
			continue
		}
		if options.recursive {
			walkFunc := func(destAbsPath chezmoi.AbsPath, fileInfo fs.FileInfo, err error) error {
				switch {
				case errors.Is(err, fs.ErrNotExist):
					switch options.onNotExist {
					case onNotExistError:
						return err
					case onNotExistIgnore:
					case onNotExistAdd:
						if _, ok := destAbsPathInfos[destAbsPath]; !ok {
							destAbsPathInfos[destAbsPath] = nil
						}
					}
					return nil
				case err != nil:
					return err
				}

				targetRelPath, err := c.targetRelPath(destAbsPath)
				if err != nil {
					return err
				}
				if sourceState.Ignore(targetRelPath) {
					options.onIgnoreFunc(targetRelPath)
					if fileInfo.IsDir() {
						return fs.SkipDir
					}
					return nil
				}

				if options.follow && fileInfo.Mode().Type() == fs.ModeSymlink {
					fileInfo, err = c.destSystem.Stat(destAbsPath)
					if err != nil {
						return err
					}
				}

				return sourceState.AddDestAbsPathInfos(destAbsPathInfos, c.destSystem, destAbsPath, fileInfo)
			}
			if err := chezmoi.Walk(c.destSystem, destAbsPath, walkFunc); err != nil {
				return nil, err
			}
		} else {
			var fileInfo fs.FileInfo
			if options.follow {
				fileInfo, err = c.destSystem.Stat(destAbsPath)
			} else {
				fileInfo, err = c.destSystem.Lstat(destAbsPath)
			}
			switch {
			case errors.Is(err, fs.ErrNotExist):
				switch options.onNotExist {
				case onNotExistError:
					return nil, err
				case onNotExistIgnore:
				case onNotExistAdd:
					if _, ok := destAbsPathInfos[destAbsPath]; !ok {
						destAbsPathInfos[destAbsPath] = nil
					}
				}
				continue
			case err != nil:
				return nil, err
			}
			if err := sourceState.AddDestAbsPathInfos(destAbsPathInfos, c.destSystem, destAbsPath, fileInfo); err != nil {
				return nil, err
			}
		}
	}
	return destAbsPathInfos, nil
}

// diffFile outputs the diff between fromAbsPath, fromData and fromMode and
// toAbsPath, toData and toMode at relPath.
func (c *Config) diffFile(
	relPath chezmoi.RelPath,
	fromAbsPath chezmoi.AbsPath, fromData []byte, fromMode fs.FileMode,
	toAbsPath chezmoi.AbsPath, toData []byte, toMode fs.FileMode,
) error {
	if c.useBuiltinDiff || c.Diff.Command == "" {
		return c.builtinDiffFile(relPath, fromData, fromMode, toData, toMode)
	}
	return c.externalDiffFile(
		relPath,
		fromAbsPath, fromData, fromMode,
		toAbsPath, toData, toMode,
	)
}

// editor returns the path to the user's editor and any extra arguments.
func (c *Config) editor(args []string) (string, []string, error) {
	editCommand := c.Edit.Command
	editArgs := c.Edit.Args

	// If the user has set an edit command then use it.
	if editCommand != "" {
		return editCommand, append(editArgs, args...), nil
	}

	// Prefer $VISUAL over $EDITOR and fallback to the OS's default editor.
	editCommand = cmp.Or(os.Getenv("VISUAL"), os.Getenv("EDITOR"), defaultEditor)

	return parseCommand(editCommand, append(editArgs, args...))
}

// errorf writes an error to stderr.
func (c *Config) errorf(format string, args ...any) {
	_, _ = fmt.Fprintf(c.stderr, "chezmoi: "+format, args...)
}

// execute creates a new root command and executes it with args.
func (c *Config) execute(args []string) error {
	rootCmd, err := c.newRootCmd()
	if err != nil {
		return err
	}
	defer c.finalize()

	rootCmd.SetArgs(args)

	return rootCmd.Execute()
}

func (c *Config) externalDiffFile(
	relPath chezmoi.RelPath,
	fromAbsPath chezmoi.AbsPath, fromData []byte, fromMode fs.FileMode,
	toAbsPath chezmoi.AbsPath, toData []byte, toMode fs.FileMode,
) error {
	if fromAbsPath.IsEmpty() {
		fromTempDir, err := c.tempDir("chezmoi-diff-from")
		if err != nil {
			return err
		}
		fromAbsPath = fromTempDir.Join(relPath)
		if err := os.MkdirAll(fromAbsPath.Dir().String(), 0o700); err != nil {
			return err
		}
		if err := os.WriteFile(fromAbsPath.String(), fromData, fromMode); err != nil {
			return err
		}
	}

	if toAbsPath.IsEmpty() {
		toTempDir, err := c.tempDir("chezmoi-diff-to")
		if err != nil {
			return err
		}
		toAbsPath = toTempDir.Join(relPath)
		if err := os.MkdirAll(toAbsPath.Dir().String(), 0o700); err != nil {
			return err
		}
		if err := os.WriteFile(toAbsPath.String(), toData, toMode); err != nil {
			return err
		}
	}

	externalDiffSystem := c.newExternalDiffSystem(c.baseSystem)
	return externalDiffSystem.RunDiffCommand(fromAbsPath, toAbsPath)
}

// filterInput reads from args (or the standard input if args is empty),
// transforms it with f, and writes the output.
func (c *Config) filterInput(args []string, f func([]byte) ([]byte, error)) error {
	if len(args) == 0 {
		input, err := io.ReadAll(c.stdin)
		if err != nil {
			return err
		}
		output, err := f(input)
		if err != nil {
			return err
		}
		return c.writeOutput(output, 0o666)
	}

	for _, arg := range args {
		argAbsPath, err := chezmoi.NewAbsPathFromExtPath(arg, c.homeDirAbsPath)
		if err != nil {
			return err
		}
		input, err := c.baseSystem.ReadFile(argAbsPath)
		if err != nil {
			return err
		}
		output, err := f(input)
		if err != nil {
			return err
		}
		if err := c.writeOutput(output, 0o666); err != nil {
			return err
		}
	}

	return nil
}

// finalize cleans up.
func (c *Config) finalize() {
	if c.persistentState != nil {
		if err := c.persistentState.Close(); err != nil {
			c.errorf("error: failed to close persistent state: %v\n", err)
		}
	}

	// Wait for any diff pager process to terminate.
	if c.diffPagerCmd != nil {
		if err := c.diffPagerCmdStdin.Close(); err != nil {
			c.errorf("error: failed to close diff pager stdin: %v\n", err)
		}
		if c.diffPagerCmd.Process != nil {
			if err := chezmoilog.LogCmdWait(c.logger, c.diffPagerCmd); err != nil {
				c.errorf("error: failed to wait for diff pager to close: %v\n", err)
			}
		}
	}

	if c.restoreWindowsConsole != nil {
		if err := c.restoreWindowsConsole(); err != nil {
			c.errorf("error: failed to restore console: %v\n", err)
		}
	}

	// Lock Bitwarden CLI.
	if err := c.bitwardenLock(); err != nil {
		c.errorf("error: failed to lock bitwarden-cli: %v\n", err)
	}

	// Close any connection to keepassxc-cli.
	if err := c.keepassxcClose(); err != nil {
		c.errorf("error: failed to close connection to keepassxc-cli: %v\n", err)
	}
}

type configTemplate struct {
	sourceAbsPath chezmoi.AbsPath
	format        chezmoi.Format
	targetRelPath chezmoi.RelPath
	contents      []byte
}

// findConfigTemplate searches for a config template, returning the path,
// format, and contents. It returns an error if multiple config file templates
// are found.
func (c *Config) findConfigTemplate() (*configTemplate, error) {
	sourceDirAbsPath, err := c.getSourceDirAbsPath(nil)
	if err != nil {
		return nil, err
	}

	dirEntries, err := c.baseSystem.ReadDir(sourceDirAbsPath)
	switch {
	case errors.Is(err, fs.ErrNotExist):
		return nil, nil
	case err != nil:
		return nil, err
	}

	dirEntryNames := chezmoiset.NewWithCapacity[chezmoi.RelPath](len(dirEntries))
	for _, dirEntry := range dirEntries {
		dirEntryNames.Add(chezmoi.NewRelPath(dirEntry.Name()))
	}

	var configTemplates []*configTemplate //nolint:prealloc
	for _, extension := range chezmoi.FormatExtensions {
		relPath := chezmoi.NewRelPath(chezmoi.Prefix + "." + extension + chezmoi.TemplateSuffix)
		if !dirEntryNames.Contains(relPath) {
			continue
		}
		absPath := sourceDirAbsPath.Join(relPath)
		contents, err := c.baseSystem.ReadFile(absPath)
		if err != nil {
			return nil, err
		}
		configTemplate := &configTemplate{
			targetRelPath: chezmoi.NewRelPath("chezmoi." + extension),
			format:        chezmoi.FormatsByExtension[extension],
			sourceAbsPath: absPath,
			contents:      contents,
		}
		configTemplates = append(configTemplates, configTemplate)
	}

	switch len(configTemplates) {
	case 0:
		return nil, nil
	case 1:
		return configTemplates[0], nil
	default:
		sourceAbsPathStrs := make([]string, len(configTemplates))
		for i, configTemplate := range configTemplates {
			sourceAbsPathStr := configTemplate.sourceAbsPath.String()
			sourceAbsPathStrs[i] = sourceAbsPathStr
		}
		return nil, fmt.Errorf("multiple config file templates: %s ", englishList(sourceAbsPathStrs))
	}
}

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,
		},
	}
}

// gitAutoAdd adds all changes to the git index and returns the new git status.
func (c *Config) gitAutoAdd() (*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)
}

// gitAutoCommit commits all changes in the git index, including generating a
// commit message from status.
func (c *Config) gitAutoCommit(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")
}

// gitAutoPush pushes all changes to the remote if status is not empty.
func (c *Config) gitAutoPush(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")
}

// 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)
}

// 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.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.gitAutoAdd()
			if err != nil {
				return err
			}
		}
		if c.Git.AutoCommit || c.Git.AutoPush {
			if err := c.gitAutoCommit(cmd, status); err != nil {
				return err
			}
		}
		if c.Git.AutoPush {
			if err := c.gitAutoPush(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
	}
}
