diff --git a/internal/chezmoi/sourcestate.go b/internal/chezmoi/sourcestate.go index 65aa01f0926..883cd0d568c 100644 --- a/internal/chezmoi/sourcestate.go +++ b/internal/chezmoi/sourcestate.go @@ -313,20 +313,21 @@ type ReplaceFunc func(targetRelPath RelPath, newSourceStateEntry, oldSourceState // AddOptions are options to SourceState.Add. type AddOptions struct { - AutoTemplate bool // Automatically create templates, if possible. - Create bool // Add create_ entries instead of normal entries. - Encrypt bool // Encrypt files. - EncryptedSuffix string // Suffix for encrypted files. - Exact bool // Add the exact_ attribute to added directories. - Filter *EntryTypeFilter // Entry type filter. - OnIgnoreFunc func(RelPath) // Function to call when a target is ignored. - PreAddFunc PreAddFunc // Function to be called before a source entry is added. - ConfigFileAbsPath AbsPath // Path to config file. - ProtectedAbsPaths []AbsPath // Paths that must not be added. - RemoveDir RelPath // Directory to remove before adding. - ReplaceFunc ReplaceFunc // Function to be called before a source entry is replaced. - Template bool // Add the .tmpl attribute to added files. - TemplateSymlinks bool // Add symlinks with targets in the source or home directories as templates. + AutoTemplate bool // Automatically create templates, if possible. + Create bool // Add create_ entries instead of normal entries. + Encrypt bool // Encrypt files. + EncryptedSuffix string // Suffix for encrypted files. + Errorf func(string, ...any) // Function to print errors. + Exact bool // Add the exact_ attribute to added directories. + Filter *EntryTypeFilter // Entry type filter. + OnIgnoreFunc func(RelPath) // Function to call when a target is ignored. + PreAddFunc PreAddFunc // Function to be called before a source entry is added. + ConfigFileAbsPath AbsPath // Path to config file. + ProtectedAbsPaths []AbsPath // Paths that must not be added. + RemoveDir RelPath // Directory to remove before adding. + ReplaceFunc ReplaceFunc // Function to be called before a source entry is replaced. + Template bool // Add the .tmpl attribute to added files. + TemplateSymlinks bool // Add symlinks with targets in the source or home directories as templates. } // Add adds destAbsPathInfos to s. @@ -389,11 +390,19 @@ func (s *SourceState) Add( newSourceStateEntries := make(map[SourceRelPath]SourceStateEntry) newSourceStateEntriesByTargetRelPath := make(map[RelPath]SourceStateEntry) nonEmptyDirs := make(map[SourceRelPath]struct{}) + externalDirRelPaths := make(map[RelPath]struct{}) dirRenames := make(map[AbsPath]AbsPath) DEST_ABS_PATH: for _, destAbsPath := range destAbsPaths { targetRelPath := destAbsPath.MustTrimDirPrefix(s.destDirAbsPath) + // Skip any entries in known external dirs. + for externalDir := range externalDirRelPaths { + if targetRelPath.HasDirPrefix(externalDir) { + continue DEST_ABS_PATH + } + } + // Find the target's parent directory in the source state. var parentSourceRelPath SourceRelPath if targetParentRelPath := targetRelPath.Dir(); targetParentRelPath == DotRelPath { @@ -418,7 +427,13 @@ DEST_ABS_PATH: case i != len(nodes)-1 && !ok: panic(fmt.Errorf("nodes[%d]: unexpected non-terminal source state entry, got %T", i, node.sourceStateEntry)) case ok && sourceStateDir.Attr.External: - return fmt.Errorf("%s: cannot add entry in external_ directory", destAbsPath) + targetRelPathComponents := targetRelPath.SplitAll() + externalDirRelPath := EmptyRelPath.Join(targetRelPathComponents[:i]...) + externalDirRelPaths[externalDirRelPath] = struct{}{} + if options.Errorf != nil { + options.Errorf("%s: skipping entries in external_ directory\n", externalDirRelPath) + } + continue DEST_ABS_PATH } } parentSourceRelPath = nodes[len(nodes)-1].sourceStateEntry.SourceRelPath() diff --git a/internal/cmd/addcmd.go b/internal/cmd/addcmd.go index 0ee7871ef7a..480518416a5 100644 --- a/internal/cmd/addcmd.go +++ b/internal/cmd/addcmd.go @@ -187,6 +187,7 @@ func (c *Config) runAddCmd(cmd *cobra.Command, args []string, sourceState *chezm Encrypt: c.Add.Encrypt, EncryptedSuffix: c.encryption.EncryptedSuffix(), Exact: c.Add.exact, + Errorf: c.errorf, Filter: c.Add.filter, OnIgnoreFunc: c.defaultOnIgnoreFunc, PreAddFunc: c.defaultPreAddFunc, diff --git a/internal/cmd/importcmd.go b/internal/cmd/importcmd.go index 561b6c3ec86..a5dd3309b2e 100644 --- a/internal/cmd/importcmd.go +++ b/internal/cmd/importcmd.go @@ -88,6 +88,7 @@ func (c *Config) runImportCmd(cmd *cobra.Command, args []string, sourceState *ch archiveReaderSystem, archiveReaderSystem.FileInfos(), &chezmoi.AddOptions{ + Errorf: c.errorf, Exact: c._import.exact, Filter: c._import.filter, RemoveDir: removeDir, diff --git a/internal/cmd/mackupcmd_darwin.go b/internal/cmd/mackupcmd_darwin.go index 6effbf13fae..0e0ed986a82 100644 --- a/internal/cmd/mackupcmd_darwin.go +++ b/internal/cmd/mackupcmd_darwin.go @@ -109,6 +109,7 @@ func (c *Config) runMackupAddCmd(cmd *cobra.Command, args []string, sourceState c.destSystem, destAbsPathInfos, &chezmoi.AddOptions{ + Errorf: c.errorf, Filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone), OnIgnoreFunc: c.defaultOnIgnoreFunc, PreAddFunc: c.defaultPreAddFunc, diff --git a/internal/cmd/readdcmd.go b/internal/cmd/readdcmd.go index 544d1b9c1e0..edf0e365439 100644 --- a/internal/cmd/readdcmd.go +++ b/internal/cmd/readdcmd.go @@ -146,6 +146,7 @@ TARGET_REL_PATH: if err := sourceState.Add(c.sourceSystem, c.persistentState, c.destSystem, destAbsPathInfos, &chezmoi.AddOptions{ Encrypt: sourceStateFile.Attr.Encrypted, EncryptedSuffix: c.encryption.EncryptedSuffix(), + Errorf: c.errorf, Filter: c.reAdd.filter, }); err != nil { return err diff --git a/internal/cmd/testdata/scripts/issue3525.txtar b/internal/cmd/testdata/scripts/issue3525.txtar index 631b8033f38..150dcc4d9c6 100644 --- a/internal/cmd/testdata/scripts/issue3525.txtar +++ b/internal/cmd/testdata/scripts/issue3525.txtar @@ -1,10 +1,10 @@ # test that chezmoi add does not add files in external_ directories -! exec chezmoi add $HOME${/}.external/file -stderr 'cannot add entry in external_ directory' +exec chezmoi add $HOME${/}.external/file +stderr '.external: skipping entries in external_ directory' # test that chezmoi add does not add files in subdirectories of external_ directories -! exec chezmoi add $HOME${/}.external/dir/file -stderr 'cannot add entry in external_ directory' +exec chezmoi add $HOME${/}.external/dir/file +stderr '.external: skipping entries in external_ directory' -- home/user/.external/dir/file -- # contents of .external/dir/file diff --git a/internal/cmd/testdata/scripts/issue3652.txtar b/internal/cmd/testdata/scripts/issue3652.txtar new file mode 100644 index 00000000000..5207e2c158c --- /dev/null +++ b/internal/cmd/testdata/scripts/issue3652.txtar @@ -0,0 +1,12 @@ +# test that chezmoi add skips files in external_ directories +exec chezmoi apply +exists $HOME/.dir/submodule/.git/.keep +exec chezmoi add $HOME${/}.dir +stderr '.dir/submodule: skipping entries in external_ directory' +cmp $CHEZMOISOURCEDIR/dot_dir/file golden/file + +-- golden/file -- +# contents of file +-- home/user/.dir/file -- +# contents of file +-- home/user/.local/share/chezmoi/dot_dir/external_submodule/.git/.keep --