diff --git a/assets/chezmoi.io/docs/reference/special-files-and-directories/chezmoiexternals.md b/assets/chezmoi.io/docs/reference/special-files-and-directories/chezmoiexternals.md new file mode 100644 index 000000000000..d3cef028856b --- /dev/null +++ b/assets/chezmoi.io/docs/reference/special-files-and-directories/chezmoiexternals.md @@ -0,0 +1,4 @@ +# `.chezmoiexternals` + +If a directory called `.chezmoiexternals` exists, then all files in this +directory are treated as `.chezmoiexternal.` files. diff --git a/assets/chezmoi.io/mkdocs.yml b/assets/chezmoi.io/mkdocs.yml index 89739d276a3a..674c56fa805b 100644 --- a/assets/chezmoi.io/mkdocs.yml +++ b/assets/chezmoi.io/mkdocs.yml @@ -119,6 +119,7 @@ nav: - .chezmoi.<format>.tmpl: reference/special-files-and-directories/chezmoi-format-tmpl.md - .chezmoidata.<format>: reference/special-files-and-directories/chezmoidata-format.md - .chezmoiexternal.<format>: reference/special-files-and-directories/chezmoiexternal-format.md + - .chezmoiexternals: reference/special-files-and-directories/chezmoiexternals.md - .chezmoiignore: reference/special-files-and-directories/chezmoiignore.md - .chezmoiremove: reference/special-files-and-directories/chezmoiremove.md - .chezmoiroot: reference/special-files-and-directories/chezmoiroot.md diff --git a/pkg/chezmoi/chezmoi.go b/pkg/chezmoi/chezmoi.go index f7972da52ba9..900aae192ed2 100644 --- a/pkg/chezmoi/chezmoi.go +++ b/pkg/chezmoi/chezmoi.go @@ -75,6 +75,7 @@ const ( VersionName = Prefix + "version" dataName = Prefix + "data" externalName = Prefix + "external" + externalsDirName = Prefix + "externals" ignoreName = Prefix + "ignore" removeName = Prefix + "remove" scriptsDirName = Prefix + "scripts" @@ -115,6 +116,7 @@ var knownPrefixedFiles = newSet( var knownPrefixedDirs = newSet( TemplatesDirName, dataName, + externalsDirName, scriptsDirName, ) diff --git a/pkg/chezmoi/sourcestate.go b/pkg/chezmoi/sourcestate.go index f2b7b4c48d4f..bc66bac8cc42 100644 --- a/pkg/chezmoi/sourcestate.go +++ b/pkg/chezmoi/sourcestate.go @@ -940,7 +940,13 @@ func (s *SourceState) Read(ctx context.Context, options *ReadOptions) error { case s.templateDataOnly: return nil case isPrefixDotFormat(fileInfo.Name(), externalName) || isPrefixDotFormatDotTmpl(fileInfo.Name(), externalName): - return s.addExternal(sourceAbsPath) + parentAbsPath, _ := sourceAbsPath.Split() + return s.addExternal(sourceAbsPath, parentAbsPath) + case fileInfo.Name() == externalsDirName: + if err := s.addExternalDir(ctx, sourceAbsPath); err != nil { + return err + } + return vfs.SkipDir case fileInfo.Name() == ignoreName || fileInfo.Name() == ignoreName+TemplateSuffix: return s.addPatterns(s.ignore, sourceAbsPath, parentSourceRelPath) case fileInfo.Name() == removeName || fileInfo.Name() == removeName+TemplateSuffix: @@ -1287,9 +1293,8 @@ func (s *SourceState) TemplateData() map[string]any { } // addExternal adds external source entries to s. -func (s *SourceState) addExternal(sourceAbsPath AbsPath) error { - parentAbsPath, _ := sourceAbsPath.Split() - +func (s *SourceState) addExternal(sourceAbsPath, parentAbsPath AbsPath) error { + fmt.Fprintf(os.Stderr, "addExternal sourceAbsPath=%s parentAbsPath=%s\n", sourceAbsPath, parentAbsPath) parentRelPath, err := parentAbsPath.TrimDirPrefix(s.sourceDirAbsPath) if err != nil { return err @@ -1325,6 +1330,41 @@ func (s *SourceState) addExternal(sourceAbsPath AbsPath) error { return nil } +// addExternalDir adds all externals in externalsDirAbsPath to s. +func (s *SourceState) addExternalDir(ctx context.Context, externalsDirAbsPath AbsPath) error { + walkFunc := func(ctx context.Context, externalAbsPath AbsPath, fileInfo fs.FileInfo, err error) error { + if externalAbsPath == externalsDirAbsPath { + return nil + } + if err == nil && fileInfo.Mode().Type() == fs.ModeSymlink { + fileInfo, err = s.system.Stat(externalAbsPath) + } + switch { + case err != nil: + return err + case strings.HasPrefix(fileInfo.Name(), Prefix): + return fmt.Errorf("%s: not allowed in %s directory", externalAbsPath, externalsDirName) + case strings.HasPrefix(fileInfo.Name(), ignorePrefix): + if fileInfo.IsDir() { + return vfs.SkipDir + } + return nil + case fileInfo.Mode().IsRegular(): + parentAbsPath, _ := externalAbsPath.Split() + fmt.Fprintf(os.Stderr, "before addExternal parentAbsPath=%s\n", parentAbsPath) + return s.addExternal(externalAbsPath, parentAbsPath.TrimSuffix("/").Dir()) + case fileInfo.IsDir(): + return nil + default: + return &unsupportedFileTypeError{ + absPath: externalAbsPath, + mode: fileInfo.Mode(), + } + } + } + return concurrentWalkSourceDir(ctx, s.system, externalsDirAbsPath, walkFunc) +} + // addPatterns executes the template at sourceAbsPath, interprets the result as // a list of patterns, and adds all patterns found to patternSet. func (s *SourceState) addPatterns( diff --git a/pkg/cmd/testdata/scripts/external.txtar b/pkg/cmd/testdata/scripts/external.txtar index 8b1360c34a3c..b04383b090f1 100644 --- a/pkg/cmd/testdata/scripts/external.txtar +++ b/pkg/cmd/testdata/scripts/external.txtar @@ -110,6 +110,12 @@ exec chezmoi apply [umask:002] cmpmod 775 $HOME/.file [umask:022] cmpmod 755 $HOME/.file +chhome home14/user + +# test that chezmoi reads external files from the .chezmoiexternal directory +exec chezmoi apply +cmp $HOME/.file golden/dir/file + -- archive/dir/file -- # contents of dir/file -- golden/.file -- @@ -156,6 +162,12 @@ exec chezmoi apply path = "dir/file" executable = true stripComponents = 1 +-- home14/user/.local/share/chezmoi/.chezmoiexternals/external.toml -- +[".file"] + type = "archive-file" + url = "{{ env "HTTPD_URL" }}/archive.tar.gz" + path = "dir/file" + stripComponents = 1 -- home2/user/.local/share/chezmoi/.chezmoiexternal.toml -- [".file"] type = "file"