From 9e77ea56e0df88e8b53ea2bca86276b331bd77c1 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Fri, 3 Sep 2021 22:30:20 +0200 Subject: [PATCH 1/2] Add remove_ attribute --- docs/REFERENCE.md | 9 +++++++++ internal/chezmoi/attr.go | 6 ++++++ internal/chezmoi/attr_test.go | 7 +++++++ internal/chezmoi/chezmoi.go | 3 ++- internal/chezmoi/sourcestate.go | 10 ++++++++++ internal/cmd/testdata/scripts/remove.txt | 15 +++++++++++++++ 6 files changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 496afae12cc..e3af2e3af84 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -424,6 +424,7 @@ to as "attributes": | `modify_` | Treat the contents as a script that modifies an existing file. | | `once_` | Run script once. | | `private_` | Remove all group and world permissions from the target file or directory. | +| `remove_` | Remove the entry if it exists. | | `run_` | Treat the contents as a script to run. | | `symlink_` | Create a symlink instead of a regular file. | @@ -441,6 +442,7 @@ prefixes is important. | Regular file | File | `encrypted_`, `private_`, `executable_`, `dot_` | `.tmpl` | | Create file | File | `create_`, `encrypted_`, `private_`, `executable_`, `dot_` | `.tmpl` | | Modify file | File | `modify_`, `encrypted_`, `private_`, `executable_`, `dot_` | `.tmpl` | +| Remove | File | `remove_`, `dot_` | *none* | | Script | File | `run_`, `once_`, `before_` or `after_` | `.tmpl` | | Symbolic link | File | `symlink_`, `dot_`, | `.tmpl` | @@ -495,6 +497,13 @@ new contents are read from the scripts standard output. --- +#### Remove entry + +Files with the `remove_` prefix will cause the corresponding entry (file, +directory, or symlink) to be removed in the target state. + +--- + ### Directories Directories are represented by regular directories in the source state. The diff --git a/internal/chezmoi/attr.go b/internal/chezmoi/attr.go index 4f35a562959..ef72942db84 100644 --- a/internal/chezmoi/attr.go +++ b/internal/chezmoi/attr.go @@ -15,6 +15,7 @@ const ( SourceFileTypeCreate SourceFileTargetType = iota SourceFileTypeFile SourceFileTypeModify + SourceFileTypeRemove SourceFileTypeScript SourceFileTypeSymlink ) @@ -125,6 +126,9 @@ func parseFileAttr(sourceName, encryptedSuffix string) FileAttr { name = mustTrimPrefix(name, executablePrefix) executable = true } + case strings.HasPrefix(name, removePrefix): + sourceFileType = SourceFileTypeRemove + name = mustTrimPrefix(name, removePrefix) case strings.HasPrefix(name, runPrefix): sourceFileType = SourceFileTypeScript name = mustTrimPrefix(name, runPrefix) @@ -240,6 +244,8 @@ func (fa FileAttr) SourceName(encryptedSuffix string) string { if fa.Executable { sourceName += executablePrefix } + case SourceFileTypeRemove: + sourceName = removePrefix case SourceFileTypeScript: sourceName = runPrefix if fa.Once { diff --git a/internal/chezmoi/attr_test.go b/internal/chezmoi/attr_test.go index c9eba98af1c..3b3b3ecf44f 100644 --- a/internal/chezmoi/attr_test.go +++ b/internal/chezmoi/attr_test.go @@ -133,6 +133,13 @@ func TestFileAttr(t *testing.T) { Private: []bool{false, true}, Template: []bool{false, true}, })) + require.NoError(t, combinator.Generate(&fas, struct { + Type SourceFileTargetType + TargetName []string + }{ + Type: SourceFileTypeRemove, + TargetName: targetNames, + })) require.NoError(t, combinator.Generate(&fas, struct { Type SourceFileTargetType TargetName []string diff --git a/internal/chezmoi/chezmoi.go b/internal/chezmoi/chezmoi.go index d05bca05345..8500fc184d2 100644 --- a/internal/chezmoi/chezmoi.go +++ b/internal/chezmoi/chezmoi.go @@ -37,6 +37,7 @@ const ( modifyPrefix = "modify_" oncePrefix = "once_" privatePrefix = "private_" + removePrefix = "remove_" runPrefix = "run_" symlinkPrefix = "symlink_" literalSuffix = ".literal" @@ -57,7 +58,7 @@ const ( var ( dirPrefixRegexp = regexp.MustCompile(`\A(dot|exact|literal|private)_`) - filePrefixRegexp = regexp.MustCompile(`\A(after|before|create|dot|empty|encrypted|executable|literal|modify|once|private|run|symlink)_`) + filePrefixRegexp = regexp.MustCompile(`\A(after|before|create|dot|empty|encrypted|executable|literal|modify|once|private|remove|run|symlink)_`) fileSuffixRegexp = regexp.MustCompile(`\.(literal|tmpl)\z`) ) diff --git a/internal/chezmoi/sourcestate.go b/internal/chezmoi/sourcestate.go index 3f4c89f4365..c45c9f68395 100644 --- a/internal/chezmoi/sourcestate.go +++ b/internal/chezmoi/sourcestate.go @@ -1230,6 +1230,14 @@ func (s *SourceState) newModifyTargetStateEntryFunc(sourceRelPath SourceRelPath, } } +// newRemoveTargetStateEntryFunc returns a targetStateEntryFunc that removes a +// target. +func (s *SourceState) newRemoveTargetStateEntryFunc(sourceRelPath SourceRelPath, fileAttr FileAttr) targetStateEntryFunc { + return func(destSystem System, destAbsPath AbsPath) (TargetStateEntry, error) { + return &TargetStateRemove{}, nil + } +} + // newScriptTargetStateEntryFunc returns a targetStateEntryFunc that returns a // script with sourceLazyContents. func (s *SourceState) newScriptTargetStateEntryFunc(sourceRelPath SourceRelPath, fileAttr FileAttr, targetRelPath RelPath, sourceLazyContents *lazyContents, interpreter *Interpreter) targetStateEntryFunc { @@ -1314,6 +1322,8 @@ func (s *SourceState) newSourceStateFile(sourceRelPath SourceRelPath, fileAttr F targetRelPath = targetRelPath[:len(targetRelPath)-len(ext)-1] } targetStateEntryFunc = s.newModifyTargetStateEntryFunc(sourceRelPath, fileAttr, sourceLazyContents, interpreter) + case SourceFileTypeRemove: + targetStateEntryFunc = s.newRemoveTargetStateEntryFunc(sourceRelPath, fileAttr) case SourceFileTypeScript: // If the script has an extension, determine if it indicates an // interpreter to use. diff --git a/internal/cmd/testdata/scripts/remove.txt b/internal/cmd/testdata/scripts/remove.txt index 4bd77253ff2..8ffc97bbe91 100644 --- a/internal/cmd/testdata/scripts/remove.txt +++ b/internal/cmd/testdata/scripts/remove.txt @@ -19,3 +19,18 @@ exists $HOME/.executable ! chezmoi remove --force $HOME${/}.newfile $HOME${/}.executable stderr 'not in source state' exists $HOME/.executable + +chhome home2/user + +# test that chezmoi apply removes a file and a directory +exists $HOME/.file +exists $HOME/.dir +chezmoi apply +! exists $HOME/.file +! exists $HOME/.dir + +-- home2/user/.dir/.keep -- +-- home2/user/.file -- +# contents of .file +-- home2/user/.local/share/chezmoi/remove_dot_file -- +-- home2/user/.local/share/chezmoi/remove_dot_dir -- From 767e834771095ba810add671daa8c8c26d52254f Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Sat, 4 Sep 2021 20:10:44 +0200 Subject: [PATCH 2/2] Remove accidentally unimplemented --remove option --- docs/HOWTO.md | 15 +++++---------- docs/REFERENCE.md | 6 ------ internal/cmd/config.go | 3 --- internal/cmd/testdata/scripts/applyremove.txt | 12 ++++++------ 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/docs/HOWTO.md b/docs/HOWTO.md index 5e261f56fca..75ff861e2c9 100644 --- a/docs/HOWTO.md +++ b/docs/HOWTO.md @@ -250,18 +250,13 @@ chezmoi automatically creates `.keep` files when you add an empty directory with ### Ensure that a target is removed Create a file called `.chezmoiremove` in the source directory containing a list -of patterns of files to remove. When you run +of patterns of files to remove. chezmoi will remove anything in the target +directory that matches the pattern. As this command is potentially dangerous, +you should run chezmoi in verbose, dry-run mode beforehand to see what would be +removed: ```console -$ chezmoi apply --remove -``` - -chezmoi will remove anything in the target directory that matches the pattern. -As this command is potentially dangerous, you should run chezmoi in verbose, -dry-run mode beforehand to see what would be removed: - -```console -$ chezmoi apply --remove --dry-run --verbose +$ chezmoi apply --dry-run --verbose ``` `.chezmoiremove` is interpreted as a template, so you can remove different files diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index e3af2e3af84..ff895c6b082 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -18,7 +18,6 @@ Manage your dotfiles across multiple machines, securely. * [`--no-tty`](#--no-tty) * [`-o`, `--output` *filename*](#-o---output-filename) * [`-R`, `--refresh-externals`](#-r---refresh-externals) - * [`-r`. `--remove`](#-r---remove) * [`-S`, `--source` *directory*](#-s---source-directory) * [`--use-builtin-git` *value*](#--use-builtin-git-value) * [`-v`, `--verbose`](#-v---verbose) @@ -213,10 +212,6 @@ Write the output to *filename* instead of stdout. Refresh externals cache. See `.chezmoiexternal.`. -### `-r`. `--remove` - -Also remove targets according to `.chezmoiremove`. - ### `-S`, `--source` *directory* Use *directory* as the source directory. @@ -313,7 +308,6 @@ The following configuration variables are available: | | `encryption` | string | *none* | Encryption tool, either `age` or `gpg` | | | `format` | string | `json` | Format for data output, either `json` or `yaml` | | | `mode` | string | `file` | Mode in target dir, either `file` or `symlink` | -| | `remove` | bool | `false` | Remove targets | | | `sourceDir` | string | `~/.local/share/chezmoi` | Source directory | | | `pager` | string | `$PAGER` | Default pager | | | `umask` | int | *from system* | Umask | diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 41cee68d2e9..802488f57bd 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -62,7 +62,6 @@ type Config struct { Mode chezmoi.Mode `mapstructure:"mode"` Pager string `mapstructure:"pager"` Safe bool `mapstructure:"safe"` - Remove bool `mapstructure:"remove"` SourceDirAbsPath chezmoi.AbsPath `mapstructure:"sourceDir"` Template templateConfig `mapstructure:"template"` Umask fs.FileMode `mapstructure:"umask"` @@ -1033,7 +1032,6 @@ func (c *Config) newRootCmd() (*cobra.Command, error) { persistentFlags.Var(&c.Color, "color", "Colorize output") persistentFlags.VarP(&c.DestDirAbsPath, "destination", "D", "Set destination directory") persistentFlags.BoolVar(&c.Safe, "safe", c.Safe, "Safely replace files and symlinks") - persistentFlags.BoolVar(&c.Remove, "remove", c.Remove, "Remove entries from destination directory") persistentFlags.VarP(&c.SourceDirAbsPath, "source", "S", "Set source directory") persistentFlags.Var(&c.Mode, "mode", "Mode") persistentFlags.Var(&c.UseBuiltinGit, "use-builtin-git", "Use builtin git") @@ -1041,7 +1039,6 @@ func (c *Config) newRootCmd() (*cobra.Command, error) { "color", "destination", "mode", - "remove", "source", } { if err := viper.BindPFlag(key, persistentFlags.Lookup(key)); err != nil { diff --git a/internal/cmd/testdata/scripts/applyremove.txt b/internal/cmd/testdata/scripts/applyremove.txt index 9af27848d48..4000901baf2 100644 --- a/internal/cmd/testdata/scripts/applyremove.txt +++ b/internal/cmd/testdata/scripts/applyremove.txt @@ -1,17 +1,17 @@ -# test that chezmoi apply --dry-run --remove does not remove entries -chezmoi apply --dry-run --force --remove +# test that chezmoi apply --dry-run does not remove entries +chezmoi apply --dry-run --force exists $HOME/.dir/file exists $HOME/.file1 exists $HOME/.file2 -# test that chezmoi apply --remove file removes only file -chezmoi apply --force --remove $HOME${/}.file1 +# test that chezmoi apply file removes only file +chezmoi apply --force $HOME${/}.file1 exists $HOME/.dir/file ! exists $HOME/.file1 exists $HOME/.file2 -# test that chezmoi apply --remove removes all entries -chezmoi apply --force --remove +# test that chezmoi apply removes all entries +chezmoi apply --force ! exists $HOME/.dir/file ! exists $HOME/.file1 ! exists $HOME/.file2