diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0377ccf7bac5..528b60d2e552 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,8 +1,10 @@ +name: Test and release on: pull_request: push: jobs: test: + name: Test strategy: matrix: go-version: @@ -38,7 +40,7 @@ jobs: - name: Run run: go run . --version - name: Test - run: go test ./... # FIXME add -race when https://github.com/etcd-io/bbolt/issues/187 is fixed + run: go test -race ./... - name: Lint if: matrix.os == 'ubuntu-latest' run: $(go env GOPATH)/bin/golangci-lint run @@ -57,6 +59,7 @@ jobs: - name: Test release if: matrix.os == 'ubuntu-latest' run: | + export PATH=$PATH:/snap/bin sudo chown root:root / sudo snap install goreleaser --classic sudo snap install snapcraft --classic @@ -67,6 +70,7 @@ jobs: ./dist/chezmoi-nocgo_linux_386/chezmoi --version | tee /dev/stderr | grep -q "chezmoi version v" ./dist/chezmoi-nocgo-snap_linux_386/chezmoi --version | tee /dev/stderr | grep -q "chezmoi version v" release: + name: Release if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') needs: - test @@ -84,6 +88,7 @@ jobs: env: SNAPCRAFT_LOGIN: ${{ secrets.SNAPCRAFT_LOGIN }} run: | + export PATH=$PATH:/snap/bin sudo snap install snapcraft --classic sudo chown root:root / echo ${SNAPCRAFT_LOGIN} | snapcraft login --with - @@ -91,5 +96,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} run: | + export PATH=$PATH:/snap/bin sudo snap install goreleaser --classic goreleaser release \ No newline at end of file diff --git a/README.md b/README.md index fc4f90f91fa3..eef9f74aa1fa 100644 --- a/README.md +++ b/README.md @@ -135,17 +135,19 @@ Then you've probably run into at least one of the following problems: system, chezmoi already has a tried-and-tested solution ready to use. * If your system is written in a scripting language like Python, Perl, or Ruby, - then you also need ensure that that language's runtime is installed. chezmoi - is distributed as a single stand-alone statically-linked binary with no - dependencies that you can simply copy onto your machine and run. chezmoi - provides one-line installs, pre-built binaries, packages for Linux and BSD - distributions, Homebrew formulae, Scoop support on Windows, and a initial - config file generation mechanism to make installing your dotfiles on a new - machine as painless as possible. + then you also need to install that language's runtime before you can use your + system. chezmoi is distributed as a single stand-alone statically-linked + binary with no dependencies that you can simply copy onto your machine and + run. chezmoi provides one-line installs, pre-built binaries, packages for + Linux and BSD distributions, Homebrew formulae, Scoop support on Windows, and + a initial config file generation mechanism to make installing your dotfiles on + a new machine as painless as possible. ## What does a chezmoi dotfile repo look like? -Have a look at [repos tagged with `chezmoi` on GitHub](https://github.com/topics/chezmoi). +Have a look at [repos tagged with `chezmoi` on +GitHub](https://github.com/topics/chezmoi?o=desc&s=updated). You can also read +what [has been written about chezmoi](docs/MEDIA.md). ## How do I start with chezmoi? diff --git a/chezmoi.io/.gitignore b/chezmoi.io/.gitignore index dad94529bf68..85516b921e25 100644 --- a/chezmoi.io/.gitignore +++ b/chezmoi.io/.gitignore @@ -3,6 +3,7 @@ /content/docs/faq.md /content/docs/how-to.md /content/docs/install.md +/content/docs/media.md /content/docs/quick-start.md /content/docs/reference.md /public diff --git a/chezmoi.io/Makefile b/chezmoi.io/Makefile index 759febde2e2c..1a4edfc0c134 100644 --- a/chezmoi.io/Makefile +++ b/chezmoi.io/Makefile @@ -9,6 +9,7 @@ content-docs: \ content/docs/faq.md \ content/docs/how-to.md \ content/docs/install.md \ + content/docs/media.md \ content/docs/quick-start.md \ content/docs/reference.md @@ -27,6 +28,9 @@ content/docs/how-to.md: ../docs/HOWTO.md ../internal/generate-chezmoi.io-content content/docs/install.md: ../docs/INSTALL.md ../internal/generate-chezmoi.io-content-docs/main.go Makefile go run ../internal/generate-chezmoi.io-content-docs -shorttitle="Install" -longtitle="Install Guide" < $< > $@ || ( rm -f $@ ; false ) +content/docs/media.md: ../docs/MEDIA.md ../internal/generate-chezmoi.io-content-docs/main.go Makefile + go run ../internal/generate-chezmoi.io-content-docs -shorttitle="Media" -longtitle="Media" < $< > $@ || ( rm -f $@ ; false ) + content/docs/quick-start.md: ../docs/QUICKSTART.md ../internal/generate-chezmoi.io-content-docs/main.go Makefile go run ../internal/generate-chezmoi.io-content-docs -shorttitle="Quick Start" -longtitle="Quick Start Guide" < $< > $@ || ( rm -f $@ ; false ) diff --git a/chezmoi.io/content/_index.md b/chezmoi.io/content/_index.md index 801dc7bcb36b..ac804cc181d7 100644 --- a/chezmoi.io/content/_index.md +++ b/chezmoi.io/content/_index.md @@ -118,17 +118,19 @@ Then you've probably run into at least one of the following problems: system, chezmoi already has a tried-and-tested solution ready to use. * If your system is written in a scripting language like Python, Perl, or Ruby, - then you also need ensure that that language's runtime is installed. chezmoi - is distributed as a single stand-alone statically-linked binary with no - dependencies that you can simply copy onto your machine and run. chezmoi - provides one-line installs, pre-built binaries, packages for Linux and BSD - distributions, Homebrew formulae, Scoop support on Windows, and a initial - config file generation mechanism to make installing your dotfiles on a new - machine as painless as possible. + then you also need to install that language's runtime before you can use your + system. chezmoi is distributed as a single stand-alone statically-linked + binary with no dependencies that you can simply copy onto your machine and + run. chezmoi provides one-line installs, pre-built binaries, packages for + Linux and BSD distributions, Homebrew formulae, Scoop support on Windows, and + a initial config file generation mechanism to make installing your dotfiles on + a new machine as painless as possible. ## What does a chezmoi dotfile repo look like? -Have a look at [repos tagged with `chezmoi` on GitHub](https://github.com/topics/chezmoi). +Have a look at [repos tagged with `chezmoi` on +GitHub](https://github.com/topics/chezmoi?o=desc&s=updated). You can also read +what [has been written about chezmoi](/docs/media/). ## How do I start with chezmoi? diff --git a/chezmoi.io/content/docs/menu/index.md b/chezmoi.io/content/docs/menu/index.md index 708f4fcd827c..4031f78bdd06 100644 --- a/chezmoi.io/content/docs/menu/index.md +++ b/chezmoi.io/content/docs/menu/index.md @@ -8,5 +8,6 @@ headless = true - [FAQ]({{< relref "/docs/faq.md" >}}) - [Changes]({{< relref "/docs/changes.md" >}}) - [Reference]({{< relref "/docs/reference.md" >}}) +- [Media]({{< relref "/docs/media.md" >}}) - [Contributing]({{< relref "/docs/contributing.md" >}}) - [GitHub](https://github.com/twpayne/chezmoi) \ No newline at end of file diff --git a/cmd/add.go b/cmd/add.go index 35d5d4587db8..32cf10be7bed 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -24,10 +24,9 @@ var addCmd = &cobra.Command{ } type addCmdConfig struct { - force bool - recursive bool - prompt bool - options chezmoi.AddOptions + force bool + prompt bool + options chezmoi.AddOptions } func init() { @@ -39,7 +38,7 @@ func init() { persistentFlags.BoolVarP(&config.add.force, "force", "f", false, "overwrite source state, even if template would be lost") persistentFlags.BoolVarP(&config.add.options.Exact, "exact", "x", false, "add directories exactly") persistentFlags.BoolVarP(&config.add.prompt, "prompt", "p", false, "prompt before adding") - persistentFlags.BoolVarP(&config.add.recursive, "recursive", "r", false, "recurse in to subdirectories") + persistentFlags.BoolVarP(&config.add.options.Recursive, "recursive", "r", false, "recurse in to subdirectories") persistentFlags.BoolVarP(&config.add.options.Template, "template", "T", false, "add files as templates") persistentFlags.BoolVarP(&config.add.options.AutoTemplate, "autotemplate", "a", false, "auto generate the template when adding files as templates") @@ -47,6 +46,11 @@ func init() { } func (c *Config) runAddCmd(cmd *cobra.Command, args []string) (err error) { + // Make --autotemplate imply --template. + if c.add.options.AutoTemplate { + c.add.options.Template = true + } + ts, err := c.getTargetState(nil) if err != nil { return err @@ -70,7 +74,7 @@ func (c *Config) runAddCmd(cmd *cobra.Command, args []string) (err error) { if err != nil { return err } - if c.add.recursive { + if c.add.options.Recursive { if err := vfs.Walk(c.fs, path, func(path string, info os.FileInfo, err error) error { if err != nil { return err diff --git a/cmd/add_test.go b/cmd/add_test.go index f966cfa578c5..2e3513f08851 100644 --- a/cmd/add_test.go +++ b/cmd/add_test.go @@ -47,6 +47,63 @@ func TestAddCommand(t *testing.T) { root interface{} tests interface{} }{ + { + name: "add_dir", + args: []string{"/home/user/.config/htop"}, + root: map[string]interface{}{ + "/home/user/.config/htop": &vfst.Dir{Perm: 0755}, + }, + tests: []vfst.Test{ + vfst.TestPath("/home/user/.local/share/chezmoi", + vfst.TestIsDir, + vfst.TestModePerm(0700), + ), + vfst.TestPath("/home/user/.local/share/chezmoi/dot_config", + vfst.TestIsDir, + vfst.TestModePerm(0755), + ), + vfst.TestPath("/home/user/.local/share/chezmoi/dot_config/htop", + vfst.TestIsDir, + vfst.TestModePerm(0755), + ), + vfst.TestPath("/home/user/.local/share/chezmoi/dot_config/htop/.keep", + vfst.TestModeIsRegular, + vfst.TestModePerm(0644), + vfst.TestContents(nil), + ), + }, + }, + { + name: "add_non_empty_dir", + args: []string{"/home/user/.config/htop"}, + root: map[string]interface{}{ + "/home/user/.config/htop": map[string]interface{}{ + "foo": "bar", + }, + }, + tests: []vfst.Test{ + vfst.TestPath("/home/user/.local/share/chezmoi", + vfst.TestIsDir, + vfst.TestModePerm(0700), + ), + vfst.TestPath("/home/user/.local/share/chezmoi/dot_config", + vfst.TestIsDir, + vfst.TestModePerm(0755), + ), + vfst.TestPath("/home/user/.local/share/chezmoi/dot_config/htop", + vfst.TestIsDir, + vfst.TestModePerm(0755), + ), + vfst.TestPath("/home/user/.local/share/chezmoi/dot_config/htop/.keep", + vfst.TestModeIsRegular, + vfst.TestModePerm(0644), + vfst.TestContents(nil), + ), + vfst.TestPath("/home/user/.local/share/chezmoi/dot_config/htop/foo", + vfst.TestDoesNotExist, + ), + }, + }, { name: "add_first_file", args: []string{"/home/user/.bashrc"}, @@ -65,11 +122,10 @@ func TestAddCommand(t *testing.T) { }, }, { - name: "add_template", + name: "add_autotemplate", args: []string{"/home/user/.gitconfig"}, add: addCmdConfig{ options: chezmoi.AddOptions{ - Template: true, AutoTemplate: true, }, }, @@ -111,7 +167,9 @@ func TestAddCommand(t *testing.T) { name: "add_recursive", args: []string{"/home/user/.config"}, add: addCmdConfig{ - recursive: true, + options: chezmoi.AddOptions{ + Recursive: true, + }, }, root: map[string]interface{}{ "/home/user": &vfst.Dir{Perm: 0755}, @@ -163,9 +221,9 @@ func TestAddCommand(t *testing.T) { name: "add_exact_dir_recursive", args: []string{"/home/user/dir"}, add: addCmdConfig{ - recursive: true, options: chezmoi.AddOptions{ - Exact: true, + Exact: true, + Recursive: true, }, }, root: map[string]interface{}{ @@ -306,7 +364,9 @@ func TestAddCommand(t *testing.T) { name: "add_symlink_in_dir_recursive", args: []string{"/home/user/foo"}, add: addCmdConfig{ - recursive: true, + options: chezmoi.AddOptions{ + Recursive: true, + }, }, root: map[string]interface{}{ "/home/user": &vfst.Dir{Perm: 0755}, @@ -367,7 +427,9 @@ func TestAddCommand(t *testing.T) { name: "dont_add_ignored_file_recursive", args: []string{"/home/user/foo"}, add: addCmdConfig{ - recursive: true, + options: chezmoi.AddOptions{ + Recursive: true, + }, }, root: map[string]interface{}{ "/home/user": &vfst.Dir{Perm: 0755}, diff --git a/cmd/completion.go b/cmd/completion.go index 99951c3d3c3d..cee36d73e785 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -27,7 +27,7 @@ func (c *Config) runCompletion(cmd *cobra.Command, args []string) error { case "zsh": return rootCmd.GenZshCompletion(c.Stdout) case "fish": - return rootCmd.GenFishCompletion(c.Stdout) + return rootCmd.GenFishCompletion(c.Stdout, true) default: return errors.New("unsupported shell") } diff --git a/cmd/config.go b/cmd/config.go index 035f93eeacf7..33d944be7ef4 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -37,6 +37,7 @@ type sourceVCSConfig struct { AutoCommit bool AutoPush bool Init interface{} + NotGit bool Pull interface{} } @@ -66,6 +67,7 @@ type Config struct { Merge mergeConfig Bitwarden bitwardenCmdConfig CD cdCmdConfig + Diff diffCmdConfig GenericSecret genericSecretCmdConfig Gopass gopassCmdConfig KeePassXC keePassXCCmdConfig @@ -84,6 +86,7 @@ type Config struct { _import importCmdConfig init initCmdConfig keyring keyringCmdConfig + managed managedCmdConfig purge purgeCmdConfig remove removeCmdConfig update updateCmdConfig @@ -136,9 +139,15 @@ func newConfig(options ...configOption) *Config { Template: templateConfig{ Options: chezmoi.DefaultTemplateOptions, }, + Diff: diffCmdConfig{ + Format: "chezmoi", + }, Merge: mergeConfig{ Command: "vimdiff", }, + GPG: chezmoi.GPG{ + Command: "gpg", + }, maxDiffDataSize: 1 * 1024 * 1024, // 1MB templateFuncs: sprig.TxtFuncMap(), scriptStateBucket: []byte("script"), diff --git a/cmd/diff.go b/cmd/diff.go index bfad5dc41030..1267a1d78432 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -1,11 +1,27 @@ package cmd import ( + "fmt" + "io" + "os/exec" + "path/filepath" + "strings" + "unicode" + + "github.com/go-git/go-git/v5/plumbing/format/diff" "github.com/spf13/cobra" "github.com/twpayne/chezmoi/internal/chezmoi" + "github.com/twpayne/go-shell" + "github.com/twpayne/go-vfs" bolt "go.etcd.io/bbolt" ) +type diffCmdConfig struct { + Format string + NoPager bool + Pager string +} + var diffCmd = &cobra.Command{ Use: "diff [targets...]", Short: "Print the diff between the target state and the destination state", @@ -18,16 +34,27 @@ var diffCmd = &cobra.Command{ func init() { rootCmd.AddCommand(diffCmd) + persistentFlags := diffCmd.PersistentFlags() + persistentFlags.StringVarP(&config.Diff.Format, "format", "f", config.Diff.Format, "format, \"chezmoi\" or \"git\"") + persistentFlags.BoolVar(&config.Diff.NoPager, "no-pager", false, "disable pager") + markRemainingZshCompPositionalArgumentsAsFiles(diffCmd, 1) } func (c *Config) runDiffCmd(cmd *cobra.Command, args []string) error { - c.DryRun = true - c.mutator = chezmoi.NullMutator{} + c.DryRun = true // Prevent scripts from running. + + switch c.Diff.Format { + case "chezmoi": + c.mutator = chezmoi.NullMutator{} + case "git": + c.mutator = chezmoi.NewFSMutator(vfs.NewReadOnlyFS(config.fs)) + default: + return fmt.Errorf("unknown diff format: %q", c.Diff.Format) + } if c.Debug { c.mutator = chezmoi.NewDebugMutator(c.mutator) } - c.mutator = chezmoi.NewVerboseMutator(c.Stdout, c.mutator, c.colored, c.maxDiffDataSize) persistentState, err := c.getPersistentState(&bolt.Options{ ReadOnly: true, @@ -37,5 +64,56 @@ func (c *Config) runDiffCmd(cmd *cobra.Command, args []string) error { } defer persistentState.Close() - return c.applyArgs(args, persistentState) + if c.Diff.NoPager || c.Diff.Pager == "" { + switch c.Diff.Format { + case "chezmoi": + c.mutator = chezmoi.NewVerboseMutator(c.Stdout, c.mutator, c.colored, c.maxDiffDataSize) + case "git": + unifiedEncoder := diff.NewUnifiedEncoder(c.Stdout, diff.DefaultContextLines) + c.mutator = chezmoi.NewGitDiffMutator(unifiedEncoder, c.mutator, c.DestDir+string(filepath.Separator)) + } + return c.applyArgs(args, persistentState) + } + + var pagerCmd *exec.Cmd + var pagerStdinPipe io.WriteCloser + + // If 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. + if strings.IndexFunc(c.Diff.Pager, unicode.IsSpace) != -1 { + shell, _ := shell.CurrentUserShell() + //nolint:gosec + pagerCmd = exec.Command(shell, "-c", c.Diff.Pager) + } else { + //nolint:gosec + pagerCmd = exec.Command(c.Diff.Pager) + } + pagerStdinPipe, err = pagerCmd.StdinPipe() + if err != nil { + return err + } + pagerCmd.Stdout = c.Stdout + pagerCmd.Stderr = c.Stderr + if err := pagerCmd.Start(); err != nil { + return err + } + + switch c.Diff.Format { + case "chezmoi": + c.mutator = chezmoi.NewVerboseMutator(pagerStdinPipe, c.mutator, c.colored, c.maxDiffDataSize) + case "git": + unifiedEncoder := diff.NewUnifiedEncoder(pagerStdinPipe, diff.DefaultContextLines) + c.mutator = chezmoi.NewGitDiffMutator(unifiedEncoder, c.mutator, c.DestDir+string(filepath.Separator)) + } + + if err := c.applyArgs(args, persistentState); err != nil { + return err + } + + if err := pagerStdinPipe.Close(); err != nil { + return err + } + + return pagerCmd.Wait() } diff --git a/cmd/docs.gen.go b/cmd/docs.gen.go index 2dcb7105b03b..7a4a4412ca3a 100644 --- a/cmd/docs.gen.go +++ b/cmd/docs.gen.go @@ -9,10 +9,17 @@ func init() { "\n" + "\n" + "* [Upcoming](#upcoming)\n" + + " * [Default diff format changing from `chezmoi` to `git`.](#default-diff-format-changing-from-chezmoi-to-git)\n" + " * [`gpgRecipient` config variable changing to `gpg.recipient`](#gpgrecipient-config-variable-changing-to-gpgrecipient)\n" + "\n" + "## Upcoming\n" + "\n" + + "### Default diff format changing from `chezmoi` to `git`.\n" + + "\n" + + "Currently chezmoi outputs diffs in its own format, containing a mix of unified\n" + + "diffs and shell commands. This will be replaced with a [git format\n" + + "diff](https://git-scm.com/docs/diff-format) in version 2.0.0.\n" + + "\n" + "### `gpgRecipient` config variable changing to `gpg.recipient`\n" + "\n" + "The `gpgRecipient` config variable is changing to `gpg.recipient`. To update,\n" + @@ -227,11 +234,13 @@ func init() { "* [How can I quickly check for problems with chezmoi on my machine?](#how-can-i-quickly-check-for-problems-with-chezmoi-on-my-machine)\n" + "* [What are the consequences of \"bare\" modifications to the target files? If my `.zshrc` is managed by chezmoi and I edit `~/.zshrc` without using `chezmoi edit`, what happens?](#what-are-the-consequences-of-bare-modifications-to-the-target-files-if-my-zshrc-is-managed-by-chezmoi-and-i-edit-zshrc-without-using-chezmoi-edit-what-happens)\n" + "* [How can I tell what dotfiles in my home directory aren't managed by chezmoi? Is there an easy way to have chezmoi manage a subset of them?](#how-can-i-tell-what-dotfiles-in-my-home-directory-arent-managed-by-chezmoi-is-there-an-easy-way-to-have-chezmoi-manage-a-subset-of-them)\n" + + "* [How can I tell what dotfiles in my home directory are currently managed by chezmoi?](#how-can-i-tell-what-dotfiles-in-my-home-directory-are-currently-managed-by-chezmoi)\n" + "* [If there's a mechanism in place for the above, is there also a way to tell chezmoi to ignore specific files or groups of files (e.g. by directory name or by glob)?](#if-theres-a-mechanism-in-place-for-the-above-is-there-also-a-way-to-tell-chezmoi-to-ignore-specific-files-or-groups-of-files-eg-by-directory-name-or-by-glob)\n" + "* [If the target already exists, but is \"behind\" the source, can chezmoi be configured to preserve the target version before replacing it with one derived from the source?](#if-the-target-already-exists-but-is-behind-the-source-can-chezmoi-be-configured-to-preserve-the-target-version-before-replacing-it-with-one-derived-from-the-source)\n" + "* [Once I've made a change to the source directory, how do I commit it?](#once-ive-made-a-change-to-the-source-directory-how-do-i-commit-it)\n" + "* [How do I only run a script when a file has changed?](#how-do-i-only-run-a-script-when-a-file-has-changed)\n" + "* [I've made changes to both the destination state and the source state that I want to keep. How can I keep them both?](#ive-made-changes-to-both-the-destination-state-and-the-source-state-that-i-want-to-keep-how-can-i-keep-them-both)\n" + + "* [Why does chezmoi convert all my template variables to lowercase?](#why-does-chezmoi-convert-all-my-template-variables-to-lowercase)\n" + "* [chezmoi's source file naming system cannot handle all possible filenames](#chezmois-source-file-naming-system-cannot-handle-all-possible-filenames)\n" + "* [gpg encryption fails. What could be wrong?](#gpg-encryption-fails-what-could-be-wrong)\n" + "* [I'm getting errors trying to build chezmoi from source](#im-getting-errors-trying-to-build-chezmoi-from-source)\n" + @@ -261,6 +270,10 @@ func init() { "`chezmoi unmanaged` will list everything not managed by chezmoi. You can add\n" + "entire directories with `chezmoi add -r`.\n" + "\n" + + "## How can I tell what dotfiles in my home directory are currently managed by chezmoi?\n" + + "\n" + + "`chezmoi managed` will list everything managed by chezmoi.\n" + + "\n" + "## If there's a mechanism in place for the above, is there also a way to tell chezmoi to ignore specific files or groups of files (e.g. by directory name or by glob)?\n" + "\n" + "By default, chezmoi ignores everything that you haven't explicitly `chezmoi\n" + @@ -350,6 +363,13 @@ func init() { "state, target state, and destination state. Copy the changes you want to keep in\n" + "to the source state.\n" + "\n" + + "## Why does chezmoi convert all my template variables to lowercase?\n" + + "\n" + + "This is due to a feature in\n" + + "[`github.com/spf13/viper`](https://github.com/spf13/viper), the library that\n" + + "chezmoi uses to read its configuration file. For more information see [this\n" + + "GitHub issue issue](https://github.com/twpayne/chezmoi/issues/463).\n" + + "\n" + "## chezmoi's source file naming system cannot handle all possible filenames\n" + "\n" + "This is correct. Certain target filenames, for example `~/dot_example`, are\n" + @@ -453,6 +473,7 @@ func init() { "* [Use templates to manage files that vary from machine to machine](#use-templates-to-manage-files-that-vary-from-machine-to-machine)\n" + "* [Use completely separate config files on different machines](#use-completely-separate-config-files-on-different-machines)\n" + "* [Create a config file on a new machine automatically](#create-a-config-file-on-a-new-machine-automatically)\n" + + "* [Have chezmoi create a directory, but ignore its contents](#have-chezmoi-create-a-directory-but-ignore-its-contents)\n" + "* [Ensure that a target is removed](#ensure-that-a-target-is-removed)\n" + "* [Include a subdirectory from another repository, like Oh My Zsh](#include-a-subdirectory-from-another-repository-like-oh-my-zsh)\n" + "* [Handle configuration files which are externally modified](#handle-configuration-files-which-are-externally-modified)\n" + @@ -474,6 +495,7 @@ func init() { "* [Import archives](#import-archives)\n" + "* [Export archives](#export-archives)\n" + "* [Use a non-git version control system](#use-a-non-git-version-control-system)\n" + + "* [Customize the `diff` command](#customize-the-diff-command)\n" + "* [Use a merge tool other than vimdiff](#use-a-merge-tool-other-than-vimdiff)\n" + "* [Migrate from a dotfile manager that uses symlinks](#migrate-from-a-dotfile-manager-that-uses-symlinks)\n" + "\n" + @@ -579,6 +601,9 @@ func init() { " [data]\n" + " email = \"john@home.org\"\n" + "\n" + + "Note that all variable names will be converted to lowercase. This is due to a\n" + + "feature of a library used by chezmoi.\n" + + "\n" + "If you intend to store private data (e.g. access tokens) in\n" + "`~/.config/chezmoi/chezmoi.toml`, make sure it has permissions `0600`.\n" + "\n" + @@ -587,10 +612,11 @@ func init() { "includes JSON, YAML, and TOML. Variable names must start with a letter and be\n" + "followed by zero or more letters or digits.\n" + "\n" + - "Then, add `~/.gitconfig` to chezmoi using the `--template` flag to automatically\n" + - "turn it in to a template:\n" + + "Then, add `~/.gitconfig` to chezmoi using the `--autotemplate` flag to turn it\n" + + "into a template and automatically detect variables from the `data` section\n" + + "of your `~/.config/chezmoi/chezmoi.toml` file:\n" + "\n" + - " chezmoi add --template --autotemplate ~/.gitconfig\n" + + " chezmoi add --autotemplate ~/.gitconfig\n" + "\n" + "You can then open the template (which will be saved in the file\n" + "`~/.local/share/chezmoi/dot_gitconfig.tmpl`):\n" + @@ -602,9 +628,8 @@ func init() { " [user]\n" + " email = \"{{ .email }}\"\n" + "\n" + - "The `--autotemplate` flag to `chezmoi add` above instructs chezmoi to generate a\n" + - "template by substituting variables from the `data` section of your\n" + - "`~/.config/chezmoi/chezmoi.toml` file.\n" + + "To disable automatic variable detection, use the `--template` or `-T` option to\n" + + "`chezmoi add` instead of `--autotemplate`.\n" + "\n" + "Templates are often used to capture machine-specifc differences. For example, in\n" + "your `~/.local/share/chezmoi/dot_bashrc.tmpl` you might have:\n" + @@ -736,6 +761,26 @@ func init() { "Then `chezmoi init` will create an initial `chezmoi.toml` using this template.\n" + "`promptString` is a special function that prompts the user (you) for a value.\n" + "\n" + + "## Have chezmoi create a directory, but ignore its contents\n" + + "\n" + + "If you want chezmoi to create a directory, but ignore its contents, say `~/src`,\n" + + "first run:\n" + + "\n" + + " mkdir -p $(chezmoi source-path)/src\n" + + "\n" + + "This creates the directory in the source state, which means that chezmoi will\n" + + "create it (if it does not already exist) when you run `chezmoi apply`.\n" + + "\n" + + "However, as this is an empty directory it will be ignored by git. So, create a\n" + + "file in the directory in the source state that will be seen by git (so git does\n" + + "not ignore the directory) but ignored by chezmoi (so chezmoi does not include it\n" + + "in the target state):\n" + + "\n" + + " touch $(chezmoi source-path)/src/.keep\n" + + "\n" + + "chezmoi automatically creates `.keep` files when you add an empty directory with\n" + + "`chezmoi add`.\n" + + "\n" + "## Ensure that a target is removed\n" + "\n" + "Create a file called `.chezmoiremove` in the source directory containing a list\n" + @@ -1187,6 +1232,19 @@ func init() { "you'd like to see your VCS better supported, please [open an issue on\n" + "GitHub](https://github.com/twpayne/chezmoi/issues/new/choose).\n" + "\n" + + "## Customize the `diff` command\n" + + "\n" + + "By default, chezmoi uses a built-in diff. You can change the format, and/or pipe\n" + + "the output into a pager of your choice. For example, to use\n" + + "[`diff-so-fancy`](https://github.com/so-fancy/diff-so-fancy) specify:\n" + + "\n" + + " [diff]\n" + + " format = \"git\"\n" + + " pager = \"diff-so-fancy\"\n" + + "\n" + + "The format can also be set with the `--format` option to the `diff` command, and\n" + + "the pager can be disabled using `--no-pager`.\n" + + "\n" + "## Use a merge tool other than vimdiff\n" + "\n" + "By default, chezmoi uses vimdiff, but you can use any merge tool of your choice.\n" + @@ -1295,6 +1353,23 @@ func init() { "This will re-use whichever mechanism you used to install chezmoi to install the\n" + "latest release.\n" + "\n") + assets["docs/MEDIA.md"] = []byte("" + + "# chezmoi in the media\n" + + "\n" + + "\n" + + "\n" + + "| Date | Version | Format | Link |\n" + + "| ---------- | ------- | ------------ | ------------------------------------------------------------------------------------------------------------------------- |\n" + + "| 2020-04-16 | 1.17.19 | Text (FR) | [Chezmoi, visite guidée](https://blog.wescale.fr/2020/04/16/chezmoi-visite-guidee/) |\n" + + "| 2020-04-03 | 1.7.17 | Text | [Fedora Magazine: Take back your dotfiles with Chezmoi](https://fedoramagazine.org/take-back-your-dotfiles-with-chezmoi/) |\n" + + "| 2020-03-12 | 1.7.16 | Video | [Manging Dotfiles with ChezMoi](https://www.youtube.com/watch?v=HXx6ugA98Qo) |\n" + + "| 2019-11-20 | 1.7.2 | Audio/video | [FLOSS weekly episode 556: chezmoi](https://twit.tv/shows/floss-weekly/episodes/556) |\n" + + "| 2019-01-10 | 0.0.11 | Text | [Linux Fu: The kitchen sync](https://hackaday.com/2019/01/10/linux-fu-the-kitchen-sync/) |\n" + + "\n" + + "To add your article to this page please either [open an\n" + + "issue](https://github.com/twpayne/chezmoi/issues/new/choose) or submit a pull\n" + + "request that modifies this file\n" + + "([`docs/MEDIA.md`](https://github.com/twpayne/chezmoi/blob/master/docs/MEDIA.md)).\n") assets["docs/QUICKSTART.md"] = []byte("" + "# chezmoi Quick Start Guide\n" + "\n" + @@ -1434,6 +1509,7 @@ func init() { " * [`init` [*repo*]](#init-repo)\n" + " * [`import` *filename*](#import-filename)\n" + " * [`manage` *targets*](#manage-targets)\n" + + " * [`managed`](#managed)\n" + " * [`merge` *targets*](#merge-targets)\n" + " * [`purge`](#purge)\n" + " * [`remove` *targets*](#remove-targets)\n" + @@ -1565,10 +1641,13 @@ func init() { "| `color` | string | `auto` | Colorize diffs |\n" + "| `data` | any | *none* | Template data |\n" + "| `destDir` | string | `~` | Destination directory |\n" + + "| `diff.format` | string | `chezmoi` | Diff format, either `chezmoi` or `git` |\n" + + "| `diff.pager` | string | *none* | Pager |\n" + "| `dryRun` | bool | `false` | Dry run mode |\n" + "| `follow` | bool | `false` | Follow symlinks |\n" + "| `genericSecret.command` | string | *none* | Generic secret command |\n" + "| `gopass.command` | string | `gopass` | gopass CLI command |\n" + + "| `gpg.command` | string | `gpg` | GPG CLI command |\n" + "| `gpg.recipient` | string | *none* | GPG recipient |\n" + "| `gpg.symmetric` | bool | `false` | Use symmetric GPG encryption |\n" + "| `keepassxc.args` | []string | *none* | Extra args to KeePassXC CLI command |\n" + @@ -1650,10 +1729,9 @@ func init() { "### `.chezmoiignore`\n" + "\n" + "If a file called `.chezmoiignore` exists in the source state then it is\n" + - "interpreted as a set of patterns to ignore. Patterns are matched using the Go\n" + - "standard libary's [`filepath.Match` pattern\n" + - "syntax](https://pkg.go.dev/path/filepath?tab=doc#Match) and match against the\n" + - "target path, not the source path.\n" + + "interpreted as a set of patterns to ignore. Patterns are matched using\n" + + "[`doublestar.PathMatch`](https://pkg.go.dev/github.com/bmatcuk/doublestar?tab=doc#PathMatch)\n" + + "and match against the target path, not the source path.\n" + "\n" + "Patterns can be excluded by prefixing them with a `!` character. All excludes\n" + "take priority over all includes.\n" + @@ -1728,6 +1806,12 @@ func init() { "then its source state is replaced with its current state in the destination\n" + "directory. The `add` command accepts additional flags:\n" + "\n" + + "#### `--autotemplate`\n" + + "\n" + + "Automatically generate a template by replacing strings with variable names from\n" + + "the `data` section of the config file. Longer subsitutions occur before shorter\n" + + "ones. This implies the `--template` option.\n" + + "\n" + "#### `-e`, `--empty`\n" + "\n" + "Set the `empty` attribute on added files.\n" + @@ -1750,10 +1834,7 @@ func init() { "\n" + "#### `-T`, `--template`\n" + "\n" + - "Set the `template` attribute on added files and symlinks. In addition, if the\n" + - "`--autotemplate` flag is set, chezmoi attempts to automatically generate the\n" + - "template by replacing any template data values with the equivalent template data\n" + - "keys. Longer subsitutions occur before shorter ones.\n" + + "Set the `template` attribute on added files and symlinks.\n" + "\n" + "#### `add` examples\n" + "\n" + @@ -1855,15 +1936,38 @@ func init() { "\n" + "### `diff` [*targets*]\n" + "\n" + - "Print the approximate shell commands required to ensure that *targets* in the\n" + - "destination directory match the target state. If no targets are specified, print\n" + - "the commands required for all targets. It is equivalent to `chezmoi apply\n" + - "--dry-run --verbose`.\n" + + "Print the difference between the target state and the destination state for\n" + + "*targets*. If no targets are specified, print the differences for all targets.\n" + + "\n" + + "If a `diff.pager` command is set in the configuration file then the output will\n" + + "be piped into it.\n" + + "\n" + + "#### `-f`, `--format` *format*\n" + + "\n" + + "Print the diff in *format*. The format can be set with the `diff.format`\n" + + "variable in the configuration file. Valid formats are:\n" + + "\n" + + "##### `chezmoi`\n" + + "\n" + + "A mix of unified diffs and pseudo shell commands, equivalent to `chezmoi apply\n" + + "--dry-run --verbose`. They can be colorized and include scripts.\n" + + "\n" + + "##### `git`\n" + + "\n" + + "A [git format diff](https://git-scm.com/docs/diff-format), without color and not\n" + + "including scripts. In version 2.0.0 of chezmoi, `git` format diffs will become\n" + + "the default and support color and scripts and the `chezmoi` format will be\n" + + "removed.\n" + + "\n" + + "#### `--no-pager`\n" + + "\n" + + "Do not use the pager.\n" + "\n" + "#### `diff` examples\n" + "\n" + " chezmoi diff\n" + " chezmoi diff ~/.bashrc\n" + + " chezmoi diff --format=git\n" + "\n" + "### `docs` [*regexp*]\n" + "\n" + @@ -2026,6 +2130,25 @@ func init() { "\n" + "`manage` is an alias for `add` for symmetry with `unmanage`.\n" + "\n" + + "### `managed`\n" + + "\n" + + "List all managed entries in the destination directory in alphabetical order.\n" + + "\n" + + "#### `-i`, `--include` *types*\n" + + "\n" + + "Only list entries of type *types*. *types* is a comma-separated list of types of\n" + + "entry to include. Valid types are `dirs`, `files`, and `symlinks` which can be\n" + + "abbreviated to `d`, `f`, and `s` respectively. By default, `manage` will list\n" + + "entries of all types.\n" + + "\n" + + "#### `managed` examples\n" + + "\n" + + " chezmoi managed\n" + + " chezmoi managed --include=files\n" + + " chezmoi managed --include=files,symlinks\n" + + " chezmoi managed -i d\n" + + " chezmoi managed -i d,f\n" + + "\n" + "### `merge` *targets*\n" + "\n" + "Perform a three-way merge between the destination state, the source state, and\n" + diff --git a/cmd/dump_test.go b/cmd/dump_test.go index 1083cb7ee2bd..952f94b76bbb 100644 --- a/cmd/dump_test.go +++ b/cmd/dump_test.go @@ -3,7 +3,6 @@ package cmd import ( "bytes" "encoding/json" - "fmt" "path/filepath" "testing" @@ -29,7 +28,6 @@ func TestDumpCmd(t *testing.T) { withStdout(stdout), ) assert.NoError(t, c.runDumpCmd(nil, nil)) - fmt.Println(stdout.String()) var actual interface{} assert.NoError(t, json.NewDecoder(stdout).Decode(&actual)) expected := []interface{}{ diff --git a/cmd/helps.gen.go b/cmd/helps.gen.go index 2c360126abbc..eeb8bade4270 100644 --- a/cmd/helps.gen.go +++ b/cmd/helps.gen.go @@ -15,6 +15,12 @@ var helps = map[string]help{ " state, then its source state is replaced with its current state in the\n" + " destination directory. The `add` command accepts additional flags:\n" + "\n" + + " `--autotemplate`\n" + + "\n" + + " Automatically generate a template by replacing strings with variable names\n" + + " from the `data` section of the config file. Longer subsitutions occur before\n" + + " shorter ones. This implies the `--template` option.\n" + + "\n" + " `-e`, `--empty`\n" + "\n" + " Set the `empty` attribute on added files.\n" + @@ -38,10 +44,7 @@ var helps = map[string]help{ "\n" + " `-T`, `--template`\n" + "\n" + - " Set the `template` attribute on added files and symlinks. In addition, if the\n" + - " `--autotemplate` flag is set, chezmoi attempts to automatically generate the\n" + - " template by replacing any template data values with the equivalent template\n" + - " data keys. Longer subsitutions occur before shorter ones.", + " Set the `template` attribute on added files and symlinks.", example: "" + " chezmoi add ~/.bashrc\n" + " chezmoi add ~/.gitconfig --template\n" + @@ -137,13 +140,36 @@ var helps = map[string]help{ "diff": { long: "" + "Description:\n" + - " Print the approximate shell commands required to ensure that *targets* in the\n" + - " destination directory match the target state. If no targets are specified,\n" + - " print the commands required for all targets. It is equivalent to `chezmoi\n" + - " apply --dry-run --verbose`.", + " Print the difference between the target state and the destination state for\n" + + " *targets*. If no targets are specified, print the differences for all targets.\n" + + "\n" + + " If a `diff.pager` command is set in the configuration file then the output\n" + + " will be piped into it.\n" + + "\n" + + " `-f`, `--format` *format*\n" + + "\n" + + " Print the diff in *format*. The format can be set with the `diff.format`\n" + + " variable in the configuration file. Valid formats are:\n" + + "\n" + + " ##### `chezmoi`\n" + + "\n" + + " A mix of unified diffs and pseudo shell commands, equivalent to `chezmoi apply --\n" + + " dry-run --verbose`. They can be colorized and include scripts.\n" + + "\n" + + " ##### `git`\n" + + "\n" + + " A git format diff https://git-scm.com/docs/diff-format, without color and not\n" + + " including scripts. In version 2.0.0 of chezmoi, `git` format diffs will become\n" + + " the default and support color and scripts and the `chezmoi` format will be\n" + + " removed.\n" + + "\n" + + " `--no-pager`\n" + + "\n" + + " Do not use the pager.", example: "" + " chezmoi diff\n" + - " chezmoi diff ~/.bashrc", + " chezmoi diff ~/.bashrc\n" + + " chezmoi diff --format=git", }, "docs": { long: "" + @@ -303,6 +329,24 @@ var helps = map[string]help{ "Description:\n" + " `manage` is an alias for `add` for symmetry with `unmanage`.", }, + "managed": { + long: "" + + "Description:\n" + + " List all managed entries in the destination directory in alphabetical order.\n" + + "\n" + + " `-i`, `--include` *types*\n" + + "\n" + + " Only list entries of type *types*. *types* is a comma-separated list of types\n" + + " of entry to include. Valid types are `dirs`, `files`, and `symlinks` which can\n" + + " be abbreviated to `d`, `f`, and `s` respectively. By default, `manage` will\n" + + " list entries of all types.", + example: "" + + " chezmoi managed\n" + + " chezmoi managed --include=files\n" + + " chezmoi managed --include=files,symlinks\n" + + " chezmoi managed -i d\n" + + " chezmoi managed -i d,f", + }, "merge": { long: "" + "Description:\n" + diff --git a/cmd/managed.go b/cmd/managed.go new file mode 100644 index 000000000000..6cb64d8f03e0 --- /dev/null +++ b/cmd/managed.go @@ -0,0 +1,82 @@ +package cmd + +import ( + "fmt" + "path/filepath" + "sort" + + "github.com/spf13/cobra" + "github.com/twpayne/chezmoi/internal/chezmoi" +) + +var managedCmd = &cobra.Command{ + Use: "managed", + Args: cobra.NoArgs, + Short: "List the managed files in the destination directory", + Long: mustGetLongHelp("managed"), + Example: getExample("managed"), + PreRunE: config.ensureNoError, + RunE: config.runManagedCmd, +} + +type managedCmdConfig struct { + include []string +} + +func init() { + rootCmd.AddCommand(managedCmd) + + persistentFlags := managedCmd.PersistentFlags() + persistentFlags.StringSliceVarP(&config.managed.include, "include", "i", []string{"dirs", "files", "symlinks"}, "include") +} + +func (c *Config) runManagedCmd(cmd *cobra.Command, args []string) error { + ts, err := c.getTargetState(nil) + if err != nil { + return err + } + + var ( + includeDirs = false + includeFiles = false + includeSymlinks = false + ) + for _, what := range c.managed.include { + switch what { + case "dirs", "d": + includeDirs = true + case "files", "f": + includeFiles = true + case "symlinks", "s": + includeSymlinks = true + default: + return fmt.Errorf("unrecognized include: %q", what) + } + } + + allEntries := ts.AllEntries() + + targetNames := make([]string, 0, len(allEntries)) + for _, entry := range allEntries { + if _, ok := entry.(*chezmoi.Dir); ok && !includeDirs { + continue + } + if _, ok := entry.(*chezmoi.File); ok && !includeFiles { + continue + } + if _, ok := entry.(*chezmoi.Symlink); ok && !includeSymlinks { + continue + } + targetNames = append(targetNames, entry.TargetName()) + } + + sort.Strings(targetNames) + for _, targetName := range targetNames { + if ts.TargetIgnore.Match(targetName) { + continue + } + fmt.Fprintln(c.Stdout, filepath.Join(ts.DestDir, targetName)) + } + + return nil +} diff --git a/cmd/managed_test.go b/cmd/managed_test.go new file mode 100644 index 000000000000..5266d7335543 --- /dev/null +++ b/cmd/managed_test.go @@ -0,0 +1,119 @@ +package cmd + +import ( + "bufio" + "bytes" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/twpayne/go-vfs/vfst" +) + +func TestManagedCmd(t *testing.T) { + for _, tc := range []struct { + include []string + expectedTargetNames []string + }{ + { + include: []string{"dirs", "files", "symlinks"}, + expectedTargetNames: []string{ + "/home/user/dir", + "/home/user/dir/file1", + "/home/user/dir/subdir", + "/home/user/dir/subdir/file2", + "/home/user/symlink", + }, + }, + { + include: []string{"d", "f", "s"}, + expectedTargetNames: []string{ + "/home/user/dir", + "/home/user/dir/file1", + "/home/user/dir/subdir", + "/home/user/dir/subdir/file2", + "/home/user/symlink", + }, + }, + { + include: []string{"dirs"}, + expectedTargetNames: []string{ + "/home/user/dir", + "/home/user/dir/subdir", + }, + }, + { + include: []string{"files"}, + expectedTargetNames: []string{ + "/home/user/dir/file1", + "/home/user/dir/subdir/file2", + }, + }, + { + include: []string{"symlinks"}, + expectedTargetNames: []string{ + "/home/user/symlink", + }, + }, + { + include: []string{"f", "s"}, + expectedTargetNames: []string{ + "/home/user/dir/file1", + "/home/user/dir/subdir/file2", + "/home/user/symlink", + }, + }, + } { + t.Run(strings.Join(tc.include, "_"), func(t *testing.T) { + fs, cleanup, err := vfst.NewTestFS(map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + "dir/file1": "contents", + "dir/subdir/file2": "contents", + "symlink_symlink": "target", + }, + }) + require.NoError(t, err) + defer cleanup() + stdout := &bytes.Buffer{} + c := newTestConfig( + fs, + withStdout(stdout), + withManaged(managedCmdConfig{ + include: tc.include, + }), + ) + assert.NoError(t, c.runManagedCmd(nil, nil)) + posixTargetNames, err := extractPOSIXTargetNames(stdout.Bytes()) + require.NoError(t, err) + assert.Equal(t, tc.expectedTargetNames, posixTargetNames) + }) + } +} + +// extractPOSIXTargetNames extracts all target names from b and coverts them to +// POSIX-like names. +func extractPOSIXTargetNames(b []byte) ([]string, error) { + var targetNames []string + s := bufio.NewScanner(bytes.NewBuffer(b)) + for s.Scan() { + targetNames = append(targetNames, posixify(s.Text())) + } + if err := s.Err(); err != nil { + return nil, err + } + return targetNames, nil +} + +// posixify returns a POSIX-like path based on path, stripping any volume name +// and converting backward slashes. +func posixify(path string) string { + return filepath.ToSlash(strings.TrimPrefix(path, filepath.VolumeName(path))) +} + +func withManaged(managed managedCmdConfig) configOption { + return func(c *Config) { + c.managed = managed + } +} diff --git a/cmd/root.go b/cmd/root.go index 370fd85de0fd..f635b0427936 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "path/filepath" "strings" "github.com/coreos/go-semver/semver" @@ -87,6 +88,19 @@ func init() { if config.err != nil { rootCmd.Printf("warning: %s: %v\n", config.configFile, config.err) } + if config.GPGRecipient != "" { + rootCmd.Printf("" + + "warning: your config file uses gpgRecipient which will be deprecated in v2\n" + + "warning: to disable this warning, set gpg.recipient in your config file instead\n", + ) + } + if config.SourceVCS.Command != "" && !config.SourceVCS.NotGit && !strings.Contains(filepath.Base(config.SourceVCS.Command), "git") { + rootCmd.Printf("" + + "warning: it looks like you are using a version control system that is not git which will be deprecated in v2\n" + + "warning: please report this at https://github.com/twpayne/chezmoi/issues/459\n" + + "warning: to disable this warning, set sourceVCS.notGit = true in your config file\n", + ) + } case os.IsNotExist(err): default: printErrorAndExit(err) diff --git a/completions/chezmoi-completion.bash b/completions/chezmoi-completion.bash index 349baedcf79a..642bfcb5fb0d 100644 --- a/completions/chezmoi-completion.bash +++ b/completions/chezmoi-completion.bash @@ -36,9 +36,71 @@ __chezmoi_contains_word() return 1 } +__chezmoi_handle_go_custom_completion() +{ + __chezmoi_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}" + + local out requestComp lastParam lastChar comp directive args + + # Prepare the command to request completions for the program. + # Calling ${words[0]} instead of directly chezmoi allows to handle aliases + args=("${words[@]:1}") + requestComp="${words[0]} __completeNoDesc ${args[*]}" + + lastParam=${words[$((${#words[@]}-1))]} + lastChar=${lastParam:$((${#lastParam}-1)):1} + __chezmoi_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}" + + if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __chezmoi_debug "${FUNCNAME[0]}: Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __chezmoi_debug "${FUNCNAME[0]}: calling ${requestComp}" + # Use eval to handle any environment variables and such + out=$(eval "${requestComp}" 2>/dev/null) + + # Extract the directive integer at the very end of the output following a colon (:) + directive=${out##*:} + # Remove the directive + out=${out%:*} + if [ "${directive}" = "${out}" ]; then + # There is not directive specified + directive=0 + fi + __chezmoi_debug "${FUNCNAME[0]}: the completion directive is: ${directive}" + __chezmoi_debug "${FUNCNAME[0]}: the completions are: ${out[*]}" + + if [ $((directive & 1)) -ne 0 ]; then + # Error code. No completion. + __chezmoi_debug "${FUNCNAME[0]}: received error from custom completion go code" + return + else + if [ $((directive & 2)) -ne 0 ]; then + if [[ $(type -t compopt) = "builtin" ]]; then + __chezmoi_debug "${FUNCNAME[0]}: activating no space" + compopt -o nospace + fi + fi + if [ $((directive & 4)) -ne 0 ]; then + if [[ $(type -t compopt) = "builtin" ]]; then + __chezmoi_debug "${FUNCNAME[0]}: activating no file completion" + compopt +o default + fi + fi + + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${out[*]}" -- "$cur") + fi +} + __chezmoi_handle_reply() { __chezmoi_debug "${FUNCNAME[0]}" + local comp case $cur in -*) if [[ $(type -t compopt) = "builtin" ]]; then @@ -50,8 +112,8 @@ __chezmoi_handle_reply() else allflags=("${flags[*]} ${two_word_flags[*]}") fi - while IFS='' read -r c; do - COMPREPLY+=("$c") + while IFS='' read -r comp; do + COMPREPLY+=("$comp") done < <(compgen -W "${allflags[*]}" -- "$cur") if [[ $(type -t compopt) = "builtin" ]]; then [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace @@ -98,17 +160,21 @@ __chezmoi_handle_reply() completions=("${commands[@]}") if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then completions=("${must_have_one_noun[@]}") + elif [[ -n "${has_completion_function}" ]]; then + # if a go completion function is provided, defer to that function + completions=() + __chezmoi_handle_go_custom_completion fi if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then completions+=("${must_have_one_flag[@]}") fi - while IFS='' read -r c; do - COMPREPLY+=("$c") + while IFS='' read -r comp; do + COMPREPLY+=("$comp") done < <(compgen -W "${completions[*]}" -- "$cur") if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then - while IFS='' read -r c; do - COMPREPLY+=("$c") + while IFS='' read -r comp; do + COMPREPLY+=("$comp") done < <(compgen -W "${noun_aliases[*]}" -- "$cur") fi @@ -598,6 +664,10 @@ _chezmoi_diff() flags_with_completion=() flags_completion=() + flags+=("--format=") + two_word_flags+=("--format") + two_word_flags+=("-f") + flags+=("--no-pager") flags+=("--color=") two_word_flags+=("--color") flags+=("--config=") @@ -1058,6 +1128,47 @@ _chezmoi_init() noun_aliases=() } +_chezmoi_managed() +{ + last_command="chezmoi_managed" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--include=") + two_word_flags+=("--include") + two_word_flags+=("-i") + flags+=("--color=") + two_word_flags+=("--color") + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--debug") + flags+=("--destination=") + two_word_flags+=("--destination") + two_word_flags+=("-D") + flags+=("--dry-run") + flags+=("-n") + flags+=("--follow") + flags+=("--remove") + flags+=("--source=") + two_word_flags+=("--source") + two_word_flags+=("-S") + flags+=("--verbose") + flags+=("-v") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _chezmoi_merge() { last_command="chezmoi_merge" @@ -1935,6 +2046,7 @@ _chezmoi_root_command() commands+=("hg") commands+=("import") commands+=("init") + commands+=("managed") commands+=("merge") commands+=("purge") commands+=("remove") @@ -2000,6 +2112,7 @@ __start_chezmoi() local commands=("chezmoi") local must_have_one_flag=() local must_have_one_noun=() + local has_completion_function local last_command local nouns=() diff --git a/completions/chezmoi.fish b/completions/chezmoi.fish index ab2a589bd831..c17bc26e533e 100644 --- a/completions/chezmoi.fish +++ b/completions/chezmoi.fish @@ -1,492 +1,137 @@ +# fish completion for chezmoi -*- shell-script -*- -function __fish_chezmoi_no_subcommand --description 'Test if chezmoi has yet to be given the subcommand' - for i in (commandline -opc) - if contains -- $i add apply archive cat cd chattr completion data diff docs doctor dump edit edit-config execute-template forget git hg import init merge purge remove secret source source-path unmanaged update upgrade verify - return 1 - end - end - return 0 +function __chezmoi_debug + set file "$BASH_COMP_DEBUG_FILE" + if test -n "$file" + echo "$argv" >> $file + end end -function __fish_chezmoi_seen_subcommand_path --description 'Test whether the full path of subcommands is the current path' - set -l cmd (commandline -opc) - set -e cmd[1] - set -l pattern (string replace -a " " ".+" "$argv") - string match -r "$pattern" (string trim -- "$cmd") + +function __chezmoi_perform_completion + __chezmoi_debug "Starting __chezmoi_perform_completion with: $argv" + + set args (string split -- " " "$argv") + set lastArg "$args[-1]" + + __chezmoi_debug "args: $args" + __chezmoi_debug "last arg: $lastArg" + + set emptyArg "" + if test -z "$lastArg" + __chezmoi_debug "Setting emptyArg" + set emptyArg \"\" + end + __chezmoi_debug "emptyArg: $emptyArg" + + set requestComp "$args[1] __complete $args[2..-1] $emptyArg" + __chezmoi_debug "Calling $requestComp" + + set results (eval $requestComp 2> /dev/null) + set comps $results[1..-2] + set directiveLine $results[-1] + + # For Fish, when completing a flag with an = (e.g., -n=) + # completions must be prefixed with the flag + set flagPrefix (string match -r -- '-.*=' "$lastArg") + + __chezmoi_debug "Comps: $comps" + __chezmoi_debug "DirectiveLine: $directiveLine" + __chezmoi_debug "flagPrefix: $flagPrefix" + + for comp in $comps + printf "%s%s\n" "$flagPrefix" "$comp" + end + + printf "%s\n" "$directiveLine" end -# borrowed from current fish-shell master, since it is not in current 2.7.1 release -function __fish_seen_argument - argparse 's/short=+' 'l/long=+' -- $argv - set cmd (commandline -co) - set -e cmd[1] - for t in $cmd - for s in $_flag_s - if string match -qr "^-[A-z0-9]*"$s"[A-z0-9]*\$" -- $t - return 0 - end - end +# This function does three things: +# 1- Obtain the completions and store them in the global __chezmoi_comp_results +# 2- Set the __chezmoi_comp_do_file_comp flag if file completion should be performed +# and unset it otherwise +# 3- Return true if the completion results are not empty +function __chezmoi_prepare_completions + # Start fresh + set --erase __chezmoi_comp_do_file_comp + set --erase __chezmoi_comp_results + + # Check if the command-line is already provided. This is useful for testing. + if not set --query __chezmoi_comp_commandLine + set __chezmoi_comp_commandLine (commandline) + end + __chezmoi_debug "commandLine is: $__chezmoi_comp_commandLine" + + set results (__chezmoi_perform_completion "$__chezmoi_comp_commandLine") + set --erase __chezmoi_comp_commandLine + __chezmoi_debug "Completion results: $results" + + if test -z "$results" + __chezmoi_debug "No completion, probably due to a failure" + # Might as well do file completion, in case it helps + set --global __chezmoi_comp_do_file_comp 1 + return 0 + end - for l in $_flag_l - if string match -q -- "--$l" $t - return 0 - end - end - end + set directive (string sub --start 2 $results[-1]) + set --global __chezmoi_comp_results $results[1..-2] - return 1 + __chezmoi_debug "Completions are: $__chezmoi_comp_results" + __chezmoi_debug "Directive is: $directive" + + if test -z "$directive" + set directive 0 + end + + set compErr (math (math --scale 0 $directive / 1) % 2) + if test $compErr -eq 1 + __chezmoi_debug "Received error directive: aborting." + # Might as well do file completion, in case it helps + set --global __chezmoi_comp_do_file_comp 1 + return 0 + end + + set nospace (math (math --scale 0 $directive / 2) % 2) + set nofiles (math (math --scale 0 $directive / 4) % 2) + + __chezmoi_debug "nospace: $nospace, nofiles: $nofiles" + + # Important not to quote the variable for count to work + set numComps (count $__chezmoi_comp_results) + __chezmoi_debug "numComps: $numComps" + + if test $numComps -eq 1; and test $nospace -ne 0 + # To support the "nospace" directive we trick the shell + # by outputting an extra, longer completion. + __chezmoi_debug "Adding second completion to perform nospace directive" + set --append __chezmoi_comp_results $__chezmoi_comp_results[1]. + end + + if test $numComps -eq 0; and test $nofiles -eq 0 + __chezmoi_debug "Requesting file completion" + set --global __chezmoi_comp_do_file_comp 1 + end + + # If we don't want file completion, we must return true even if there + # are no completions found. This is because fish will perform the last + # completion command, even if its condition is false, if no other + # completion command was triggered + return (not set --query __chezmoi_comp_do_file_comp) end -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a add -d 'Add an existing file, directory, or symlink to the source state' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a apply -d 'Update the destination directory to match the target state' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a archive -d 'Write a tar archive of the target state to stdout' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a cat -d 'Print the target contents of a file or symlink' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a cd -d 'Launch a shell in the source directory' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a chattr -d 'Change the attributes of a target in the source state' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a completion -d 'Write shell completion code for the specified shell (bash, fish, or zsh) to stdout' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a data -d 'Print the template data' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a diff -d 'Print the diff between the target state and the destination state' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a docs -d 'Print documentation' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a doctor -d 'Check your system for potential problems' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a dump -d 'Write a dump of the target state to stdout' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a edit -d 'Edit the source state of a target' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a edit-config -d 'Edit the configuration file' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a execute-template -d 'Write the result of executing the given template(s) to stdout' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a forget -d 'Remove a target from the source state' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a git -d 'Run git in the source directory' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a hg -d 'Run mercurial in the source directory' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a import -d 'Import a tar archive into the source state' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a init -d 'Setup the source directory and update the destination directory to match the target state' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a merge -d 'Perform a three-way merge between the destination state, the source state, and the target state' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a purge -d 'Purge all of chezmoi\'s configuration and data' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a remove -d 'Remove a target from the source state and the destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a secret -d 'Interact with a secret manager' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a source -d 'Run the source version control system command in the source directory' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a source-path -d 'Print the path of a target in the source state' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a unmanaged -d 'List the unmanaged files in the destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a update -d 'Pull changes from the source VCS and apply any changes' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a upgrade -d 'Upgrade chezmoi' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -a verify -d 'Exit with success if the destination state matches the target state, fail otherwise' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_no_subcommand' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -s a -l autotemplate -d 'auto generate the template when adding files as templates' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -s e -l empty -d 'add empty files' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -l encrypt -d 'encrypt files' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -s x -l exact -d 'add directories exactly' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -s f -l force -d 'overwrite source state, even if template would be lost' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -s p -l prompt -d 'prompt before adding' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -s r -l recursive -d 'recurse in to subdirectories' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -s T -l template -d 'add files as templates' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path add' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path apply' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path apply' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path apply' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path apply' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path apply' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path apply' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path apply' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path apply' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path apply' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path archive' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path archive' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path archive' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path archive' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path archive' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path archive' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path archive' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path archive' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path archive' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cat' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cat' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cat' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cat' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cat' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cat' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cat' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cat' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cat' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cd' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cd' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cd' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cd' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cd' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cd' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cd' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cd' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path cd' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path chattr' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path chattr' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path chattr' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path chattr' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path chattr' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path chattr' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path chattr' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path chattr' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path chattr' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion; and not __fish_seen_argument -s h -l help' -a bash -d 'Positional Argument to completion' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion; and not __fish_seen_argument -s h -l help' -a fish -d 'Positional Argument to completion' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion; and not __fish_seen_argument -s h -l help' -a zsh -d 'Positional Argument to completion' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -s h -l help -d 'help for completion' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path completion' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -r -s f -l format -d 'format (JSON, TOML, or YAML)' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path data' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path diff' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path diff' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path diff' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path diff' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path diff' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path diff' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path diff' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path diff' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path diff' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path docs' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path docs' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path docs' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path docs' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path docs' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path docs' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path docs' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path docs' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path docs' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path doctor' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path doctor' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path doctor' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path doctor' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path doctor' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path doctor' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path doctor' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path doctor' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path doctor' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -r -s f -l format -d 'format (JSON, TOML, or YAML)' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -s r -l recursive -d 'recursive' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path dump' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -s a -l apply -d 'apply edit after editing' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -s d -l diff -d 'print diff after editing' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -s p -l prompt -d 'prompt before applying (implies --diff)' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit-config' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit-config' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit-config' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit-config' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit-config' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit-config' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit-config' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit-config' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path edit-config' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path execute-template' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path execute-template' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path execute-template' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path execute-template' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path execute-template' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path execute-template' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path execute-template' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path execute-template' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path execute-template' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path forget' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path forget' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path forget' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path forget' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path forget' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path forget' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path forget' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path forget' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path forget' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path git' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path git' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path git' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path git' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path git' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path git' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path git' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path git' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path git' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path hg' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path hg' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path hg' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path hg' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path hg' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path hg' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path hg' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path hg' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path hg' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -s x -l exact -d 'import directories exactly' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -s r -l remove-destination -d 'remove destination before import' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -r -l strip-components -d 'strip components' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path import' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -l apply -d 'update destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path init' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path merge' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path merge' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path merge' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path merge' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path merge' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path merge' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path merge' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path merge' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path merge' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -s f -l force -d 'remove without prompting' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path purge' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -s f -l force -d 'remove without prompting' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path remove' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -a bitwarden -d 'Execute the Bitwarden CLI (bw)' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -a generic -d 'Execute a generic secret command' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -a gopass -d 'Execute the gopass CLI' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -a keepassxc -d 'Execute the KeePassXC CLI (keepassxc-cli)' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -a keyring -d 'Interact with keyring' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -a lastpass -d 'Execute the LastPass CLI (lpass)' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -a onepassword -d 'Execute the 1Password CLI (op)' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -a pass -d 'Execute the pass CLI' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -a vault -d 'Execute the Hashicorp Vault CLI (vault)' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret bitwarden' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret bitwarden' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret bitwarden' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret bitwarden' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret bitwarden' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret bitwarden' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret bitwarden' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret bitwarden' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret bitwarden' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret generic' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret generic' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret generic' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret generic' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret generic' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret generic' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret generic' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret generic' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret generic' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret gopass' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret gopass' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret gopass' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret gopass' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret gopass' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret gopass' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret gopass' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret gopass' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret gopass' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keepassxc' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keepassxc' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keepassxc' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keepassxc' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keepassxc' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keepassxc' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keepassxc' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keepassxc' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keepassxc' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -a get -d 'Get a password from keyring' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -a set -d 'Set a password in keyring' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -r -l service -d 'service' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -r -l user -d 'user' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -r -l service -d 'service' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -r -l user -d 'user' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring get' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -r -l password -d 'password' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -r -l service -d 'service' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -r -l user -d 'user' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret keyring set' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret lastpass' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret lastpass' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret lastpass' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret lastpass' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret lastpass' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret lastpass' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret lastpass' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret lastpass' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret lastpass' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret onepassword' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret onepassword' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret onepassword' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret onepassword' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret onepassword' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret onepassword' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret onepassword' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret onepassword' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret onepassword' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret pass' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret pass' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret pass' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret pass' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret pass' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret pass' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret pass' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret pass' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret pass' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret vault' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret vault' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret vault' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret vault' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret vault' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret vault' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret vault' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret vault' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path secret vault' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source-path' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source-path' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source-path' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source-path' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source-path' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source-path' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source-path' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source-path' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path source-path' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path unmanaged' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path unmanaged' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path unmanaged' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path unmanaged' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path unmanaged' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path unmanaged' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path unmanaged' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path unmanaged' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path unmanaged' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -s a -l apply -d 'apply after pulling' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path update' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -s f -l force -d 'force upgrade' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -r -s m -l method -d 'set method' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -r -s o -l owner -d 'set owner' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -r -s r -l repo -d 'set repo' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path upgrade' -s v -l verbose -d 'verbose' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path verify' -r -l color -d 'colorize diffs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path verify' -r -s c -l config -d 'config file' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path verify' -l debug -d 'write debug logs' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path verify' -r -s D -l destination -d 'destination directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path verify' -s n -l dry-run -d 'dry run' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path verify' -l follow -d 'follow symlinks' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path verify' -l remove -d 'remove targets' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path verify' -r -s S -l source -d 'source directory' -complete -c chezmoi -f -n '__fish_chezmoi_seen_subcommand_path verify' -s v -l verbose -d 'verbose' + +# Remove any pre-existing completions for the program since we will be handling all of them +# TODO this cleanup is not sufficient. Fish completions are only loaded once the user triggers +# them, so the below deletion will not work as it is run too early. What else can we do? +complete -c chezmoi -e + +# The order in which the below two lines are defined is very important so that __chezmoi_prepare_completions +# is called first. It is __chezmoi_prepare_completions that sets up the __chezmoi_comp_do_file_comp variable. +# +# This completion will be run second as complete commands are added FILO. +# It triggers file completion choices when __chezmoi_comp_do_file_comp is set. +complete -c chezmoi -n 'set --query __chezmoi_comp_do_file_comp' + +# This completion will be run first as complete commands are added FILO. +# The call to __chezmoi_prepare_completions will setup both __chezmoi_comp_results abd __chezmoi_comp_do_file_comp. +# It provides the program's completion choices. +complete -c chezmoi -n '__chezmoi_prepare_completions' -f -a '$__chezmoi_comp_results' + diff --git a/completions/chezmoi.zsh b/completions/chezmoi.zsh index c60b9170a417..4d5ba8b11822 100644 --- a/completions/chezmoi.zsh +++ b/completions/chezmoi.zsh @@ -41,6 +41,7 @@ function _chezmoi { "hg:Run mercurial in the source directory" "import:Import a tar archive into the source state" "init:Setup the source directory and update the destination directory to match the target state" + "managed:List the managed files in the destination directory" "merge:Perform a three-way merge between the destination state, the source state, and the target state" "purge:Purge all of chezmoi's configuration and data" "remove:Remove a target from the source state and the destination directory" @@ -120,6 +121,9 @@ function _chezmoi { init) _chezmoi_init ;; + managed) + _chezmoi_managed + ;; merge) _chezmoi_merge ;; @@ -303,6 +307,8 @@ function _chezmoi_data { function _chezmoi_diff { _arguments \ + '(-f --format)'{-f,--format}'[format, "chezmoi" or "git"]:' \ + '--no-pager[disable pager]' \ '--color[colorize diffs]:' \ '(-c --config)'{-c,--config}'[config file]:' \ '--debug[write debug logs]' \ @@ -512,6 +518,20 @@ function _chezmoi_init { '(-v --verbose)'{-v,--verbose}'[verbose]' } +function _chezmoi_managed { + _arguments \ + '(*-i *--include)'{\*-i,\*--include}'[include]:' \ + '--color[colorize diffs]:' \ + '(-c --config)'{-c,--config}'[config file]:' \ + '--debug[write debug logs]' \ + '(-D --destination)'{-D,--destination}'[destination directory]:' \ + '(-n --dry-run)'{-n,--dry-run}'[dry run]' \ + '--follow[follow symlinks]' \ + '--remove[remove targets]' \ + '(-S --source)'{-S,--source}'[source directory]:' \ + '(-v --verbose)'{-v,--verbose}'[verbose]' +} + function _chezmoi_merge { _arguments \ '--color[colorize diffs]:' \ diff --git a/docs/CHANGES.md b/docs/CHANGES.md index c873c56c0452..bc1c174fe1eb 100644 --- a/docs/CHANGES.md +++ b/docs/CHANGES.md @@ -2,10 +2,17 @@ * [Upcoming](#upcoming) + * [Default diff format changing from `chezmoi` to `git`.](#default-diff-format-changing-from-chezmoi-to-git) * [`gpgRecipient` config variable changing to `gpg.recipient`](#gpgrecipient-config-variable-changing-to-gpgrecipient) ## Upcoming +### Default diff format changing from `chezmoi` to `git`. + +Currently chezmoi outputs diffs in its own format, containing a mix of unified +diffs and shell commands. This will be replaced with a [git format +diff](https://git-scm.com/docs/diff-format) in version 2.0.0. + ### `gpgRecipient` config variable changing to `gpg.recipient` The `gpgRecipient` config variable is changing to `gpg.recipient`. To update, diff --git a/docs/FAQ.md b/docs/FAQ.md index e25a09402367..127111cd99c3 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -4,11 +4,13 @@ * [How can I quickly check for problems with chezmoi on my machine?](#how-can-i-quickly-check-for-problems-with-chezmoi-on-my-machine) * [What are the consequences of "bare" modifications to the target files? If my `.zshrc` is managed by chezmoi and I edit `~/.zshrc` without using `chezmoi edit`, what happens?](#what-are-the-consequences-of-bare-modifications-to-the-target-files-if-my-zshrc-is-managed-by-chezmoi-and-i-edit-zshrc-without-using-chezmoi-edit-what-happens) * [How can I tell what dotfiles in my home directory aren't managed by chezmoi? Is there an easy way to have chezmoi manage a subset of them?](#how-can-i-tell-what-dotfiles-in-my-home-directory-arent-managed-by-chezmoi-is-there-an-easy-way-to-have-chezmoi-manage-a-subset-of-them) +* [How can I tell what dotfiles in my home directory are currently managed by chezmoi?](#how-can-i-tell-what-dotfiles-in-my-home-directory-are-currently-managed-by-chezmoi) * [If there's a mechanism in place for the above, is there also a way to tell chezmoi to ignore specific files or groups of files (e.g. by directory name or by glob)?](#if-theres-a-mechanism-in-place-for-the-above-is-there-also-a-way-to-tell-chezmoi-to-ignore-specific-files-or-groups-of-files-eg-by-directory-name-or-by-glob) * [If the target already exists, but is "behind" the source, can chezmoi be configured to preserve the target version before replacing it with one derived from the source?](#if-the-target-already-exists-but-is-behind-the-source-can-chezmoi-be-configured-to-preserve-the-target-version-before-replacing-it-with-one-derived-from-the-source) * [Once I've made a change to the source directory, how do I commit it?](#once-ive-made-a-change-to-the-source-directory-how-do-i-commit-it) * [How do I only run a script when a file has changed?](#how-do-i-only-run-a-script-when-a-file-has-changed) * [I've made changes to both the destination state and the source state that I want to keep. How can I keep them both?](#ive-made-changes-to-both-the-destination-state-and-the-source-state-that-i-want-to-keep-how-can-i-keep-them-both) +* [Why does chezmoi convert all my template variables to lowercase?](#why-does-chezmoi-convert-all-my-template-variables-to-lowercase) * [chezmoi's source file naming system cannot handle all possible filenames](#chezmois-source-file-naming-system-cannot-handle-all-possible-filenames) * [gpg encryption fails. What could be wrong?](#gpg-encryption-fails-what-could-be-wrong) * [I'm getting errors trying to build chezmoi from source](#im-getting-errors-trying-to-build-chezmoi-from-source) @@ -38,6 +40,10 @@ run `chezmoi apply` your modified `~/.zshrc` will remain in place. `chezmoi unmanaged` will list everything not managed by chezmoi. You can add entire directories with `chezmoi add -r`. +## How can I tell what dotfiles in my home directory are currently managed by chezmoi? + +`chezmoi managed` will list everything managed by chezmoi. + ## If there's a mechanism in place for the above, is there also a way to tell chezmoi to ignore specific files or groups of files (e.g. by directory name or by glob)? By default, chezmoi ignores everything that you haven't explicitly `chezmoi @@ -127,6 +133,13 @@ the desired behavior: state, target state, and destination state. Copy the changes you want to keep in to the source state. +## Why does chezmoi convert all my template variables to lowercase? + +This is due to a feature in +[`github.com/spf13/viper`](https://github.com/spf13/viper), the library that +chezmoi uses to read its configuration file. For more information see [this +GitHub issue issue](https://github.com/twpayne/chezmoi/issues/463). + ## chezmoi's source file naming system cannot handle all possible filenames This is correct. Certain target filenames, for example `~/dot_example`, are diff --git a/docs/HOWTO.md b/docs/HOWTO.md index 3405d51bf9d3..03cafcf286a1 100644 --- a/docs/HOWTO.md +++ b/docs/HOWTO.md @@ -8,6 +8,7 @@ * [Use templates to manage files that vary from machine to machine](#use-templates-to-manage-files-that-vary-from-machine-to-machine) * [Use completely separate config files on different machines](#use-completely-separate-config-files-on-different-machines) * [Create a config file on a new machine automatically](#create-a-config-file-on-a-new-machine-automatically) +* [Have chezmoi create a directory, but ignore its contents](#have-chezmoi-create-a-directory-but-ignore-its-contents) * [Ensure that a target is removed](#ensure-that-a-target-is-removed) * [Include a subdirectory from another repository, like Oh My Zsh](#include-a-subdirectory-from-another-repository-like-oh-my-zsh) * [Handle configuration files which are externally modified](#handle-configuration-files-which-are-externally-modified) @@ -29,6 +30,7 @@ * [Import archives](#import-archives) * [Export archives](#export-archives) * [Use a non-git version control system](#use-a-non-git-version-control-system) +* [Customize the `diff` command](#customize-the-diff-command) * [Use a merge tool other than vimdiff](#use-a-merge-tool-other-than-vimdiff) * [Migrate from a dotfile manager that uses symlinks](#migrate-from-a-dotfile-manager-that-uses-symlinks) @@ -134,6 +136,9 @@ to machine. For example, for your home machine: [data] email = "john@home.org" +Note that all variable names will be converted to lowercase. This is due to a +feature of a library used by chezmoi. + If you intend to store private data (e.g. access tokens) in `~/.config/chezmoi/chezmoi.toml`, make sure it has permissions `0600`. @@ -142,10 +147,11 @@ If you prefer, you can use any format supported by includes JSON, YAML, and TOML. Variable names must start with a letter and be followed by zero or more letters or digits. -Then, add `~/.gitconfig` to chezmoi using the `--template` flag to automatically -turn it in to a template: +Then, add `~/.gitconfig` to chezmoi using the `--autotemplate` flag to turn it +into a template and automatically detect variables from the `data` section +of your `~/.config/chezmoi/chezmoi.toml` file: - chezmoi add --template --autotemplate ~/.gitconfig + chezmoi add --autotemplate ~/.gitconfig You can then open the template (which will be saved in the file `~/.local/share/chezmoi/dot_gitconfig.tmpl`): @@ -157,9 +163,8 @@ The file should look something like: [user] email = "{{ .email }}" -The `--autotemplate` flag to `chezmoi add` above instructs chezmoi to generate a -template by substituting variables from the `data` section of your -`~/.config/chezmoi/chezmoi.toml` file. +To disable automatic variable detection, use the `--template` or `-T` option to +`chezmoi add` instead of `--autotemplate`. Templates are often used to capture machine-specifc differences. For example, in your `~/.local/share/chezmoi/dot_bashrc.tmpl` you might have: @@ -291,6 +296,26 @@ Specifically, if you have `.chezmoi.toml.tmpl` that looks like this: Then `chezmoi init` will create an initial `chezmoi.toml` using this template. `promptString` is a special function that prompts the user (you) for a value. +## Have chezmoi create a directory, but ignore its contents + +If you want chezmoi to create a directory, but ignore its contents, say `~/src`, +first run: + + mkdir -p $(chezmoi source-path)/src + +This creates the directory in the source state, which means that chezmoi will +create it (if it does not already exist) when you run `chezmoi apply`. + +However, as this is an empty directory it will be ignored by git. So, create a +file in the directory in the source state that will be seen by git (so git does +not ignore the directory) but ignored by chezmoi (so chezmoi does not include it +in the target state): + + touch $(chezmoi source-path)/src/.keep + +chezmoi automatically creates `.keep` files when you add an empty directory with +`chezmoi add`. + ## Ensure that a target is removed Create a file called `.chezmoiremove` in the source directory containing a list @@ -742,6 +767,19 @@ The source VCS command is used in the chezmoi commands `init`, `source`, and you'd like to see your VCS better supported, please [open an issue on GitHub](https://github.com/twpayne/chezmoi/issues/new/choose). +## Customize the `diff` command + +By default, chezmoi uses a built-in diff. You can change the format, and/or pipe +the output into a pager of your choice. For example, to use +[`diff-so-fancy`](https://github.com/so-fancy/diff-so-fancy) specify: + + [diff] + format = "git" + pager = "diff-so-fancy" + +The format can also be set with the `--format` option to the `diff` command, and +the pager can be disabled using `--no-pager`. + ## Use a merge tool other than vimdiff By default, chezmoi uses vimdiff, but you can use any merge tool of your choice. diff --git a/docs/MEDIA.md b/docs/MEDIA.md new file mode 100644 index 000000000000..819ba073b830 --- /dev/null +++ b/docs/MEDIA.md @@ -0,0 +1,16 @@ +# chezmoi in the media + + + +| Date | Version | Format | Link | +| ---------- | ------- | ------------ | ------------------------------------------------------------------------------------------------------------------------- | +| 2020-04-16 | 1.17.19 | Text (FR) | [Chezmoi, visite guidée](https://blog.wescale.fr/2020/04/16/chezmoi-visite-guidee/) | +| 2020-04-03 | 1.7.17 | Text | [Fedora Magazine: Take back your dotfiles with Chezmoi](https://fedoramagazine.org/take-back-your-dotfiles-with-chezmoi/) | +| 2020-03-12 | 1.7.16 | Video | [Manging Dotfiles with ChezMoi](https://www.youtube.com/watch?v=HXx6ugA98Qo) | +| 2019-11-20 | 1.7.2 | Audio/video | [FLOSS weekly episode 556: chezmoi](https://twit.tv/shows/floss-weekly/episodes/556) | +| 2019-01-10 | 0.0.11 | Text | [Linux Fu: The kitchen sync](https://hackaday.com/2019/01/10/linux-fu-the-kitchen-sync/) | + +To add your article to this page please either [open an +issue](https://github.com/twpayne/chezmoi/issues/new/choose) or submit a pull +request that modifies this file +([`docs/MEDIA.md`](https://github.com/twpayne/chezmoi/blob/master/docs/MEDIA.md)). \ No newline at end of file diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 1efcbc1fc824..0bb03d4380f5 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -48,6 +48,7 @@ Manage your dotfiles securely across multiple machines. * [`init` [*repo*]](#init-repo) * [`import` *filename*](#import-filename) * [`manage` *targets*](#manage-targets) + * [`managed`](#managed) * [`merge` *targets*](#merge-targets) * [`purge`](#purge) * [`remove` *targets*](#remove-targets) @@ -179,10 +180,13 @@ The following configuration variables are available: | `color` | string | `auto` | Colorize diffs | | `data` | any | *none* | Template data | | `destDir` | string | `~` | Destination directory | +| `diff.format` | string | `chezmoi` | Diff format, either `chezmoi` or `git` | +| `diff.pager` | string | *none* | Pager | | `dryRun` | bool | `false` | Dry run mode | | `follow` | bool | `false` | Follow symlinks | | `genericSecret.command` | string | *none* | Generic secret command | | `gopass.command` | string | `gopass` | gopass CLI command | +| `gpg.command` | string | `gpg` | GPG CLI command | | `gpg.recipient` | string | *none* | GPG recipient | | `gpg.symmetric` | bool | `false` | Use symmetric GPG encryption | | `keepassxc.args` | []string | *none* | Extra args to KeePassXC CLI command | @@ -264,10 +268,9 @@ config file formats. ### `.chezmoiignore` If a file called `.chezmoiignore` exists in the source state then it is -interpreted as a set of patterns to ignore. Patterns are matched using the Go -standard libary's [`filepath.Match` pattern -syntax](https://pkg.go.dev/path/filepath?tab=doc#Match) and match against the -target path, not the source path. +interpreted as a set of patterns to ignore. Patterns are matched using +[`doublestar.PathMatch`](https://pkg.go.dev/github.com/bmatcuk/doublestar?tab=doc#PathMatch) +and match against the target path, not the source path. Patterns can be excluded by prefixing them with a `!` character. All excludes take priority over all includes. @@ -342,6 +345,12 @@ Add *targets* to the source state. If any target is already in the source state, then its source state is replaced with its current state in the destination directory. The `add` command accepts additional flags: +#### `--autotemplate` + +Automatically generate a template by replacing strings with variable names from +the `data` section of the config file. Longer subsitutions occur before shorter +ones. This implies the `--template` option. + #### `-e`, `--empty` Set the `empty` attribute on added files. @@ -364,10 +373,7 @@ Recursively add all files, directories, and symlinks. #### `-T`, `--template` -Set the `template` attribute on added files and symlinks. In addition, if the -`--autotemplate` flag is set, chezmoi attempts to automatically generate the -template by replacing any template data values with the equivalent template data -keys. Longer subsitutions occur before shorter ones. +Set the `template` attribute on added files and symlinks. #### `add` examples @@ -469,15 +475,38 @@ Print the computed template data in the given format. The accepted formats are ### `diff` [*targets*] -Print the approximate shell commands required to ensure that *targets* in the -destination directory match the target state. If no targets are specified, print -the commands required for all targets. It is equivalent to `chezmoi apply ---dry-run --verbose`. +Print the difference between the target state and the destination state for +*targets*. If no targets are specified, print the differences for all targets. + +If a `diff.pager` command is set in the configuration file then the output will +be piped into it. + +#### `-f`, `--format` *format* + +Print the diff in *format*. The format can be set with the `diff.format` +variable in the configuration file. Valid formats are: + +##### `chezmoi` + +A mix of unified diffs and pseudo shell commands, equivalent to `chezmoi apply +--dry-run --verbose`. They can be colorized and include scripts. + +##### `git` + +A [git format diff](https://git-scm.com/docs/diff-format), without color and not +including scripts. In version 2.0.0 of chezmoi, `git` format diffs will become +the default and support color and scripts and the `chezmoi` format will be +removed. + +#### `--no-pager` + +Do not use the pager. #### `diff` examples chezmoi diff chezmoi diff ~/.bashrc + chezmoi diff --format=git ### `docs` [*regexp*] @@ -640,6 +669,25 @@ Strip *n* leading components from paths. `manage` is an alias for `add` for symmetry with `unmanage`. +### `managed` + +List all managed entries in the destination directory in alphabetical order. + +#### `-i`, `--include` *types* + +Only list entries of type *types*. *types* is a comma-separated list of types of +entry to include. Valid types are `dirs`, `files`, and `symlinks` which can be +abbreviated to `d`, `f`, and `s` respectively. By default, `manage` will list +entries of all types. + +#### `managed` examples + + chezmoi managed + chezmoi managed --include=files + chezmoi managed --include=files,symlinks + chezmoi managed -i d + chezmoi managed -i d,f + ### `merge` *targets* Perform a three-way merge between the destination state, the source state, and diff --git a/go.mod b/go.mod index 8305d729b01d..5281d10b1302 100644 --- a/go.mod +++ b/go.mod @@ -7,48 +7,48 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible github.com/alecthomas/chroma v0.7.1 // indirect + github.com/bmatcuk/doublestar v1.2.4 github.com/charmbracelet/glamour v0.1.0 github.com/coreos/go-semver v0.3.0 github.com/dlclark/regexp2 v1.2.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/golang/protobuf v1.3.5 // indirect + github.com/go-git/go-git/v5 v5.0.0 + github.com/golang/protobuf v1.4.0 // indirect github.com/google/go-github/v26 v26.1.3 github.com/google/renameio v0.1.0 github.com/google/uuid v1.1.1 // indirect github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 // indirect - github.com/huandu/xstrings v1.3.0 // indirect - github.com/imdario/mergo v0.3.8 // indirect + github.com/huandu/xstrings v1.3.1 // indirect + github.com/imdario/mergo v0.3.9 // indirect github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect github.com/mattn/go-isatty v0.0.11 // indirect - github.com/mattn/go-runewidth v0.0.8 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.2.1 // indirect + github.com/mitchellh/mapstructure v1.2.2 // indirect github.com/mitchellh/reflectwalk v1.0.1 // indirect - github.com/pelletier/go-toml v1.6.0 + github.com/pelletier/go-toml v1.7.0 github.com/pkg/diff v0.0.0-20190930165518-531926345625 + github.com/sergi/go-diff v1.1.0 github.com/spf13/afero v1.2.2 // indirect github.com/spf13/cast v1.3.1 // indirect - github.com/spf13/cobra v0.0.6 + github.com/spf13/cobra v1.0.0 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.6.2 + github.com/spf13/viper v1.6.3 github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.4.0 github.com/twpayne/go-shell v0.1.1 github.com/twpayne/go-vfs v1.3.6 github.com/twpayne/go-vfsafero v1.0.0 github.com/twpayne/go-xdg/v3 v3.1.0 - github.com/yuin/goldmark v1.1.25 // indirect + github.com/yuin/goldmark v1.1.28 // indirect github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717 go.etcd.io/bbolt v1.3.4 - golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 - golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect + golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 + golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d + golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 google.golang.org/appengine v1.6.5 // indirect gopkg.in/ini.v1 v1.55.0 // indirect gopkg.in/yaml.v2 v2.2.8 ) - -// Temporary while waiting for https://github.com/spf13/cobra/pull/754 to be merged -replace github.com/spf13/cobra => github.com/twpayne/cobra v0.0.6 diff --git a/go.sum b/go.sum index 5c5241c77ea0..0798ddded254 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,7 @@ github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZC github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= @@ -27,15 +28,20 @@ github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkx github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bmatcuk/doublestar v1.2.4 h1:CXTEjc5/WPKLJEqrS9D0IQAUhpjAIJuUQ4XtXG+zmEU= +github.com/bmatcuk/doublestar v1.2.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/charmbracelet/glamour v0.1.0 h1:BHCtc+YJjoBjNUnFKBtXyyM4Bp9u7L2kf49qV+/AGYw= github.com/charmbracelet/glamour v0.1.0/go.mod h1:Z1C2JkVGBom/RYfoKcPBZ81lHMR3xp3W6OCLNWWEIMc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= @@ -43,6 +49,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= @@ -57,11 +64,19 @@ github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg= +github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -77,14 +92,23 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github/v26 v26.1.3 h1:n03e8IGgLdD78L+ETWxvqpBIBWEZLlTBCQVU2yImw1o= github.com/google/go-github/v26 v26.1.3/go.mod h1:v6/FmX9au22j4CtYxnMhJJkP+JfOQDXALk7hI+MPDNM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= @@ -104,17 +128,20 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hectane/go-acl v0.0.0-20190523051433-dfeb47f3e2ef/go.mod h1:xk/21OELzVCkl0NZCoB+eLISXe1p+YDiha8WaQDD1d8= github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 h1:S4qyfL2sEm5Budr4KVMyEniCy+PbS55651I/a+Kn/NQ= github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E= -github.com/huandu/xstrings v1.3.0 h1:gvV6jG9dTgFEncxo+AF7PH6MZXi/vZl25owA/8Dg8Wo= -github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= +github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -124,6 +151,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23 h1:Wp7NjqGKGN9te9N/rvXYRhlVcrulGdxnz8zadXWs7fc= github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= @@ -140,8 +169,8 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= -github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= @@ -150,23 +179,28 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.2.1 h1:pSevhhKCEjOuZHQWDBYAHxcimg60m1fGFj6atY7zAdE= -github.com/mitchellh/mapstructure v1.2.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= +github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/muesli/reflow v0.0.0-20191216070243-e5efeac4e302 h1:jOh3Kh03uOFkRPV3PI4Am5tqACv2aELgbPgr7YgNX00= github.com/muesli/reflow v0.0.0-20191216070243-e5efeac4e302/go.mod h1:I9bWAt7QTg/que/qmUCJBGlj7wEq8OAFBjPNjc6xK4I= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= -github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pkg/diff v0.0.0-20190930165518-531926345625 h1:b5m9ubdpxvfhiJnF64/W1rUTSUOzKHipjy5wOWsZCBM= github.com/pkg/diff v0.0.0-20190930165518-531926345625/go.mod h1:kFj35MyHn14a6pIgWhm46KVjJr5CHys3eEYxkuKD1EI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -186,6 +220,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -203,6 +239,8 @@ github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= @@ -212,8 +250,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= -github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= +github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= @@ -227,8 +265,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/twpayne/cobra v0.0.6 h1:zJ26aak/ChId/jCdBCgrqJR6hixAOGLC1fo+ONtZPQc= -github.com/twpayne/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/twpayne/go-shell v0.1.1 h1:Kr1hSEFrPBTRmhOW8woaj7ZxV3/OH7Qefg86OFJ5DFc= github.com/twpayne/go-shell v0.1.1/go.mod h1:H/gzux0DOH5jsjQSHXs6rs2Onxy+V4j6ycZTOulC0l8= github.com/twpayne/go-vfs v1.0.1/go.mod h1:OIXA6zWkcn7Jk46XT7ceYqBMeIkfzJ8WOBhGJM0W4y8= @@ -242,12 +278,13 @@ github.com/twpayne/go-xdg/v3 v3.1.0/go.mod h1:z6/LkoG2gtuzrsxEqPRoEjccS5Q35GK+lg github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.19 h1:0s2/60x0XsFCXHeFut+F3azDVAAyIMyUfJRbRexiTYs= github.com/yuin/goldmark v1.1.19/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.25 h1:isv+Q6HQAmmL2Ofcmg8QauBmDPlUUnSoNhEcC940Rds= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.28 h1:3Ksz4BbKZVlaGbkXzHxoazZzASQKsfUuOZPr5CNxnC4= +github.com/yuin/goldmark v1.1.28/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717 h1:3M/uUZajYn/082wzUajekePxpUAZhMTfXvI9R+26SJ0= github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -257,10 +294,12 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 h1:TjszyFsQsyZNHwdVdZ5m7bjmreu0znc2kRYsEml9/Ww= -golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 h1:DOmugCavvUtnUD114C1Wh+UgTgQZ4pMLzXxi1pSt+/Y= +golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -274,8 +313,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2eP golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= @@ -291,6 +331,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5 h1:sM3evRHxE/1RuMe1FYAL3j7C7fUfIjkbE+NiDAYUF8U= golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -298,8 +339,10 @@ golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d h1:62ap6LNOjDU6uGmKXHJbSfciMoV+FeI1sRXx/pLDL44= -golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -310,6 +353,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -318,16 +363,26 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/internal/chezmoi/chezmoi.go b/internal/chezmoi/chezmoi.go index cec6f52a830c..892584439b8a 100644 --- a/internal/chezmoi/chezmoi.go +++ b/internal/chezmoi/chezmoi.go @@ -49,6 +49,7 @@ type ApplyOptions struct { // An Entry is either a Dir, a File, or a Symlink. type Entry interface { + AppendAllEntries(allEntries []Entry) []Entry Apply(fs vfs.FS, mutator Mutator, follow bool, applyOptions *ApplyOptions) error ConcreteValue(ignore func(string) bool, sourceDir string, umask os.FileMode, recursive bool) (interface{}, error) Evaluate(ignore func(string) bool) error diff --git a/internal/chezmoi/dir.go b/internal/chezmoi/dir.go index 87c7ab672845..a25db354db59 100644 --- a/internal/chezmoi/dir.go +++ b/internal/chezmoi/dir.go @@ -85,6 +85,15 @@ func newDir(sourceName string, targetName string, exact bool, perm os.FileMode) } } +// AppendAllEntries appends all Entries in d to allEntries. +func (d *Dir) AppendAllEntries(allEntries []Entry) []Entry { + allEntries = append(allEntries, d) + for _, entry := range d.Entries { + allEntries = entry.AppendAllEntries(allEntries) + } + return allEntries +} + // Apply ensures that destDir in fs matches d. func (d *Dir) Apply(fs vfs.FS, mutator Mutator, follow bool, applyOptions *ApplyOptions) error { if applyOptions.Ignore(d.targetName) { diff --git a/internal/chezmoi/file.go b/internal/chezmoi/file.go index d44dc8977260..0b3fd0496f55 100644 --- a/internal/chezmoi/file.go +++ b/internal/chezmoi/file.go @@ -125,6 +125,11 @@ func (fa FileAttributes) SourceName() string { return sourceName } +// AppendAllEntries appends all f to allEntries. +func (f *File) AppendAllEntries(allEntries []Entry) []Entry { + return append(allEntries, f) +} + // Apply ensures that the state of targetPath in fs matches f. func (f *File) Apply(fs vfs.FS, mutator Mutator, follow bool, applyOptions *ApplyOptions) error { if applyOptions.Ignore(f.targetName) { diff --git a/internal/chezmoi/gitdiffmutator.go b/internal/chezmoi/gitdiffmutator.go new file mode 100644 index 000000000000..f706ef1127ba --- /dev/null +++ b/internal/chezmoi/gitdiffmutator.go @@ -0,0 +1,262 @@ +package chezmoi + +import ( + "os" + "os/exec" + "strings" + "time" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/format/diff" + "github.com/sergi/go-diff/diffmatchpatch" +) + +// A GitDiffMutator wraps a Mutator and logs all of the actions it would execute +// as a git diff. +type GitDiffMutator struct { + m Mutator + prefix string + unifiedEncoder *diff.UnifiedEncoder +} + +// NewGitDiffMutator returns a new GitDiffMutator. +func NewGitDiffMutator(unifiedEncoder *diff.UnifiedEncoder, m Mutator, prefix string) *GitDiffMutator { + return &GitDiffMutator{ + m: m, + prefix: prefix, + unifiedEncoder: unifiedEncoder, + } +} + +// Chmod implements Mutator.Chmod. +func (m *GitDiffMutator) Chmod(name string, mode os.FileMode) error { + fromFileMode, info, err := m.getFileMode(name) + if err != nil { + return err + } + // Assume that we're only changing permissions. + toFileMode, err := filemode.NewFromOSFileMode(info.Mode()&^os.ModePerm | mode) + if err != nil { + return err + } + path := m.trimPrefix(name) + return m.unifiedEncoder.Encode(&gitDiffPatch{ + filePatches: []diff.FilePatch{ + &gitDiffFilePatch{ + from: &gitDiffFile{ + fileMode: fromFileMode, + path: path, + hash: plumbing.ZeroHash, + }, + to: &gitDiffFile{ + fileMode: toFileMode, + path: path, + hash: plumbing.ZeroHash, + }, + }, + }, + }) +} + +// IdempotentCmdOutput implements Mutator.IdempotentCmdOutput. +func (m *GitDiffMutator) IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) { + return m.m.IdempotentCmdOutput(cmd) +} + +// Mkdir implements Mutator.Mkdir. +func (m *GitDiffMutator) Mkdir(name string, perm os.FileMode) error { + toFileMode, err := filemode.NewFromOSFileMode(os.ModeDir | perm) + if err != nil { + return err + } + return m.unifiedEncoder.Encode(&gitDiffPatch{ + filePatches: []diff.FilePatch{ + &gitDiffFilePatch{ + to: &gitDiffFile{ + fileMode: toFileMode, + path: m.trimPrefix(name), + hash: plumbing.ZeroHash, + }, + }, + }, + }) +} + +// RemoveAll implements Mutator.RemoveAll. +func (m *GitDiffMutator) RemoveAll(name string) error { + fromFileMode, _, err := m.getFileMode(name) + if err != nil { + return err + } + return m.unifiedEncoder.Encode(&gitDiffPatch{ + filePatches: []diff.FilePatch{ + &gitDiffFilePatch{ + from: &gitDiffFile{ + fileMode: fromFileMode, + path: m.trimPrefix(name), + hash: plumbing.ZeroHash, + }, + }, + }, + }) +} + +// RunCmd implements Mutator.RunCmd. +func (m *GitDiffMutator) RunCmd(cmd *exec.Cmd) error { + // FIXME write scripts to diff + return nil +} + +// Stat implements Mutator.Stat. +func (m *GitDiffMutator) Stat(name string) (os.FileInfo, error) { + return m.m.Stat(name) +} + +// Rename implements Mutator.Rename. +func (m *GitDiffMutator) Rename(oldpath, newpath string) error { + fileMode, _, err := m.getFileMode(oldpath) + if err != nil { + return err + } + return m.unifiedEncoder.Encode(&gitDiffPatch{ + filePatches: []diff.FilePatch{ + &gitDiffFilePatch{ + from: &gitDiffFile{ + fileMode: fileMode, + path: m.trimPrefix(oldpath), + hash: plumbing.ZeroHash, + }, + to: &gitDiffFile{ + fileMode: fileMode, + path: m.trimPrefix(newpath), + hash: plumbing.ZeroHash, + }, + }, + }, + }) +} + +// WriteFile implements Mutator.WriteFile. +func (m *GitDiffMutator) WriteFile(filename string, data []byte, perm os.FileMode, currData []byte) error { + fileMode, _, err := m.getFileMode(filename) + if err != nil { + return err + } + path := m.trimPrefix(filename) + isBinary := isBinary(currData) || isBinary(data) + var chunks []diff.Chunk + if !isBinary { + chunks = diffChunks(string(currData), string(data)) + } + return m.unifiedEncoder.Encode(&gitDiffPatch{ + filePatches: []diff.FilePatch{ + &gitDiffFilePatch{ + isBinary: isBinary, + from: &gitDiffFile{ + fileMode: fileMode, + path: path, + hash: plumbing.ComputeHash(plumbing.BlobObject, currData), + }, + to: &gitDiffFile{ + fileMode: fileMode, + path: path, + hash: plumbing.ComputeHash(plumbing.BlobObject, data), + }, + chunks: chunks, + }, + }, + }) +} + +// WriteSymlink implements Mutator.WriteSymlink. +func (m *GitDiffMutator) WriteSymlink(oldname, newname string) error { + return m.unifiedEncoder.Encode(&gitDiffPatch{ + filePatches: []diff.FilePatch{ + &gitDiffFilePatch{ + to: &gitDiffFile{ + fileMode: filemode.Symlink, + path: m.trimPrefix(newname), + hash: plumbing.ComputeHash(plumbing.BlobObject, []byte(oldname)), + }, + chunks: []diff.Chunk{ + &gitDiffChunk{ + content: oldname, + operation: diff.Add, + }, + }, + }, + }, + }) +} + +func (m *GitDiffMutator) getFileMode(name string) (filemode.FileMode, os.FileInfo, error) { + info, err := m.m.Stat(name) + if err != nil { + return filemode.Empty, nil, err + } + fileMode, err := filemode.NewFromOSFileMode(info.Mode()) + return fileMode, info, err +} + +func (m *GitDiffMutator) trimPrefix(path string) string { + return strings.TrimPrefix(path, m.prefix) +} + +var gitDiffOperation = map[diffmatchpatch.Operation]diff.Operation{ + diffmatchpatch.DiffDelete: diff.Delete, + diffmatchpatch.DiffEqual: diff.Equal, + diffmatchpatch.DiffInsert: diff.Add, +} + +type gitDiffChunk struct { + content string + operation diff.Operation +} + +func (c *gitDiffChunk) Content() string { return c.content } +func (c *gitDiffChunk) Type() diff.Operation { return c.operation } + +type gitDiffFile struct { + hash plumbing.Hash + fileMode filemode.FileMode + path string +} + +func (f *gitDiffFile) Hash() plumbing.Hash { return f.hash } +func (f *gitDiffFile) Mode() filemode.FileMode { return f.fileMode } +func (f *gitDiffFile) Path() string { return f.path } + +type gitDiffFilePatch struct { + isBinary bool + from, to diff.File + chunks []diff.Chunk +} + +func (fp *gitDiffFilePatch) IsBinary() bool { return fp.isBinary } +func (fp *gitDiffFilePatch) Files() (diff.File, diff.File) { return fp.from, fp.to } +func (fp *gitDiffFilePatch) Chunks() []diff.Chunk { return fp.chunks } + +type gitDiffPatch struct { + filePatches []diff.FilePatch + message string +} + +func (p *gitDiffPatch) FilePatches() []diff.FilePatch { return p.filePatches } +func (p *gitDiffPatch) Message() string { return p.message } + +func diffChunks(from, to string) []diff.Chunk { + dmp := diffmatchpatch.New() + dmp.DiffTimeout = time.Second + fromRunes, toRunes, runesToLines := dmp.DiffLinesToRunes(from, to) + diffs := dmp.DiffCharsToLines(dmp.DiffMainRunes(fromRunes, toRunes, false), runesToLines) + chunks := make([]diff.Chunk, 0, len(diffs)) + for _, d := range diffs { + chunk := &gitDiffChunk{ + content: d.Text, + operation: gitDiffOperation[d.Type], + } + chunks = append(chunks, chunk) + } + return chunks +} diff --git a/internal/chezmoi/gitdiffmutator_test.go b/internal/chezmoi/gitdiffmutator_test.go new file mode 100644 index 000000000000..6ed0eed113be --- /dev/null +++ b/internal/chezmoi/gitdiffmutator_test.go @@ -0,0 +1,11 @@ +package chezmoi + +import "github.com/go-git/go-git/v5/plumbing/format/diff" + +var ( + _ Mutator = &GitDiffMutator{} + _ diff.Chunk = &gitDiffChunk{} + _ diff.File = &gitDiffFile{} + _ diff.FilePatch = &gitDiffFilePatch{} + _ diff.Patch = &gitDiffPatch{} +) diff --git a/internal/chezmoi/gpg.go b/internal/chezmoi/gpg.go index ec88afecef21..32c0af8694cd 100644 --- a/internal/chezmoi/gpg.go +++ b/internal/chezmoi/gpg.go @@ -9,6 +9,7 @@ import ( // GPG interfaces with gpg. type GPG struct { + Command string Recipient string Symmetric bool } @@ -28,8 +29,9 @@ func (g *GPG) Decrypt(filename string, ciphertext []byte) ([]byte, error) { return nil, err } + //nolint:gosec cmd := exec.Command( - "gpg", + g.Command, "--output", outputFilename, "--quiet", "--decrypt", inputFilename, @@ -73,7 +75,9 @@ func (g *GPG) Encrypt(filename string, plaintext []byte) ([]byte, error) { args = append(args, "--encrypt") } args = append(args, filename) - cmd := exec.Command("gpg", args...) + + //nolint:gosec + cmd := exec.Command(g.Command, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/internal/chezmoi/patternset.go b/internal/chezmoi/patternset.go index 898222abf309..f6550ec7b1d8 100644 --- a/internal/chezmoi/patternset.go +++ b/internal/chezmoi/patternset.go @@ -1,6 +1,8 @@ package chezmoi -import "path/filepath" +import ( + "github.com/bmatcuk/doublestar" +) // An PatternSet is a set of patterns. type PatternSet struct { @@ -18,7 +20,7 @@ func NewPatternSet() *PatternSet { // Add adds a pattern to ps. func (ps *PatternSet) Add(pattern string, include bool) error { - if _, err := filepath.Match(pattern, ""); err != nil { + if _, err := doublestar.PathMatch(pattern, ""); err != nil { return nil } if include { @@ -32,12 +34,12 @@ func (ps *PatternSet) Add(pattern string, include bool) error { // Match returns if name matches any pattern in ps. func (ps *PatternSet) Match(name string) bool { for pattern := range ps.excludes { - if ok, _ := filepath.Match(pattern, name); ok { + if ok, _ := doublestar.PathMatch(pattern, name); ok { return false } } for pattern := range ps.includes { - if ok, _ := filepath.Match(pattern, name); ok { + if ok, _ := doublestar.PathMatch(pattern, name); ok { return true } } diff --git a/internal/chezmoi/patternset_test.go b/internal/chezmoi/patternset_test.go index 76b1bfdf5f17..1bdde54c5c6c 100644 --- a/internal/chezmoi/patternset_test.go +++ b/internal/chezmoi/patternset_test.go @@ -1,6 +1,7 @@ package chezmoi import ( + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -53,6 +54,17 @@ func TestPatternSet(t *testing.T) { "baz": false, }, }, + { + name: "doublestar", + ps: mustNewPatternSet(t, map[string]bool{ + "**/foo": true, + }), + expectMatches: map[string]bool{ + "foo": true, + filepath.Join("bar", "foo"): true, + filepath.Join("baz", "bar", "foo"): true, + }, + }, } { t.Run(tc.name, func(t *testing.T) { for s, expectMatch := range tc.expectMatches { diff --git a/internal/chezmoi/script.go b/internal/chezmoi/script.go index 9036b685d4b0..9bbf291e74a0 100644 --- a/internal/chezmoi/script.go +++ b/internal/chezmoi/script.go @@ -85,6 +85,11 @@ func (sa ScriptAttributes) SourceName() string { return sourceName } +// AppendAllEntries returns allEntries unchanged. +func (s *Script) AppendAllEntries(allEntries []Entry) []Entry { + return allEntries +} + // Apply runs s. func (s *Script) Apply(fs vfs.FS, mutator Mutator, follow bool, applyOptions *ApplyOptions) error { if applyOptions.Ignore(s.targetName) { diff --git a/internal/chezmoi/symlink.go b/internal/chezmoi/symlink.go index ae5de6266873..82d0543dcb18 100644 --- a/internal/chezmoi/symlink.go +++ b/internal/chezmoi/symlink.go @@ -27,6 +27,11 @@ type symlinkConcreteValue struct { Linkname string `json:"linkname" yaml:"linkname"` } +// AppendAllEntries appends all f to allEntries. +func (s *Symlink) AppendAllEntries(allEntries []Entry) []Entry { + return append(allEntries, s) +} + // Apply ensures that the state of s's target in fs matches s. func (s *Symlink) Apply(fs vfs.FS, mutator Mutator, follow bool, applyOptions *ApplyOptions) error { if applyOptions.Ignore(s.targetName) { diff --git a/internal/chezmoi/targetstate.go b/internal/chezmoi/targetstate.go index 35602ae271b0..bd64338e8ab6 100644 --- a/internal/chezmoi/targetstate.go +++ b/internal/chezmoi/targetstate.go @@ -32,6 +32,7 @@ type AddOptions struct { Empty bool Encrypt bool Exact bool + Recursive bool Template bool AutoTemplate bool } @@ -226,7 +227,6 @@ func (ts *TargetState) Add(fs vfs.FS, addOptions AddOptions, targetPath string, if err != nil { return err } - empty := len(infos) == 0 private, err := IsPrivate(fs, targetPath, perm&077 == 0) if err != nil { return err @@ -234,7 +234,11 @@ func (ts *TargetState) Add(fs vfs.FS, addOptions AddOptions, targetPath string, if private { perm &^= 077 } - return ts.addDir(targetName, entries, parentDirSourceName, addOptions.Exact, perm, empty, mutator) + // If the directory is empty, or the directory was not added + // recursively, add a .keep file so the directory is managed by git. + // chezmoi will ignore the .keep file as it begins with a dot. + createKeepFile := len(infos) == 0 || !addOptions.Recursive + return ts.addDir(targetName, entries, parentDirSourceName, addOptions.Exact, perm, createKeepFile, mutator) case info.Mode().IsRegular(): if info.Size() == 0 && !addOptions.Empty { entry, err := ts.Get(fs, targetPath) @@ -280,6 +284,15 @@ func (ts *TargetState) Add(fs vfs.FS, addOptions AddOptions, targetPath string, } } +// AllEntries returns all Entrys in ts. +func (ts *TargetState) AllEntries() []Entry { + var allEntries []Entry + for _, entry := range ts.Entries { + allEntries = entry.AppendAllEntries(allEntries) + } + return allEntries +} + // Apply ensures that ts.DestDir in fs matches ts. func (ts *TargetState) Apply(fs vfs.FS, mutator Mutator, follow bool, applyOptions *ApplyOptions) error { if applyOptions.Remove { @@ -571,7 +584,7 @@ func (ts *TargetState) Populate(fs vfs.FS, options *PopulateOptions) error { }) } -func (ts *TargetState) addDir(targetName string, entries map[string]Entry, parentDirSourceName string, exact bool, perm os.FileMode, empty bool, mutator Mutator) error { +func (ts *TargetState) addDir(targetName string, entries map[string]Entry, parentDirSourceName string, exact bool, perm os.FileMode, createKeepFile bool, mutator Mutator) error { name := filepath.Base(targetName) if entry, ok := entries[name]; ok { if _, ok = entry.(*Dir); !ok { @@ -591,10 +604,7 @@ func (ts *TargetState) addDir(targetName string, entries map[string]Entry, paren if err := mutator.Mkdir(filepath.Join(ts.SourceDir, sourceName), 0777&^ts.Umask); err != nil { return err } - // If the directory is empty, add a .keep file so the directory is - // managed by git. Chezmoi will ignore the .keep file as it begins with - // a dot. - if empty { + if createKeepFile { if err := mutator.WriteFile(filepath.Join(ts.SourceDir, sourceName, ".keep"), nil, 0666&^ts.Umask, nil); err != nil { return err } @@ -824,8 +834,8 @@ func (ts *TargetState) importHeader(r io.Reader, importTAROptions ImportTAROptio switch header.Typeflag { case tar.TypeDir: perm := os.FileMode(header.Mode).Perm() - empty := false // FIXME don't assume directory is empty - return ts.addDir(targetName, entries, parentDirSourceName, importTAROptions.Exact, perm, empty, mutator) + createKeepFile := false // FIXME don't assume that we don't need a keep file + return ts.addDir(targetName, entries, parentDirSourceName, importTAROptions.Exact, perm, createKeepFile, mutator) case tar.TypeReg: info := header.FileInfo() contents, err := ioutil.ReadAll(r) diff --git a/main.go b/main.go index ca17fcfb7d51..e6c6f5a2974d 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,4 @@ -//go:generate go run ./internal/generate-assets -o cmd/docs.gen.go -tags=!noembeddocs docs/CHANGES.md docs/CONTRIBUTING.md docs/FAQ.md docs/HOWTO.md docs/INSTALL.md docs/QUICKSTART.md docs/REFERENCE.md +//go:generate go run ./internal/generate-assets -o cmd/docs.gen.go -tags=!noembeddocs docs/CHANGES.md docs/CONTRIBUTING.md docs/FAQ.md docs/HOWTO.md docs/INSTALL.md docs/MEDIA.md docs/QUICKSTART.md docs/REFERENCE.md //go:generate go run ./internal/generate-assets -o cmd/templates.gen.go assets/templates/COMMIT_MESSAGE.tmpl //go:generate go run ./internal/generate-helps -o cmd/helps.gen.go -i docs/REFERENCE.md diff --git a/v2/chezmoi/attributes.go b/v2/chezmoi/attributes.go new file mode 100644 index 000000000000..e86f1e7ca59d --- /dev/null +++ b/v2/chezmoi/attributes.go @@ -0,0 +1,175 @@ +package chezmoi + +import ( + "strings" +) + +// A SourceFileType is a source file type. +type SourceFileType int + +// Source file types. +const ( + SourceFileTypeFile SourceFileType = iota + SourceFileTypeScript + SourceFileTypeSymlink +) + +// DirAttributes holds attributes parsed from a source directory name. +type DirAttributes struct { + Name string + Exact bool + Private bool +} + +// A FileAttributes holds attributes parsed from a source file name. +type FileAttributes struct { + Name string + Type SourceFileType + Empty bool + Encrypted bool + Executable bool + Once bool + Private bool + Template bool +} + +// ParseDirAttributes parses a single directory name. +func ParseDirAttributes(sourceName string) DirAttributes { + var ( + name = sourceName + exact = false + private = false + ) + if strings.HasPrefix(name, exactPrefix) { + name = strings.TrimPrefix(name, exactPrefix) + exact = true + } + if strings.HasPrefix(name, privatePrefix) { + name = strings.TrimPrefix(name, privatePrefix) + private = true + } + if strings.HasPrefix(name, dotPrefix) { + name = "." + strings.TrimPrefix(name, dotPrefix) + } + return DirAttributes{ + Name: name, + Exact: exact, + Private: private, + } +} + +// SourceName returns da's source name. +func (da DirAttributes) SourceName() string { + sourceName := "" + if da.Exact { + sourceName += exactPrefix + } + if da.Private { + sourceName += privatePrefix + } + if strings.HasPrefix(da.Name, ".") { + sourceName += dotPrefix + strings.TrimPrefix(da.Name, ".") + } else { + sourceName += da.Name + } + return sourceName +} + +// ParseFileAttributes parses a source file name. +func ParseFileAttributes(sourceName string) FileAttributes { + var ( + name = sourceName + typ = SourceFileTypeFile + empty = false + encrypted = false + executable = false + once = false + private = false + template = false + ) + switch { + case strings.HasPrefix(name, runPrefix): + name = strings.TrimPrefix(name, runPrefix) + typ = SourceFileTypeScript + if strings.HasPrefix(name, oncePrefix) { + name = strings.TrimPrefix(name, oncePrefix) + once = true + } + case strings.HasPrefix(name, symlinkPrefix): + name = strings.TrimPrefix(name, symlinkPrefix) + typ = SourceFileTypeSymlink + if strings.HasPrefix(name, dotPrefix) { + name = "." + strings.TrimPrefix(name, dotPrefix) + } + default: + if strings.HasPrefix(name, encryptedPrefix) { + name = strings.TrimPrefix(name, encryptedPrefix) + encrypted = true + } + if strings.HasPrefix(name, privatePrefix) { + name = strings.TrimPrefix(name, privatePrefix) + private = true + } + if strings.HasPrefix(name, emptyPrefix) { + name = strings.TrimPrefix(name, emptyPrefix) + empty = true + } + if strings.HasPrefix(name, executablePrefix) { + name = strings.TrimPrefix(name, executablePrefix) + executable = true + } + if strings.HasPrefix(name, dotPrefix) { + name = "." + strings.TrimPrefix(name, dotPrefix) + } + } + if strings.HasSuffix(name, TemplateSuffix) { + name = strings.TrimSuffix(name, TemplateSuffix) + template = true + } + return FileAttributes{ + Name: name, + Type: typ, + Empty: empty, + Encrypted: encrypted, + Executable: executable, + Once: once, + Private: private, + Template: template, + } +} + +// SourceName returns fa's source name. +func (fa FileAttributes) SourceName() string { + sourceName := "" + switch fa.Type { + case SourceFileTypeFile: + if fa.Encrypted { + sourceName += encryptedPrefix + } + if fa.Private { + sourceName += privatePrefix + } + if fa.Empty { + sourceName += emptyPrefix + } + if fa.Executable { + sourceName += executablePrefix + } + case SourceFileTypeScript: + sourceName = runPrefix + if fa.Once { + sourceName += oncePrefix + } + case SourceFileTypeSymlink: + sourceName = symlinkPrefix + } + if strings.HasPrefix(fa.Name, ".") { + sourceName += dotPrefix + strings.TrimPrefix(fa.Name, ".") + } else { + sourceName += fa.Name + } + if fa.Template { + sourceName += TemplateSuffix + } + return sourceName +} diff --git a/v2/chezmoi/attributes_test.go b/v2/chezmoi/attributes_test.go new file mode 100644 index 000000000000..299fe801b4ae --- /dev/null +++ b/v2/chezmoi/attributes_test.go @@ -0,0 +1,199 @@ +package chezmoi + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDirAttributes(t *testing.T) { + for _, tc := range []struct { + sourceName string + da DirAttributes + }{ + { + sourceName: "foo", + da: DirAttributes{ + Name: "foo", + }, + }, + { + sourceName: "dot_foo", + da: DirAttributes{ + Name: ".foo", + }, + }, + { + sourceName: "private_foo", + da: DirAttributes{ + Name: "foo", + Private: true, + }, + }, + { + sourceName: "exact_foo", + da: DirAttributes{ + Name: "foo", + Exact: true, + }, + }, + { + sourceName: "private_dot_foo", + da: DirAttributes{ + Name: ".foo", + Private: true, + }, + }, + { + sourceName: "exact_private_dot_foo", + da: DirAttributes{ + Name: ".foo", + Exact: true, + Private: true, + }, + }, + } { + t.Run(tc.sourceName, func(t *testing.T) { + assert.Equal(t, tc.da, ParseDirAttributes(tc.sourceName)) + assert.Equal(t, tc.sourceName, tc.da.SourceName()) + }) + } +} + +func TestFileAttributes(t *testing.T) { + for _, tc := range []struct { + sourceName string + fa FileAttributes + }{ + { + sourceName: "foo", + fa: FileAttributes{ + Name: "foo", + }, + }, + { + sourceName: "dot_foo", + fa: FileAttributes{ + Name: ".foo", + }, + }, + { + sourceName: "private_foo", + fa: FileAttributes{ + Name: "foo", + Private: true, + }, + }, + { + sourceName: "private_dot_foo", + fa: FileAttributes{ + Name: ".foo", + Private: true, + }, + }, + { + sourceName: "empty_foo", + fa: FileAttributes{ + Name: "foo", + Empty: true, + }, + }, + { + sourceName: "executable_foo", + fa: FileAttributes{ + Name: "foo", + Executable: true, + }, + }, + { + sourceName: "foo.tmpl", + fa: FileAttributes{ + Name: "foo", + Template: true, + }, + }, + { + sourceName: "private_executable_dot_foo.tmpl", + fa: FileAttributes{ + Name: ".foo", + Executable: true, + Private: true, + Template: true, + }, + }, + { + sourceName: "run_foo", + fa: FileAttributes{ + Name: "foo", + Type: SourceFileTypeScript, + }, + }, + { + sourceName: "run_foo.tmpl", + fa: FileAttributes{ + Name: "foo", + Type: SourceFileTypeScript, + Template: true, + }, + }, + { + sourceName: "run_once_foo", + fa: FileAttributes{ + Name: "foo", + Type: SourceFileTypeScript, + Once: true, + }, + }, + { + sourceName: "run_once_foo.tmpl", + fa: FileAttributes{ + Name: "foo", + Type: SourceFileTypeScript, + Once: true, + Template: true, + }, + }, + { + sourceName: "run_dot_foo", + fa: FileAttributes{ + Name: "dot_foo", + Type: SourceFileTypeScript, + }, + }, + { + sourceName: "symlink_foo", + fa: FileAttributes{ + Name: "foo", + Type: SourceFileTypeSymlink, + }, + }, + { + sourceName: "symlink_dot_foo", + fa: FileAttributes{ + Name: ".foo", + Type: SourceFileTypeSymlink, + }, + }, + { + sourceName: "symlink_foo.tmpl", + fa: FileAttributes{ + Name: "foo", + Type: SourceFileTypeSymlink, + Template: true, + }, + }, + { + sourceName: "encrypted_private_dot_secret_file", + fa: FileAttributes{ + Name: ".secret_file", + Encrypted: true, + Private: true, + }, + }, + } { + t.Run(tc.sourceName, func(t *testing.T) { + assert.Equal(t, tc.fa, ParseFileAttributes(tc.sourceName)) + assert.Equal(t, tc.sourceName, tc.fa.SourceName()) + }) + } +} diff --git a/v2/chezmoi/autotemplate.go b/v2/chezmoi/autotemplate.go new file mode 100644 index 000000000000..3f370a8ff863 --- /dev/null +++ b/v2/chezmoi/autotemplate.go @@ -0,0 +1,90 @@ +package chezmoi + +import ( + "sort" + "strings" +) + +type templateVariable struct { + name string + value string +} + +type byValueLength []templateVariable + +func (b byValueLength) Len() int { return len(b) } +func (b byValueLength) Less(i, j int) bool { + switch { + case len(b[i].value) < len(b[j].value): + return true + case len(b[i].value) == len(b[j].value): + // Fallback to name + return b[i].name > b[j].name + default: + return false + } +} +func (b byValueLength) Swap(i, j int) { b[i], b[j] = b[j], b[i] } + +func autoTemplate(contents []byte, data map[string]interface{}) []byte { + // FIXME this naive approach will generate incorrect templates if the + // variable names match variable values + // FIXME the algorithm here is probably O(N^2), we can do better + variables := extractVariables(nil, nil, data) + sort.Sort(sort.Reverse(byValueLength(variables))) + contentsStr := string(contents) + for _, variable := range variables { + if variable.value == "" { + continue + } + index := strings.Index(contentsStr, variable.value) + for index != -1 && index != len(contentsStr) { + if !inWord(contentsStr, index) && !inWord(contentsStr, index+len(variable.value)) { + // Replace variable.value which is on word boundaries at both + // ends. + replacement := "{{ ." + variable.name + " }}" + contentsStr = contentsStr[:index] + replacement + contentsStr[index+len(variable.value):] + index += len(replacement) + } else { + // Otherwise, keep looking. Consume at least one byte so we + // make progress. + index++ + } + // Look for the next occurrence of variable.value. + j := strings.Index(contentsStr[index:], variable.value) + if j == -1 { + // No more occurrences found, so terminate the loop. + break + } else { + // Advance to the next occurrence. + index += j + } + } + } + return []byte(contentsStr) +} + +func extractVariables(variables []templateVariable, parent []string, data map[string]interface{}) []templateVariable { + for name, value := range data { + switch value := value.(type) { + case string: + variables = append(variables, templateVariable{ + name: strings.Join(append(parent, name), "."), + value: value, + }) + case map[string]interface{}: + variables = extractVariables(variables, append(parent, name), value) + } + } + return variables +} + +// inWord returns true if splitting s at position i would split a word. +func inWord(s string, i int) bool { + return i > 0 && i < len(s) && isWord(s[i-1]) && isWord(s[i]) +} + +// isWord returns true if b is a word byte. +func isWord(b byte) bool { + return '0' <= b && b <= '9' || 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' +} diff --git a/v2/chezmoi/autotemplate_test.go b/v2/chezmoi/autotemplate_test.go new file mode 100644 index 000000000000..ea264165bf6b --- /dev/null +++ b/v2/chezmoi/autotemplate_test.go @@ -0,0 +1,168 @@ +package chezmoi + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAutoTemplate(t *testing.T) { + for _, tc := range []struct { + name string + contentsStr string + data map[string]interface{} + wantStr string + }{ + { + name: "simple", + contentsStr: "email = john.smith@company.com\n", + data: map[string]interface{}{ + "email": "john.smith@company.com", + }, + wantStr: "email = {{ .email }}\n", + }, + { + name: "longest_first", + contentsStr: "name = John Smith\nfirstName = John\n", + data: map[string]interface{}{ + "name": "John Smith", + "firstName": "John", + }, + wantStr: "name = {{ .name }}\nfirstName = {{ .firstName }}\n", + }, + { + name: "alphabetical_first", + contentsStr: "name = John Smith\n", + data: map[string]interface{}{ + "alpha": "John Smith", + "beta": "John Smith", + "gamma": "John Smith", + }, + wantStr: "name = {{ .alpha }}\n", + }, + { + name: "nested_values", + contentsStr: "email = john.smith@company.com\n", + data: map[string]interface{}{ + "personal": map[string]interface{}{ + "email": "john.smith@company.com", + }, + }, + wantStr: "email = {{ .personal.email }}\n", + }, + { + name: "only_replace_words", + contentsStr: "darwinian evolution", + data: map[string]interface{}{ + "os": "darwin", + }, + wantStr: "darwinian evolution", // not "{{ .os }}ian evolution" + }, + { + name: "longest_match_first", + contentsStr: "/home/user", + data: map[string]interface{}{ + "homedir": "/home/user", + }, + wantStr: "{{ .homedir }}", + }, + { + name: "longest_match_first_prefix", + contentsStr: "HOME=/home/user", + data: map[string]interface{}{ + "homedir": "/home/user", + }, + wantStr: "HOME={{ .homedir }}", + }, + { + name: "longest_match_first_suffix", + contentsStr: "/home/user/something", + data: map[string]interface{}{ + "homedir": "/home/user", + }, + wantStr: "{{ .homedir }}/something", + }, + { + name: "longest_match_first_prefix_and_suffix", + contentsStr: "HOME=/home/user/something", + data: map[string]interface{}{ + "homedir": "/home/user", + }, + wantStr: "HOME={{ .homedir }}/something", + }, + { + name: "words_only", + contentsStr: "aaa aa a aa aaa aa a aa aaa", + data: map[string]interface{}{ + "alpha": "a", + }, + wantStr: "aaa aa {{ .alpha }} aa aaa aa {{ .alpha }} aa aaa", + }, + { + name: "words_only_2", + contentsStr: "aaa aa a aa aaa aa a aa aaa", + data: map[string]interface{}{ + "alpha": "aa", + }, + wantStr: "aaa {{ .alpha }} a {{ .alpha }} aaa {{ .alpha }} a {{ .alpha }} aaa", + }, + { + name: "words_only_3", + contentsStr: "aaa aa a aa aaa aa a aa aaa", + data: map[string]interface{}{ + "alpha": "aaa", + }, + wantStr: "{{ .alpha }} aa a aa {{ .alpha }} aa a aa {{ .alpha }}", + }, + { + name: "skip_empty", + contentsStr: "a", + data: map[string]interface{}{ + "empty": "", + }, + wantStr: "a", + }, + } { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.wantStr, string(autoTemplate([]byte(tc.contentsStr), tc.data))) + }) + } +} + +func TestInWord(t *testing.T) { + for _, tc := range []struct { + s string + i int + want bool + }{ + {s: "", i: 0, want: false}, + {s: "a", i: 0, want: false}, + {s: "a", i: 1, want: false}, + {s: "ab", i: 0, want: false}, + {s: "ab", i: 1, want: true}, + {s: "ab", i: 2, want: false}, + {s: "abc", i: 0, want: false}, + {s: "abc", i: 1, want: true}, + {s: "abc", i: 2, want: true}, + {s: "abc", i: 3, want: false}, + {s: " abc ", i: 0, want: false}, + {s: " abc ", i: 1, want: false}, + {s: " abc ", i: 2, want: true}, + {s: " abc ", i: 3, want: true}, + {s: " abc ", i: 4, want: false}, + {s: " abc ", i: 5, want: false}, + {s: "/home/user", i: 0, want: false}, + {s: "/home/user", i: 1, want: false}, + {s: "/home/user", i: 2, want: true}, + {s: "/home/user", i: 3, want: true}, + {s: "/home/user", i: 4, want: true}, + {s: "/home/user", i: 5, want: false}, + {s: "/home/user", i: 6, want: false}, + {s: "/home/user", i: 7, want: true}, + {s: "/home/user", i: 8, want: true}, + {s: "/home/user", i: 9, want: true}, + {s: "/home/user", i: 10, want: false}, + } { + assert.Equal(t, tc.want, inWord(tc.s, tc.i)) + } +} diff --git a/v2/chezmoi/boltpersistentstate.go b/v2/chezmoi/boltpersistentstate.go new file mode 100644 index 000000000000..83f698b5aa73 --- /dev/null +++ b/v2/chezmoi/boltpersistentstate.go @@ -0,0 +1,122 @@ +package chezmoi + +import ( + "os" + "path" + + vfs "github.com/twpayne/go-vfs" + bolt "go.etcd.io/bbolt" +) + +// A BoltPersistentState is a state persisted with bolt. +type BoltPersistentState struct { + fs vfs.FS + path string + perm os.FileMode + umask os.FileMode + options *bolt.Options + db *bolt.DB +} + +// NewBoltPersistentState returns a new BoltPersistentState. +func NewBoltPersistentState(fs vfs.FS, path string, umask os.FileMode, options *bolt.Options) (*BoltPersistentState, error) { + b := &BoltPersistentState{ + fs: fs, + path: path, + perm: 0o600, + umask: umask, + options: options, + } + _, err := fs.Stat(b.path) + switch { + case err == nil: + if err := b.openDB(); err != nil { + return nil, err + } + case os.IsNotExist(err): + default: + return nil, err + } + return b, nil +} + +// Close closes b. +func (b *BoltPersistentState) Close() error { + if b.db == nil { + return nil + } + if err := b.db.Close(); err != nil { + return err + } + b.db = nil + return nil +} + +// Delete deletes the value associate with key in bucket. If bucket or key does +// not exist then Delete does nothing. +func (b *BoltPersistentState) Delete(bucket, key []byte) error { + if b.db == nil { + return nil + } + return b.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket(bucket) + if b == nil { + return nil + } + return b.Delete(key) + }) +} + +// Get returns the value associated with key in bucket. +func (b *BoltPersistentState) Get(bucket, key []byte) ([]byte, error) { + var value []byte + if b.db == nil { + return value, nil + } + return value, b.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(bucket) + if b == nil { + return nil + } + v := b.Get(key) + if v != nil { + value = make([]byte, len(v)) + copy(value, v) + } + return nil + }) +} + +// Set sets the value associated with key in bucket. bucket will be created if +// it does not already exist. +func (b *BoltPersistentState) Set(bucket, key, value []byte) error { + if b.db == nil { + if err := b.openDB(); err != nil { + return err + } + } + return b.db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists(bucket) + if err != nil { + return err + } + return b.Put(key, value) + }) +} + +func (b *BoltPersistentState) openDB() error { + if err := vfs.MkdirAll(b.fs, path.Dir(b.path), 0o777&^b.umask); err != nil { + return err + } + var options bolt.Options + if b.options != nil { + options = *b.options + } + options.OpenFile = b.fs.OpenFile + db, err := bolt.Open(b.path, b.perm&^b.umask, &options) + if err != nil { + return err + } + b.db = db + return err +} diff --git a/v2/chezmoi/boltpersistentstate_test.go b/v2/chezmoi/boltpersistentstate_test.go new file mode 100644 index 000000000000..12c26f5867fc --- /dev/null +++ b/v2/chezmoi/boltpersistentstate_test.go @@ -0,0 +1,120 @@ +package chezmoi + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/twpayne/go-vfs/vfst" + bolt "go.etcd.io/bbolt" +) + +var _ PersistentState = &BoltPersistentState{} + +func TestBoltPersistentState(t *testing.T) { + fs, cleanup, err := vfst.NewTestFS(map[string]interface{}{ + "/home/user/.config/chezmoi": &vfst.Dir{Perm: 0o755}, + }) + require.NoError(t, err) + defer cleanup() + + path := "/home/user/.config/chezmoi/chezmoistate.boltdb" + b, err := NewBoltPersistentState(fs, path, vfst.DefaultUmask, nil) + require.NoError(t, err) + vfst.RunTests(t, fs, "", + vfst.TestPath(path, + vfst.TestDoesNotExist, + ), + ) + + var ( + bucket = []byte("bucket") + key = []byte("key") + value = []byte("value") + ) + + require.NoError(t, b.Delete(bucket, key)) + vfst.RunTests(t, fs, "", + vfst.TestPath(path, + vfst.TestDoesNotExist, + ), + ) + + actualValue, err := b.Get(bucket, key) + require.NoError(t, err) + assert.Equal(t, []byte(nil), actualValue) + vfst.RunTests(t, fs, "", + vfst.TestPath(path, + vfst.TestDoesNotExist, + ), + ) + + assert.NoError(t, b.Set(bucket, key, value)) + vfst.RunTests(t, fs, "", + vfst.TestPath(path, + vfst.TestModeIsRegular, + ), + ) + + actualValue, err = b.Get(bucket, key) + require.NoError(t, err) + assert.Equal(t, value, actualValue) + + require.NoError(t, b.Close()) + + b, err = NewBoltPersistentState(fs, path, vfst.DefaultUmask, nil) + require.NoError(t, err) + + require.NoError(t, b.Delete(bucket, key)) + + actualValue, err = b.Get(bucket, key) + require.NoError(t, err) + assert.Equal(t, []byte(nil), actualValue) +} + +func TestBoltPersistentStateReadOnly(t *testing.T) { + fs, cleanup, err := vfst.NewTestFS(map[string]interface{}{ + "/home/user/.config/chezmoi": &vfst.Dir{Perm: 0o755}, + }) + require.NoError(t, err) + defer cleanup() + + path := "/home/user/.config/chezmoi/chezmoistate.boltdb" + bucket := []byte("bucket") + key := []byte("key") + value := []byte("value") + + a, err := NewBoltPersistentState(fs, path, vfst.DefaultUmask, nil) + require.NoError(t, err) + require.NoError(t, a.Set(bucket, key, value)) + require.NoError(t, a.Close()) + + b, err := NewBoltPersistentState(fs, path, vfst.DefaultUmask, &bolt.Options{ + ReadOnly: true, + Timeout: 1 * time.Second, + }) + require.NoError(t, err) + defer b.Close() + + c, err := NewBoltPersistentState(fs, path, vfst.DefaultUmask, &bolt.Options{ + ReadOnly: true, + Timeout: 1 * time.Second, + }) + require.NoError(t, err) + defer c.Close() + + actualValueB, err := b.Get(bucket, key) + require.NoError(t, err) + assert.Equal(t, value, actualValueB) + + actualValueC, err := c.Get(bucket, key) + require.NoError(t, err) + assert.Equal(t, value, actualValueC) + + assert.Error(t, b.Set(bucket, key, value)) + assert.Error(t, c.Set(bucket, key, value)) + + require.NoError(t, b.Close()) + require.NoError(t, c.Close()) +} diff --git a/v2/chezmoi/canaryfilesystem.go b/v2/chezmoi/canaryfilesystem.go new file mode 100644 index 000000000000..7307fdf85593 --- /dev/null +++ b/v2/chezmoi/canaryfilesystem.go @@ -0,0 +1,103 @@ +package chezmoi + +import ( + "os" + "os/exec" +) + +// An CanaryFileSystem wraps a FileSystem and records if any of its mutating +// methods are called. +type CanaryFileSystem struct { + fs FileSystem + mutated bool +} + +// NewCanaryFileSystem returns a new CanaryFileSystem. +func NewCanaryFileSystem(fs FileSystem) *CanaryFileSystem { + return &CanaryFileSystem{ + fs: fs, + mutated: false, + } +} + +// Chmod implements FileSystem.Chmod. +func (fs *CanaryFileSystem) Chmod(name string, mode os.FileMode) error { + fs.mutated = true + return fs.fs.Chmod(name, mode) +} + +// Glob implements FileSystem.Glob. +func (fs *CanaryFileSystem) Glob(pattern string) ([]string, error) { + return fs.fs.Glob(pattern) +} + +// IdempotentCmdOutput implements FileSystem.IdempotentCmdOutput. +func (fs *CanaryFileSystem) IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) { + return fs.fs.IdempotentCmdOutput(cmd) +} + +// Mkdir implements FileSystem.Mkdir. +func (fs *CanaryFileSystem) Mkdir(name string, perm os.FileMode) error { + fs.mutated = true + return fs.fs.Mkdir(name, perm) +} + +// Lstat implements FileSystem.Lstat. +func (fs *CanaryFileSystem) Lstat(path string) (os.FileInfo, error) { + return fs.fs.Lstat(path) +} + +// Mutated returns true if any of its mutating methods have been called. +func (fs *CanaryFileSystem) Mutated() bool { + return fs.mutated +} + +// ReadDir implements FileSystem.ReadDir. +func (fs *CanaryFileSystem) ReadDir(dirname string) ([]os.FileInfo, error) { + return fs.fs.ReadDir(dirname) +} + +// ReadFile implements FileSystem.ReadFile. +func (fs *CanaryFileSystem) ReadFile(filename string) ([]byte, error) { + return fs.fs.ReadFile(filename) +} + +// Readlink implements FileSystem.Readlink. +func (fs *CanaryFileSystem) Readlink(name string) (string, error) { + return fs.fs.Readlink(name) +} + +// RemoveAll implements FileSystem.RemoveAll. +func (fs *CanaryFileSystem) RemoveAll(name string) error { + fs.mutated = true + return fs.fs.RemoveAll(name) +} + +// Rename implements FileSystem.Rename. +func (fs *CanaryFileSystem) Rename(oldpath, newpath string) error { + fs.mutated = true + return fs.fs.Rename(oldpath, newpath) +} + +// RunCmd implements FileSystem.RunCmd. +func (fs *CanaryFileSystem) RunCmd(cmd *exec.Cmd) error { + fs.mutated = true + return fs.fs.RunCmd(cmd) +} + +// Stat implements FileSystem.Stat. +func (fs *CanaryFileSystem) Stat(path string) (os.FileInfo, error) { + return fs.fs.Stat(path) +} + +// WriteFile implements FileSystem.WriteFile. +func (fs *CanaryFileSystem) WriteFile(name string, data []byte, perm os.FileMode, currData []byte) error { + fs.mutated = true + return fs.fs.WriteFile(name, data, perm, currData) +} + +// WriteSymlink implements FileSystem.WriteSymlink. +func (fs *CanaryFileSystem) WriteSymlink(oldname, newname string) error { + fs.mutated = true + return fs.fs.WriteSymlink(oldname, newname) +} diff --git a/v2/chezmoi/canaryfilesystem_test.go b/v2/chezmoi/canaryfilesystem_test.go new file mode 100644 index 000000000000..19435dd985d1 --- /dev/null +++ b/v2/chezmoi/canaryfilesystem_test.go @@ -0,0 +1,3 @@ +package chezmoi + +var _ FileSystem = &CanaryFileSystem{} diff --git a/v2/chezmoi/chezmoi.go b/v2/chezmoi/chezmoi.go new file mode 100644 index 000000000000..3065b2138555 --- /dev/null +++ b/v2/chezmoi/chezmoi.go @@ -0,0 +1,60 @@ +package chezmoi + +import ( + "fmt" + "os" +) + +// Suffixes and prefixes. +const ( + dotPrefix = "dot_" + emptyPrefix = "empty_" + encryptedPrefix = "encrypted_" + exactPrefix = "exact_" + executablePrefix = "executable_" + oncePrefix = "once_" + privatePrefix = "private_" + runPrefix = "run_" + symlinkPrefix = "symlink_" + TemplateSuffix = ".tmpl" +) + +// Special file names. +const ( + ignoreName = ".chezmoiignore" + removeName = ".chezmoiremove" + templatesDirName = ".chezmoitemplates" + versionName = ".chezmoiversion" + + ignorePrefix = "." +) + +const pathSeparator = "/" + +// DefaultTemplateOptions are the default template options. +var DefaultTemplateOptions = []string{"missingkey=error"} + +// A PersistentState is an interface to a persistent state. +type PersistentState interface { + Close() error + Delete(bucket, key []byte) error + Get(bucket, key []byte) ([]byte, error) + Set(bucket, key, value []byte) error +} + +var modeTypeNames = map[os.FileMode]string{ + 0: "file", + os.ModeDir: "dir", + os.ModeSymlink: "symlink", + os.ModeNamedPipe: "named pipe", + os.ModeSocket: "socket", + os.ModeDevice: "device", + os.ModeCharDevice: "char device", +} + +func modeTypeName(mode os.FileMode) string { + if name, ok := modeTypeNames[mode&os.ModeType]; ok { + return name + } + return fmt.Sprintf("unknown (%d)", mode&os.ModeType) +} diff --git a/v2/chezmoi/debugfilesystem.go b/v2/chezmoi/debugfilesystem.go new file mode 100644 index 000000000000..da1cf896b298 --- /dev/null +++ b/v2/chezmoi/debugfilesystem.go @@ -0,0 +1,175 @@ +package chezmoi + +import ( + "log" + "os" + "os/exec" + "time" +) + +// A DebugFileSystem wraps a FileSystem and logs all of the actions it executes. +type DebugFileSystem struct { + fs FileSystem +} + +// NewDebugFileSystem returns a new DebugFileSystem. +func NewDebugFileSystem(fs FileSystem) *DebugFileSystem { + return &DebugFileSystem{ + fs: fs, + } +} + +// Chmod implements FileSystem.Chmod. +func (fs *DebugFileSystem) Chmod(name string, mode os.FileMode) error { + return Debugf("Chmod(%q, 0o%o)", []interface{}{name, mode}, func() error { + return fs.fs.Chmod(name, mode) + }) +} + +// Glob implements FileSystem.Glob. +func (fs *DebugFileSystem) Glob(name string) ([]string, error) { + var matches []string + err := Debugf("Glob(%q)", []interface{}{name}, func() error { + var err error + matches, err = fs.fs.Glob(name) + return err + }) + return matches, err +} + +// IdempotentCmdOutput implements FileSystem.IdempotentCmdOutput. +func (fs *DebugFileSystem) IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) { + var output []byte + cmdStr := ShellQuoteArgs(append([]string{cmd.Path}, cmd.Args[1:]...)) + err := Debugf("IdempotentCmdOutput(%q)", []interface{}{cmdStr}, func() error { + var err error + output, err = fs.fs.IdempotentCmdOutput(cmd) + return err + }) + return output, err +} + +// Lstat implements FileSystem.Lstat. +func (fs *DebugFileSystem) Lstat(name string) (os.FileInfo, error) { + var info os.FileInfo + err := Debugf("Lstat(%q)", []interface{}{name}, func() error { + var err error + info, err = fs.fs.Lstat(name) + return err + }) + return info, err +} + +// Mkdir implements FileSystem.Mkdir. +func (fs *DebugFileSystem) Mkdir(name string, perm os.FileMode) error { + return Debugf("Mkdir(%q, 0o%o)", []interface{}{name, perm}, func() error { + return fs.fs.Mkdir(name, perm) + }) +} + +// ReadDir implements FileSystem.ReadDir. +func (fs *DebugFileSystem) ReadDir(name string) ([]os.FileInfo, error) { + var infos []os.FileInfo + err := Debugf("ReadDir(%q)", []interface{}{name}, func() error { + var err error + infos, err = fs.fs.ReadDir(name) + return err + }) + return infos, err +} + +// ReadFile implements FileSystem.ReadFile. +func (fs *DebugFileSystem) ReadFile(filename string) ([]byte, error) { + var data []byte + err := Debugf("ReadFile(%q)", []interface{}{filename}, func() error { + var err error + data, err = fs.fs.ReadFile(filename) + return err + }) + return data, err +} + +// Readlink implements FileSystem.Readlink. +func (fs *DebugFileSystem) Readlink(name string) (string, error) { + var linkname string + err := Debugf("Readlink(%q)", []interface{}{name}, func() error { + var err error + linkname, err = fs.fs.Readlink(name) + return err + }) + return linkname, err +} + +// RemoveAll implements FileSystem.RemoveAll. +func (fs *DebugFileSystem) RemoveAll(name string) error { + return Debugf("RemoveAll(%q)", []interface{}{name}, func() error { + return fs.fs.RemoveAll(name) + }) +} + +// Rename implements FileSystem.Rename. +func (fs *DebugFileSystem) Rename(oldpath, newpath string) error { + return Debugf("Rename(%q, %q)", []interface{}{oldpath, newpath}, func() error { + return fs.Rename(oldpath, newpath) + }) +} + +// RunCmd implements FileSystem.RunCmd. +func (fs *DebugFileSystem) RunCmd(cmd *exec.Cmd) error { + cmdStr := ShellQuoteArgs(append([]string{cmd.Path}, cmd.Args[1:]...)) + return Debugf("Run(%q)", []interface{}{cmdStr}, func() error { + return fs.fs.RunCmd(cmd) + }) +} + +// Stat implements FileSystem.Stat. +func (fs *DebugFileSystem) Stat(name string) (os.FileInfo, error) { + var info os.FileInfo + err := Debugf("Stat(%q)", []interface{}{name}, func() error { + var err error + info, err = fs.fs.Stat(name) + return err + }) + return info, err +} + +// WriteFile implements FileSystem.WriteFile. +func (fs *DebugFileSystem) WriteFile(name string, data []byte, perm os.FileMode, currData []byte) error { + return Debugf("WriteFile(%q, _, 0%o, _)", []interface{}{name, perm}, func() error { + return fs.fs.WriteFile(name, data, perm, currData) + }) +} + +// WriteSymlink implements FileSystem.WriteSymlink. +func (fs *DebugFileSystem) WriteSymlink(oldname, newname string) error { + return Debugf("WriteSymlink(%q, %q)", []interface{}{oldname, newname}, func() error { + return fs.fs.WriteSymlink(oldname, newname) + }) +} + +// Debugf logs debugging information about calling f. +func Debugf(format string, args []interface{}, f func() error) error { + errChan := make(chan error) + start := time.Now() + go func(errChan chan<- error) { + errChan <- f() + }(errChan) + select { + case err := <-errChan: + if err == nil { + log.Printf(format+" (%s)", append(args, time.Since(start))...) + } else { + log.Printf(format+" == %v (%s)", append(args, err, time.Since(start))...) + } + return err + case <-time.After(1 * time.Second): + log.Printf(format, args...) + err := <-errChan + if err == nil { + log.Printf(format+" (%s)", append(args, time.Since(start))...) + } else { + log.Printf(format+" == %v (%s)", append(args, err, time.Since(start))...) + } + return err + } +} diff --git a/v2/chezmoi/debugfilesystem_test.go b/v2/chezmoi/debugfilesystem_test.go new file mode 100644 index 000000000000..9d92ebbeab2c --- /dev/null +++ b/v2/chezmoi/debugfilesystem_test.go @@ -0,0 +1,3 @@ +package chezmoi + +var _ FileSystem = &DebugFileSystem{} diff --git a/v2/chezmoi/deststateentry.go b/v2/chezmoi/deststateentry.go new file mode 100644 index 000000000000..3e53e9490c5e --- /dev/null +++ b/v2/chezmoi/deststateentry.go @@ -0,0 +1,122 @@ +package chezmoi + +// FIXME data command + +import ( + "os" +) + +// An DestStateEntry represents the state of an entry in the destination state. +type DestStateEntry interface { + Path() string + Remove(fs FileSystem) error +} + +// A DestStateAbsent represents the absence of an entry in the destination +// state. +type DestStateAbsent struct { + path string +} + +// A DestStateDir represents the state of a directory in the destination state. +type DestStateDir struct { + path string + perm os.FileMode +} + +// A DestStateFile represents the state of a file in the destination state. +type DestStateFile struct { + path string + perm os.FileMode + *lazyContents +} + +// A DestStateSymlink represents the state of a symlink in the destination state. +type DestStateSymlink struct { + path string + *lazyLinkname +} + +// NewDestStateEntry returns a new DestStateEntry populated with path from fs. +func NewDestStateEntry(fs FileSystemReader, path string) (DestStateEntry, error) { + info, err := fs.Lstat(path) + switch { + case os.IsNotExist(err): + return &DestStateAbsent{ + path: path, + }, nil + case err != nil: + return nil, err + } + switch info.Mode() & os.ModeType { + case 0: + return &DestStateFile{ + path: path, + perm: info.Mode() & os.ModePerm, + lazyContents: &lazyContents{ + contentsFunc: func() ([]byte, error) { + return fs.ReadFile(path) + }, + }, + }, nil + case os.ModeDir: + return &DestStateDir{ + path: path, + perm: info.Mode() & os.ModePerm, + }, nil + case os.ModeSymlink: + return &DestStateSymlink{ + path: path, + lazyLinkname: &lazyLinkname{ + linknameFunc: func() (string, error) { + return fs.Readlink(path) + }, + }, + }, nil + default: + return nil, &unsupportedFileTypeError{ + path: path, + mode: info.Mode(), + } + } +} + +// Path returns d's path. +func (d *DestStateAbsent) Path() string { + return d.path +} + +// Remove removes d. +func (d *DestStateAbsent) Remove(fs FileSystem) error { + return nil +} + +// Path returns d's path. +func (d *DestStateDir) Path() string { + return d.path +} + +// Remove removes d. +func (d *DestStateDir) Remove(fs FileSystem) error { + return fs.RemoveAll(d.path) +} + +// Path returns d's path. +func (d *DestStateFile) Path() string { + return d.path +} + +// Remove removes d. +func (d *DestStateFile) Remove(fs FileSystem) error { + return fs.RemoveAll(d.path) +} + +// Path returns d's path. +func (d *DestStateSymlink) Path() string { + return d.path +} + +// Remove removes d. +func (d *DestStateSymlink) Remove(fs FileSystem) error { + return fs.RemoveAll(d.path) +} diff --git a/v2/chezmoi/dryrunfilesystem.go b/v2/chezmoi/dryrunfilesystem.go new file mode 100644 index 000000000000..2e1408e171f1 --- /dev/null +++ b/v2/chezmoi/dryrunfilesystem.go @@ -0,0 +1,89 @@ +package chezmoi + +import ( + "os" + "os/exec" +) + +// DryRunFileSystem is an FileSystem that reads from, but does not write to, to +// a wrapped FileSystem. +type DryRunFileSystem struct { + fs FileSystem +} + +// NewDryRunFileSystem returns a new DryRunFileSystem that wraps fs. +func NewDryRunFileSystem(fs FileSystem) *DryRunFileSystem { + return &DryRunFileSystem{ + fs: fs, + } +} + +// Chmod implements FileSystem.Chmod. +func (fs *DryRunFileSystem) Chmod(name string, mode os.FileMode) error { + return nil +} + +// Glob implements FileSystem.Glob. +func (fs *DryRunFileSystem) Glob(pattern string) ([]string, error) { + return fs.fs.Glob(pattern) +} + +// IdempotentCmdOutput implements FileSystem.IdempotentCmdOutput. +func (fs *DryRunFileSystem) IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) { + return fs.fs.IdempotentCmdOutput(cmd) +} + +// Lstat implements FileSystem.Lstat. +func (fs *DryRunFileSystem) Lstat(name string) (os.FileInfo, error) { + return fs.fs.Stat(name) +} + +// Mkdir implements FileSystem.Mkdir. +func (fs *DryRunFileSystem) Mkdir(name string, perm os.FileMode) error { + return nil +} + +// ReadDir implements FileSystem.ReadDir. +func (fs *DryRunFileSystem) ReadDir(dirname string) ([]os.FileInfo, error) { + return fs.fs.ReadDir(dirname) +} + +// ReadFile implements FileSystem.ReadFile. +func (fs *DryRunFileSystem) ReadFile(filename string) ([]byte, error) { + return fs.fs.ReadFile(filename) +} + +// Readlink implements FileSystem.Readlink. +func (fs *DryRunFileSystem) Readlink(name string) (string, error) { + return fs.fs.Readlink(name) +} + +// RemoveAll implements FileSystem.RemoveAll. +func (fs *DryRunFileSystem) RemoveAll(string) error { + return nil +} + +// Rename implements FileSystem.Rename. +func (fs *DryRunFileSystem) Rename(oldpath, newpath string) error { + return nil +} + +// RunCmd implements FileSystem.RunCmd. +func (fs *DryRunFileSystem) RunCmd(cmd *exec.Cmd) error { + return nil +} + +// Stat implements FileSystem.Stat. +func (fs *DryRunFileSystem) Stat(name string) (os.FileInfo, error) { + return fs.fs.Stat(name) +} + +// WriteFile implements FileSystem.WriteFile. +func (fs *DryRunFileSystem) WriteFile(string, []byte, os.FileMode, []byte) error { + return nil +} + +// WriteSymlink implements FileSystem.WriteSymlink. +func (fs *DryRunFileSystem) WriteSymlink(string, string) error { + return nil +} diff --git a/v2/chezmoi/dryrunfilesystem_test.go b/v2/chezmoi/dryrunfilesystem_test.go new file mode 100644 index 000000000000..ba1b9e85d4d2 --- /dev/null +++ b/v2/chezmoi/dryrunfilesystem_test.go @@ -0,0 +1,3 @@ +package chezmoi + +var _ FileSystem = &DryRunFileSystem{} diff --git a/v2/chezmoi/emptyfilesystem.go b/v2/chezmoi/emptyfilesystem.go new file mode 100644 index 000000000000..eed0d0d762a0 --- /dev/null +++ b/v2/chezmoi/emptyfilesystem.go @@ -0,0 +1,36 @@ +package chezmoi + +import "os" + +// An EmptyFileSystemReader represents an empty FileSystem. +type EmptyFileSystemReader struct{} + +// Glob implements FileSystem.Glob. +func (*EmptyFileSystemReader) Glob(pattern string) ([]string, error) { + return nil, nil +} + +// Lstat implements FileSystem.Lstat. +func (*EmptyFileSystemReader) Lstat(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist +} + +// ReadDir implements FileSystem.ReadDir. +func (*EmptyFileSystemReader) ReadDir(dirname string) ([]os.FileInfo, error) { + return nil, os.ErrNotExist +} + +// ReadFile implements FileSystem.ReadFile. +func (*EmptyFileSystemReader) ReadFile(filename string) ([]byte, error) { + return nil, os.ErrNotExist +} + +// Readlink implements FileSystem.Readlink. +func (*EmptyFileSystemReader) Readlink(name string) (string, error) { + return "", os.ErrNotExist +} + +// Stat implements FileSystem.Stat. +func (*EmptyFileSystemReader) Stat(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist +} diff --git a/v2/chezmoi/emptyfilesystem_test.go b/v2/chezmoi/emptyfilesystem_test.go new file mode 100644 index 000000000000..d4edf6eee6d5 --- /dev/null +++ b/v2/chezmoi/emptyfilesystem_test.go @@ -0,0 +1,3 @@ +package chezmoi + +var _ FileSystemReader = &EmptyFileSystemReader{} diff --git a/v2/chezmoi/errors.go b/v2/chezmoi/errors.go new file mode 100644 index 000000000000..928677f3a039 --- /dev/null +++ b/v2/chezmoi/errors.go @@ -0,0 +1,25 @@ +package chezmoi + +import ( + "fmt" + "os" + "strings" +) + +type duplicateTargetError struct { + targetName string + sourcePaths []string +} + +func (e *duplicateTargetError) Error() string { + return fmt.Sprintf("%s: duplicate target (%s)", e.targetName, strings.Join(e.sourcePaths, ", ")) +} + +type unsupportedFileTypeError struct { + path string + mode os.FileMode +} + +func (e *unsupportedFileTypeError) Error() string { + return fmt.Sprintf("%s: unsupported file type %s", e.path, modeTypeName(e.mode)) +} diff --git a/v2/chezmoi/filesystem.go b/v2/chezmoi/filesystem.go new file mode 100644 index 000000000000..c58d4e6349fa --- /dev/null +++ b/v2/chezmoi/filesystem.go @@ -0,0 +1,32 @@ +package chezmoi + +// FIXME do we need Stat? +// FIXME do we need a more specific FileReader interface with just ReadFile? + +import ( + "os" + "os/exec" +) + +// A FileSystemReader reads from a file system. +type FileSystemReader interface { + Glob(pattern string) ([]string, error) + Lstat(filename string) (os.FileInfo, error) + ReadDir(dirname string) ([]os.FileInfo, error) + ReadFile(filename string) ([]byte, error) + Readlink(name string) (string, error) + Stat(name string) (os.FileInfo, error) +} + +// A FileSystem writes to a file system. +type FileSystem interface { + FileSystemReader + Chmod(name string, mode os.FileMode) error + IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) + Mkdir(name string, perm os.FileMode) error + RemoveAll(name string) error + Rename(oldpath, newpath string) error + RunCmd(cmd *exec.Cmd) error + WriteFile(filename string, data []byte, perm os.FileMode, currData []byte) error + WriteSymlink(oldname, newname string) error +} diff --git a/v2/chezmoi/fsfilesystem.go b/v2/chezmoi/fsfilesystem.go new file mode 100644 index 000000000000..647877757805 --- /dev/null +++ b/v2/chezmoi/fsfilesystem.go @@ -0,0 +1,123 @@ +package chezmoi + +import ( + "errors" + "os" + "os/exec" + "path" + "runtime" + "syscall" + + "github.com/google/renameio" + vfs "github.com/twpayne/go-vfs" +) + +// An FSFileSystem is a FileSystem on an vfs.FS. +type FSFileSystem struct { + vfs.FS + devCache map[string]uint // devCache maps directories to device numbers. + tempDirCache map[uint]string // tempDir maps device numbers to renameio temporary directories. +} + +// NewFSFileSystem returns a FileSystem that acts on fs. +func NewFSFileSystem(fs vfs.FS) *FSFileSystem { + return &FSFileSystem{ + FS: fs, + devCache: make(map[string]uint), + tempDirCache: make(map[uint]string), + } +} + +// IdempotentCmdOutput implements FileSystem.IdempotentCmdOutput. +func (fs *FSFileSystem) IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) { + return cmd.Output() +} + +// RunCmd implements FileSystem.RunCmd. +func (fs *FSFileSystem) RunCmd(cmd *exec.Cmd) error { + return cmd.Run() +} + +// WriteSymlink implements FileSystem.WriteSymlink. +func (fs *FSFileSystem) WriteSymlink(oldname, newname string) error { + // Special case: if writing to the real filesystem, use + // github.com/google/renameio. + if fs.FS == vfs.OSFS { + return renameio.Symlink(oldname, newname) + } + if err := fs.FS.RemoveAll(newname); err != nil && !os.IsNotExist(err) { + return err + } + return fs.FS.Symlink(oldname, newname) +} + +// WriteFile implements FileSystem.WriteFile. +func (fs *FSFileSystem) WriteFile(filename string, data []byte, perm os.FileMode, currData []byte) error { + // Special case: if writing to the real filesystem on a non-Windows system, + // use github.com/google/renameio. + if fs.FS == vfs.OSFS && runtime.GOOS != "windows" { + dir := path.Dir(filename) + dev, ok := fs.devCache[dir] + if !ok { + info, err := fs.Stat(dir) + if err != nil { + return err + } + statT, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return errors.New("os.FileInfo.Sys() cannot be converted to a *syscall.Stat_t") + } + dev = uint(statT.Dev) + fs.devCache[dir] = dev + } + tempDir, ok := fs.tempDirCache[dev] + if !ok { + tempDir = renameio.TempDir(dir) + fs.tempDirCache[dev] = tempDir + } + t, err := renameio.TempFile(tempDir, filename) + if err != nil { + return err + } + defer func() { + _ = t.Cleanup() + }() + if err := t.Chmod(perm); err != nil { + return err + } + if _, err := t.Write(data); err != nil { + return err + } + return t.CloseAtomicallyReplace() + } + + // ioutil.WriteFile only sets the permissions when creating a new file. We + // need to ensure permissions, so we use our own implementation. + + // Create a new file, or truncate any existing one. + f, err := fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + return err + } + + // From now on, we continue to the end of the function to ensure that + // f.Close() gets called so we don't leak any file descriptors. + + // Set permissions after truncation but before writing any data, in case the + // file contained private data before, but before writing the new contents, + // in case the contents contain private data after. + err = f.Chmod(perm) + + // If everything is OK so far, write the data. + if err == nil { + _, err = f.Write(data) + } + + // Always call f.Close(), and overwrite the error if so far there is none. + if err1 := f.Close(); err == nil { + err = err1 + } + + // Return the first error encounted. + return err +} diff --git a/v2/chezmoi/fsfilesystem_test.go b/v2/chezmoi/fsfilesystem_test.go new file mode 100644 index 000000000000..1816ea4627de --- /dev/null +++ b/v2/chezmoi/fsfilesystem_test.go @@ -0,0 +1,3 @@ +package chezmoi + +var _ FileSystem = &FSFileSystem{} diff --git a/v2/chezmoi/gpg.go b/v2/chezmoi/gpg.go new file mode 100644 index 000000000000..8b8cab5d40d3 --- /dev/null +++ b/v2/chezmoi/gpg.go @@ -0,0 +1,85 @@ +package chezmoi + +import ( + "io/ioutil" + "os" + "os/exec" + "path" +) + +// GPG interfaces with gpg. +type GPG struct { + Recipient string + Symmetric bool +} + +// Decrypt decrypts ciphertext. filename is used as a hint for naming temporary +// files. +func (g *GPG) Decrypt(filename string, ciphertext []byte) ([]byte, error) { + tempDir, err := ioutil.TempDir("", "chezmoi-decrypt") + if err != nil { + return nil, err + } + defer os.RemoveAll(tempDir) + + outputFilename := path.Join(tempDir, path.Base(filename)) + inputFilename := outputFilename + ".gpg" + if err := ioutil.WriteFile(inputFilename, ciphertext, 0o600); err != nil { + return nil, err + } + + cmd := exec.Command( + "gpg", + "--output", outputFilename, + "--quiet", + "--decrypt", inputFilename, + ) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return nil, err + } + + return ioutil.ReadFile(outputFilename) +} + +// Encrypt encrypts plaintext for ts's recipient. filename is used as a hint for +// naming temporary files. +func (g *GPG) Encrypt(filename string, plaintext []byte) ([]byte, error) { + tempDir, err := ioutil.TempDir("", "chezmoi-encrypt") + if err != nil { + return nil, err + } + defer os.RemoveAll(tempDir) + + inputFilename := path.Join(tempDir, path.Base(filename)) + if err := ioutil.WriteFile(inputFilename, plaintext, 0o600); err != nil { + return nil, err + } + outputFilename := inputFilename + ".gpg" + + args := []string{ + "--armor", + "--output", outputFilename, + "--quiet", + } + if g.Symmetric { + args = append(args, "--symmetric") + } else { + if g.Recipient != "" { + args = append(args, "--recipient", g.Recipient) + } + args = append(args, "--encrypt") + } + args = append(args, filename) + cmd := exec.Command("gpg", args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return nil, err + } + + return ioutil.ReadFile(outputFilename) +} diff --git a/v2/chezmoi/lazy.go b/v2/chezmoi/lazy.go new file mode 100644 index 000000000000..17b152baa55c --- /dev/null +++ b/v2/chezmoi/lazy.go @@ -0,0 +1,65 @@ +package chezmoi + +import "crypto/sha256" + +// A lazyContents evaluates its contents lazily. +type lazyContents struct { + contentsFunc func() ([]byte, error) + contents []byte + contentsErr error + contentsSHA256 []byte +} + +// A lazyLinkname evaluates its linkname lazily. +type lazyLinkname struct { + linknameFunc func() (string, error) + linkname string + linknameErr error +} + +// Contents returns e's contents. +func (lc *lazyContents) Contents() ([]byte, error) { + if lc == nil { + return nil, nil + } + if lc.contentsFunc != nil { + lc.contents, lc.contentsErr = lc.contentsFunc() + lc.contentsFunc = nil + if lc.contentsErr == nil { + lc.contentsSHA256 = sha256Sum(lc.contents) + } + } + return lc.contents, lc.contentsErr +} + +// ContentsSHA256 returns the SHA256 sum of f's contents. +func (lc *lazyContents) ContentsSHA256() ([]byte, error) { + if lc == nil { + return sha256Sum(nil), nil + } + if lc.contentsSHA256 == nil { + contents, err := lc.Contents() + if err != nil { + return nil, err + } + lc.contentsSHA256 = sha256Sum(contents) + } + return lc.contentsSHA256, nil +} + +// Linkname returns s's linkname. +func (ll *lazyLinkname) Linkname() (string, error) { + if ll == nil { + return "", nil + } + if ll.linknameFunc != nil { + ll.linkname, ll.linknameErr = ll.linknameFunc() + ll.linknameFunc = nil + } + return ll.linkname, ll.linknameErr +} + +func sha256Sum(data []byte) []byte { + sha256SumArr := sha256.Sum256(data) + return sha256SumArr[:] +} diff --git a/v2/chezmoi/lazy_test.go b/v2/chezmoi/lazy_test.go new file mode 100644 index 000000000000..ab99a62d0c29 --- /dev/null +++ b/v2/chezmoi/lazy_test.go @@ -0,0 +1,17 @@ +package chezmoi + +func newLazyContents(contents []byte) *lazyContents { + return &lazyContents{ + contentsFunc: func() ([]byte, error) { + return contents, nil + }, + } +} + +func newLazyLinkname(linkname string) *lazyLinkname { + return &lazyLinkname{ + linknameFunc: func() (string, error) { + return linkname, nil + }, + } +} diff --git a/v2/chezmoi/maybeshellquote.go b/v2/chezmoi/maybeshellquote.go new file mode 100644 index 000000000000..6d527d16fe35 --- /dev/null +++ b/v2/chezmoi/maybeshellquote.go @@ -0,0 +1,61 @@ +package chezmoi + +import ( + "regexp" + "strings" +) + +var needShellQuoteRegexp = regexp.MustCompile(`[^+\-./0-9=A-Z_a-z]`) + +const ( + backslash = '\\' + singleQuote = '\'' +) + +// MaybeShellQuote returns s quoted as a shell argument, if necessary. +func MaybeShellQuote(s string) string { + switch { + case s == "": + return "''" + case needShellQuoteRegexp.MatchString(s): + result := make([]byte, 0, 2+len(s)) + inSingleQuotes := false + for _, b := range []byte(s) { + switch b { + case backslash: + if !inSingleQuotes { + result = append(result, singleQuote) + inSingleQuotes = true + } + result = append(result, backslash, backslash) + case singleQuote: + if inSingleQuotes { + result = append(result, singleQuote) + inSingleQuotes = false + } + result = append(result, backslash, singleQuote) + default: + if !inSingleQuotes { + result = append(result, singleQuote) + inSingleQuotes = true + } + result = append(result, b) + } + } + if inSingleQuotes { + result = append(result, singleQuote) + } + return string(result) + default: + return s + } +} + +// ShellQuoteArgs returs args shell quoted and joined into a single string. +func ShellQuoteArgs(args []string) string { + shellQuotedArgs := make([]string, 0, len(args)) + for _, arg := range args { + shellQuotedArgs = append(shellQuotedArgs, MaybeShellQuote(arg)) + } + return strings.Join(shellQuotedArgs, " ") +} diff --git a/v2/chezmoi/maybeshellquote_test.go b/v2/chezmoi/maybeshellquote_test.go new file mode 100644 index 000000000000..cb145721cc00 --- /dev/null +++ b/v2/chezmoi/maybeshellquote_test.go @@ -0,0 +1,26 @@ +package chezmoi + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMaybeShellQuote(t *testing.T) { + for s, expected := range map[string]string{ + ``: `''`, + `'`: `\'`, + `''`: `\'\'`, + `'a'`: `\''a'\'`, + `\`: `'\\'`, + `\a`: `'\\a'`, + `$a`: `'$a'`, + `a`: `a`, + `a/b`: `a/b`, + `a b`: `'a b'`, + `--arg`: `--arg`, + `--arg=value`: `--arg=value`, + } { + assert.Equal(t, expected, MaybeShellQuote(s), "quoting %q", s) + } +} diff --git a/v2/chezmoi/patternset.go b/v2/chezmoi/patternset.go new file mode 100644 index 000000000000..8037c9f9b9a9 --- /dev/null +++ b/v2/chezmoi/patternset.go @@ -0,0 +1,52 @@ +package chezmoi + +import "github.com/bmatcuk/doublestar" + +// An PatternSet is a set of patterns. +type PatternSet struct { + includes StringSet + excludes StringSet +} + +// A PatternSetOption sets an option on a pattern set. +type PatternSetOption func(*PatternSet) + +// NewPatternSet returns a new PatternSet. +func NewPatternSet(options ...PatternSetOption) *PatternSet { + ps := &PatternSet{ + includes: NewStringSet(), + excludes: NewStringSet(), + } + for _, option := range options { + option(ps) + } + return ps +} + +// Add adds a pattern to ps. +func (ps *PatternSet) Add(pattern string, include bool) error { + if _, err := doublestar.Match(pattern, ""); err != nil { + return nil + } + if include { + ps.includes.Add(pattern) + } else { + ps.excludes.Add(pattern) + } + return nil +} + +// Match returns if name matches any pattern in ps. +func (ps *PatternSet) Match(name string) bool { + for pattern := range ps.excludes { + if ok, _ := doublestar.Match(pattern, name); ok { + return false + } + } + for pattern := range ps.includes { + if ok, _ := doublestar.Match(pattern, name); ok { + return true + } + } + return false +} diff --git a/v2/chezmoi/patternset_test.go b/v2/chezmoi/patternset_test.go new file mode 100644 index 000000000000..06d1c39e8d50 --- /dev/null +++ b/v2/chezmoi/patternset_test.go @@ -0,0 +1,82 @@ +package chezmoi + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPatternSet(t *testing.T) { + for _, tc := range []struct { + name string + ps *PatternSet + expectMatches map[string]bool + }{ + { + name: "empty", + ps: NewPatternSet(), + expectMatches: map[string]bool{ + "foo": false, + }, + }, + { + name: "exact", + ps: mustNewPatternSet(t, map[string]bool{ + "foo": true, + }), + expectMatches: map[string]bool{ + "foo": true, + "bar": false, + }, + }, + { + name: "wildcard", + ps: mustNewPatternSet(t, map[string]bool{ + "b*": true, + }), + expectMatches: map[string]bool{ + "foo": false, + "bar": true, + "baz": true, + }, + }, + { + name: "exclude", + ps: mustNewPatternSet(t, map[string]bool{ + "b*": true, + "baz": false, + }), + expectMatches: map[string]bool{ + "foo": false, + "bar": true, + "baz": false, + }, + }, + { + name: "doublestar", + ps: mustNewPatternSet(t, map[string]bool{ + "**/foo": true, + }), + expectMatches: map[string]bool{ + "foo": true, + "bar/foo": true, + "baz/bar/foo": true, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + for s, expectMatch := range tc.expectMatches { + assert.Equal(t, expectMatch, tc.ps.Match(s)) + } + }) + } +} + +func mustNewPatternSet(t *testing.T, patterns map[string]bool) *PatternSet { + ps := NewPatternSet() + for pattern, exclude := range patterns { + require.NoError(t, ps.Add(pattern, exclude)) + } + return ps +} diff --git a/v2/chezmoi/private.go b/v2/chezmoi/private.go new file mode 100644 index 000000000000..1929ee820d23 --- /dev/null +++ b/v2/chezmoi/private.go @@ -0,0 +1,24 @@ +// +build !windows + +package chezmoi + +import ( + "runtime" + + vfs "github.com/twpayne/go-vfs" +) + +// IsPrivate returns whether path should be considered private. +func IsPrivate(fs vfs.Stater, path string, want bool) (bool, error) { + // Private has no real equivalent on Windows, so always return what the + // caller wants. + if runtime.GOOS == "windows" { + return want, nil + } + + info, err := fs.Stat(path) + if err != nil { + return false, err + } + return info.Mode().Perm()&0o77 == 0, nil +} diff --git a/v2/chezmoi/sourcestate.go b/v2/chezmoi/sourcestate.go new file mode 100644 index 000000000000..0a1fba040d21 --- /dev/null +++ b/v2/chezmoi/sourcestate.go @@ -0,0 +1,448 @@ +package chezmoi + +// FIXME use vfs.PathFS instead of targetDir arg +// FIXME accumulate all source state warnings/errors +// FIXME encryption +// FIXME templates + +import ( + "bufio" + "bytes" + "fmt" + "os" + "path" + "path/filepath" + "sort" + "strings" + "text/template" + + "github.com/coreos/go-semver/semver" + vfs "github.com/twpayne/go-vfs" +) + +// A SourceState is a source state. +type SourceState struct { + fs FileSystem + sourcePath string + umask os.FileMode + entries map[string]SourceStateEntry + ignore *PatternSet + minVersion *semver.Version + remove *PatternSet + templateData interface{} + templateFuncs template.FuncMap + templateOptions []string + templates map[string]*template.Template +} + +// A SourceStateOption sets an option on a source state. +type SourceStateOption func(*SourceState) + +// WithFileSystem sets the filesystem. +func WithFileSystem(fs FileSystem) SourceStateOption { + return func(s *SourceState) { + s.fs = fs + } +} + +// WithSourcePath sets the source path. +func WithSourcePath(sourcePath string) SourceStateOption { + return func(s *SourceState) { + s.sourcePath = sourcePath + } +} + +// WithTemplateData sets the template data. +func WithTemplateData(templateData interface{}) SourceStateOption { + return func(s *SourceState) { + s.templateData = templateData + } +} + +// WithTemplateFuncs sets the template functions. +func WithTemplateFuncs(templateFuncs template.FuncMap) SourceStateOption { + return func(s *SourceState) { + s.templateFuncs = templateFuncs + } +} + +// WithTemplateOptions sets the template options. +func WithTemplateOptions(templateOptions []string) SourceStateOption { + return func(s *SourceState) { + s.templateOptions = templateOptions + } +} + +// WithUmask sets the umask. +func WithUmask(umask os.FileMode) SourceStateOption { + return func(s *SourceState) { + s.umask = umask + } +} + +// NewSourceState creates a new source state with the given options. +func NewSourceState(options ...SourceStateOption) *SourceState { + s := &SourceState{ + umask: 0o22, + entries: make(map[string]SourceStateEntry), + ignore: NewPatternSet(), + remove: NewPatternSet(), + templateOptions: DefaultTemplateOptions, + } + for _, option := range options { + option(s) + } + return s +} + +// Add adds sourceStateEntry to s. +func (s *SourceState) Add() error { + return nil // FIXME +} + +// ApplyAll updates targetDir in fs to match s. +func (s *SourceState) ApplyAll(fs FileSystem, umask os.FileMode, targetDir string) error { + for _, targetName := range s.sortedTargetNames() { + if err := s.ApplyOne(fs, umask, targetDir, targetName); err != nil { + return err + } + } + return nil +} + +// ApplyOne updates targetName in targetDir on fs to match s using fs. +func (s *SourceState) ApplyOne(fs FileSystem, umask os.FileMode, targetDir, targetName string) error { + targetPath := path.Join(targetDir, targetName) + destStateEntry, err := NewDestStateEntry(fs, targetPath) + if err != nil { + return err + } + targetStateEntry := s.entries[targetName].TargetStateEntry() + if err != nil { + return err + } + if err := targetStateEntry.Apply(fs, destStateEntry); err != nil { + return err + } + if targetStateDir, ok := targetStateEntry.(*TargetStateDir); ok { + if targetStateDir.exact { + infos, err := fs.ReadDir(targetPath) + if err != nil { + return err + } + baseNames := make([]string, 0, len(infos)) + for _, info := range infos { + if baseName := info.Name(); baseName != "." && baseName != ".." { + baseNames = append(baseNames, baseName) + } + } + sort.Strings(baseNames) + for _, baseName := range baseNames { + if _, ok := s.entries[path.Join(targetName, baseName)]; !ok { + if err := fs.RemoveAll(path.Join(targetPath, baseName)); err != nil { + return err + } + } + } + } + } + // FIXME chezmoiremove + return nil +} + +// ExecuteTemplateData returns the result of executing template data. +func (s *SourceState) ExecuteTemplateData(name string, data []byte) ([]byte, error) { + tmpl, err := template.New(name).Option(s.templateOptions...).Funcs(s.templateFuncs).Parse(string(data)) + if err != nil { + return nil, err + } + for name, t := range s.templates { + tmpl, err = tmpl.AddParseTree(name, t.Tree) + if err != nil { + return nil, err + } + } + output := &bytes.Buffer{} + if err = tmpl.ExecuteTemplate(output, name, s.templateData); err != nil { + return nil, err + } + return output.Bytes(), nil +} + +// Read reads a source state from sourcePath in fs. +func (s *SourceState) Read() error { + sourceDirPrefix := filepath.ToSlash(s.sourcePath) + pathSeparator + return vfs.Walk(s.fs, s.sourcePath, func(sourcePath string, info os.FileInfo, err error) error { + sourcePath = filepath.ToSlash(sourcePath) + if err != nil { + return err + } + if sourcePath == s.sourcePath { + return nil + } + relPath := strings.TrimPrefix(sourcePath, sourceDirPrefix) + dir, sourceName := path.Split(relPath) + targetDirName := getTargetDirName(dir) + switch { + case info.Name() == ignoreName: + return s.addPatterns(s.ignore, sourcePath, dir) + case info.Name() == removeName: + return s.addPatterns(s.remove, sourcePath, targetDirName) + case info.Name() == templatesDirName: + if err := s.addTemplatesDir(sourcePath); err != nil { + return err + } + return filepath.SkipDir + case info.Name() == versionName: + data, err := s.fs.ReadFile(sourcePath) + if err != nil { + return err + } + version, err := semver.NewVersion(strings.TrimSpace(string(data))) + if err != nil { + return err + } + if s.minVersion == nil || s.minVersion.LessThan(*version) { + s.minVersion = version + } + return nil + case strings.HasPrefix(info.Name(), ignorePrefix): + if info.IsDir() { + return filepath.SkipDir + } + return nil + case info.IsDir(): + dirAttributes := ParseDirAttributes(sourceName) + targetName := path.Join(targetDirName, dirAttributes.Name) + if s.ignore.Match(targetName) { + return nil + } + if sourceStateEntry, ok := s.entries[targetName]; ok { + return &duplicateTargetError{ + targetName: targetName, + sourcePaths: []string{ + sourceStateEntry.Path(), + sourcePath, + }, + } + } + perm := os.FileMode(0o777) + if dirAttributes.Private { + perm &^= 0o77 + } + targetStateDir := &TargetStateDir{ + perm: perm &^ s.umask, + exact: dirAttributes.Exact, + } + s.entries[targetName] = &SourceStateDir{ + path: sourcePath, + attributes: dirAttributes, + targetStateEntry: targetStateDir, + } + return nil + case info.Mode().IsRegular(): + fileAttributes := ParseFileAttributes(sourceName) + targetName := path.Join(targetDirName, fileAttributes.Name) + if s.ignore.Match(targetName) { + return nil + } + if sourceStateEntry, ok := s.entries[targetName]; ok { + return &duplicateTargetError{ + targetName: targetName, + sourcePaths: []string{ + sourceStateEntry.Path(), + sourcePath, + }, + } + } + lazyContents := &lazyContents{ + contentsFunc: func() ([]byte, error) { + return s.fs.ReadFile(sourcePath) + }, + } + var targetStateEntry TargetStateEntry + switch fileAttributes.Type { + case SourceFileTypeFile: + perm := os.FileMode(0o666) + if fileAttributes.Executable { + perm |= 0o111 + } + if fileAttributes.Private { + perm &^= 0o77 + } + targetStateEntry = &TargetStateFile{ + perm: perm &^ s.umask, + lazyContents: lazyContents, + } + case SourceFileTypeScript: + targetStateEntry = &TargetStateScript{ + name: fileAttributes.Name, + lazyContents: lazyContents, + } + case SourceFileTypeSymlink: + targetStateEntry = &TargetStateSymlink{ + lazyLinkname: &lazyLinkname{ + linknameFunc: func() (string, error) { + linknameBytes, err := lazyContents.Contents() + if err != nil { + return "", err + } + return string(linknameBytes), nil + }, + }, + } + default: + panic(nil) + } + s.entries[targetName] = &SourceStateFile{ + path: sourcePath, + attributes: fileAttributes, + lazyContents: lazyContents, + targetStateEntry: targetStateEntry, + } + return nil + default: + return &unsupportedFileTypeError{ + path: sourcePath, + mode: info.Mode(), + } + } + }) +} + +// Remove removes everything in targetDir that matches s's remove pattern set. +func (s *SourceState) Remove(fs FileSystem, targetDir string) error { + // Build a set of targets to remove. + targetDirPrefix := targetDir + pathSeparator + targetPathsToRemove := NewStringSet() + for include := range s.remove.includes { + matches, err := fs.Glob(path.Join(targetDir, include)) + if err != nil { + return err + } + for _, match := range matches { + // Don't remove targets that are excluded from remove. + if !s.remove.Match(strings.TrimPrefix(match, targetDirPrefix)) { + continue + } + targetPathsToRemove.Add(match) + } + } + + sortedTargetPathsToRemove := targetPathsToRemove.Elements() + sort.Strings(sortedTargetPathsToRemove) + for _, targetPath := range sortedTargetPathsToRemove { + if err := fs.RemoveAll(targetPath); err != nil { + return err + } + } + return nil +} + +// Evaluate evaluates every target state entry in s. +func (s *SourceState) Evaluate() error { + for _, targetName := range s.sortedTargetNames() { + sourceStateEntry := s.entries[targetName] + if err := sourceStateEntry.Evaluate(); err != nil { + return err + } + targetStateEntry := sourceStateEntry.TargetStateEntry() + if err := targetStateEntry.Evaluate(); err != nil { + return err + } + } + return nil +} + +func (s *SourceState) addPatterns(ps *PatternSet, path, relPath string) error { + data, err := s.executeTemplate(path) + if err != nil { + return err + } + dir := filepath.Dir(relPath) + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + text := scanner.Text() + if index := strings.IndexRune(text, '#'); index != -1 { + text = text[:index] + } + text = strings.TrimSpace(text) + if text == "" { + continue + } + include := true + if strings.HasPrefix(text, "!") { + include = false + text = strings.TrimPrefix(text, "!") + } + pattern := filepath.Join(dir, text) + if err := ps.Add(pattern, include); err != nil { + return fmt.Errorf("%s: %w", path, err) + } + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("%s: %w", path, err) + } + return nil +} + +func (s *SourceState) addTemplatesDir(templateDir string) error { + templateDirPrefix := filepath.ToSlash(templateDir) + pathSeparator + return vfs.Walk(s.fs, templateDir, func(templatePath string, info os.FileInfo, err error) error { + templatePath = filepath.ToSlash(templatePath) + if err != nil { + return err + } + switch { + case info.Mode().IsRegular(): + contents, err := s.fs.ReadFile(templatePath) + if err != nil { + return err + } + name := strings.TrimPrefix(templatePath, templateDirPrefix) + tmpl, err := template.New(name).Parse(string(contents)) + if err != nil { + return err + } + if s.templates == nil { + s.templates = make(map[string]*template.Template) + } + s.templates[name] = tmpl + return nil + case info.IsDir(): + return nil + default: + return &unsupportedFileTypeError{ + path: templatePath, + mode: info.Mode(), + } + } + }) +} + +func (s *SourceState) executeTemplate(path string) ([]byte, error) { + data, err := s.fs.ReadFile(path) + if err != nil { + return nil, err + } + return s.ExecuteTemplateData(path, data) +} + +func (s *SourceState) sortedTargetNames() []string { + targetNames := make([]string, 0, len(s.entries)) + for targetName := range s.entries { + targetNames = append(targetNames, targetName) + } + sort.Strings(targetNames) + return targetNames +} + +func getTargetDirName(dir string) string { + sourceNames := strings.Split(dir, pathSeparator) + targetNames := make([]string, 0, len(sourceNames)) + for _, sourceName := range sourceNames { + dirAttributes := ParseDirAttributes(sourceName) + targetNames = append(targetNames, dirAttributes.Name) + } + return strings.Join(targetNames, pathSeparator) +} diff --git a/v2/chezmoi/sourcestate_test.go b/v2/chezmoi/sourcestate_test.go new file mode 100644 index 000000000000..bfabd64aed70 --- /dev/null +++ b/v2/chezmoi/sourcestate_test.go @@ -0,0 +1,702 @@ +package chezmoi + +import ( + "archive/tar" + "bytes" + "io" + "io/ioutil" + "os" + "testing" + "text/template" + + "github.com/coreos/go-semver/semver" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/twpayne/go-vfs/vfst" +) + +func TestSourceStateApplyAll(t *testing.T) { + for _, tc := range []struct { + name string + root interface{} + tests []interface{} + }{ + { + name: "empty", + root: map[string]interface{}{ + "/home/user": map[string]interface{}{ + ".local/share/chezmoi": &vfst.Dir{Perm: 0o755}, + }, + }, + }, + { + name: "dir", + root: map[string]interface{}{ + "/home/user": map[string]interface{}{ + ".local/share/chezmoi": map[string]interface{}{ + "foo": &vfst.Dir{Perm: 0o755}, + }, + }, + }, + tests: []interface{}{ + vfst.TestPath("/home/user/foo", + vfst.TestIsDir, + vfst.TestModePerm(0o755), + ), + }, + }, + { + name: "dir_exact", + root: map[string]interface{}{ + "/home/user": map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": "", + }, + ".local/share/chezmoi": map[string]interface{}{ + "exact_foo": &vfst.Dir{Perm: 0o755}, + }, + }, + }, + tests: []interface{}{ + vfst.TestPath("/home/user/foo", + vfst.TestIsDir, + vfst.TestModePerm(0o755), + ), + vfst.TestPath("/home/user/foo/bar", + vfst.TestDoesNotExist, + ), + }, + }, + { + name: "file", + root: map[string]interface{}{ + "/home/user": map[string]interface{}{ + ".local/share/chezmoi": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + tests: []interface{}{ + vfst.TestPath("/home/user/foo", + vfst.TestModeIsRegular, + vfst.TestModePerm(0644), + vfst.TestContentsString("bar"), + ), + }, + }, + { + name: "symlink", + root: map[string]interface{}{ + "/home/user": map[string]interface{}{ + ".local/share/chezmoi": map[string]interface{}{ + "symlink_foo": "bar", + }, + }, + }, + tests: []interface{}{ + vfst.TestPath("/home/user/foo", + vfst.TestModeType(os.ModeSymlink), + vfst.TestSymlinkTarget("bar"), + ), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + fs, cleanup, err := vfst.NewTestFS(tc.root) + require.NoError(t, err) + defer cleanup() + + s := NewSourceState( + WithFileSystem(NewFSFileSystem(fs)), + WithSourcePath("/home/user/.local/share/chezmoi"), + ) + require.NoError(t, s.Read()) + require.NoError(t, s.Evaluate()) + require.NoError(t, s.ApplyAll(NewFSFileSystem(fs), vfst.DefaultUmask, "/home/user")) + + vfst.RunTests(t, fs, "", tc.tests...) + }) + } +} + +func TestSourceStateArchive(t *testing.T) { + fs, cleanup, err := vfst.NewTestFS(map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + ".chezmoiignore": "README.md\n", + ".chezmoiremove": "*.txt\n", + ".chezmoiversion": "1.2.3\n", + ".chezmoitemplates": map[string]interface{}{ + "foo": "bar", + }, + "README.md": "", + "dir": map[string]interface{}{ + "foo": "bar", + }, + "symlink_foo": "bar", + }, + }) + require.NoError(t, err) + defer cleanup() + + s := NewSourceState( + WithFileSystem(NewFSFileSystem(fs)), + WithSourcePath("/home/user/.local/share/chezmoi"), + ) + require.NoError(t, s.Read()) + require.NoError(t, s.Evaluate()) + + b := &bytes.Buffer{} + tarFS := NewTARFileSystem(b, tar.Header{}, vfst.DefaultUmask) + require.NoError(t, s.ApplyAll(tarFS, vfst.DefaultUmask, "")) + + r := tar.NewReader(b) + for _, tc := range []struct { + expectedTypeflag byte + expectedName string + expectedLinkname string + expectedContents []byte + }{ + { + expectedTypeflag: tar.TypeDir, + expectedName: "dir", + }, + { + expectedTypeflag: tar.TypeReg, + expectedName: "dir/foo", + expectedContents: []byte("bar"), + }, + { + expectedTypeflag: tar.TypeSymlink, + expectedName: "foo", + expectedLinkname: "bar", + }, + } { + header, err := r.Next() + require.NoError(t, err) + assert.Equal(t, tc.expectedTypeflag, header.Typeflag) + assert.Equal(t, tc.expectedName, header.Name) + assert.Equal(t, tc.expectedLinkname, header.Linkname) + assert.Equal(t, int64(len(tc.expectedContents)), header.Size) + if tc.expectedContents != nil { + actualContents, err := ioutil.ReadAll(r) + require.NoError(t, err) + assert.Equal(t, tc.expectedContents, actualContents) + } + } + _, err = r.Next() + assert.Equal(t, io.EOF, err) +} + +func TestSourceStateRead(t *testing.T) { + for _, tc := range []struct { + name string + root interface{} + sourceStateOptions []SourceStateOption + expectedError string + expectedSourceState *SourceState + }{ + { + name: "empty", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": &vfst.Dir{Perm: 0o755}, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + ), + }, + { + name: "dir", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + "foo": &vfst.Dir{Perm: 0o755}, + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withEntries(map[string]SourceStateEntry{ + "foo": &SourceStateDir{ + path: "/home/user/.local/share/chezmoi/foo", + attributes: DirAttributes{ + Name: "foo", + }, + targetStateEntry: &TargetStateDir{ + perm: 0o755, + }, + }, + }), + ), + }, + { + name: "file", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + "foo": "bar", + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withEntries(map[string]SourceStateEntry{ + "foo": &SourceStateFile{ + path: "/home/user/.local/share/chezmoi/foo", + attributes: FileAttributes{ + Name: "foo", + Type: SourceFileTypeFile, + }, + lazyContents: newLazyContents([]byte("bar")), + targetStateEntry: &TargetStateFile{ + perm: 0o644, + lazyContents: newLazyContents([]byte("bar")), + }, + }, + }), + ), + }, + { + name: "duplicate_target", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + "foo": "bar", + "foo.tmpl": "bar", + }, + }, + expectedError: "foo: duplicate target (/home/user/.local/share/chezmoi/foo, /home/user/.local/share/chezmoi/foo.tmpl)", + }, + { + name: "duplicate_target", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + "foo": "bar", + "exact_foo": &vfst.Dir{Perm: 0o755}, + }, + }, + expectedError: "foo: duplicate target (/home/user/.local/share/chezmoi/exact_foo, /home/user/.local/share/chezmoi/foo)", + }, + { + name: "symlink", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + "foo": &vfst.Symlink{Target: "bar"}, + }, + }, + expectedError: "/home/user/.local/share/chezmoi/foo: unsupported file type symlink", + }, + { + name: "script", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + "run_foo": "bar", + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withEntries(map[string]SourceStateEntry{ + "foo": &SourceStateFile{ + path: "/home/user/.local/share/chezmoi/run_foo", + attributes: FileAttributes{ + Name: "foo", + Type: SourceFileTypeScript, + }, + lazyContents: newLazyContents([]byte("bar")), + targetStateEntry: &TargetStateScript{ + name: "foo", + lazyContents: newLazyContents([]byte("bar")), + }, + }, + }), + ), + }, + { + name: "symlink", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + "symlink_foo": "bar", + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withEntries(map[string]SourceStateEntry{ + "foo": &SourceStateFile{ + path: "/home/user/.local/share/chezmoi/symlink_foo", + attributes: FileAttributes{ + Name: "foo", + Type: SourceFileTypeSymlink, + }, + lazyContents: newLazyContents([]byte("bar")), + targetStateEntry: &TargetStateSymlink{ + lazyLinkname: newLazyLinkname("bar"), + }, + }, + }), + ), + }, + { + name: "file_in_dir", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": "baz", + }, + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withEntries(map[string]SourceStateEntry{ + "foo": &SourceStateDir{ + path: "/home/user/.local/share/chezmoi/foo", + attributes: DirAttributes{ + Name: "foo", + }, + targetStateEntry: &TargetStateDir{ + perm: 0o755, + }, + }, + "foo/bar": &SourceStateFile{ + path: "/home/user/.local/share/chezmoi/foo/bar", + attributes: FileAttributes{ + Name: "bar", + Type: SourceFileTypeFile, + }, + lazyContents: &lazyContents{ + contents: []byte("baz"), + }, + targetStateEntry: &TargetStateFile{ + perm: 0o644, + lazyContents: &lazyContents{ + contents: []byte("baz"), + }, + }, + }, + }), + ), + }, + { + name: "chezmoiignore", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + ".chezmoiignore": "README.md\n", + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withIgnore( + NewPatternSet( + withAdd(t, "README.md", true), + ), + ), + ), + }, + { + name: "chezmoiignore_ignore_file", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + ".chezmoiignore": "README.md\n", + "README.md": "", + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withIgnore( + NewPatternSet( + withAdd(t, "README.md", true), + ), + ), + ), + }, + { + name: "chezmoiremove", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + ".chezmoiremove": "!*.txt\n", + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withRemove( + NewPatternSet( + withAdd(t, "*.txt", false), + ), + ), + ), + }, + { + name: "chezmoitemplates", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + ".chezmoitemplates": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withTemplates( + map[string]*template.Template{ + "foo": template.Must(template.New("foo").Parse("bar")), + }, + ), + ), + }, + { + name: "chezmoiversion", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + ".chezmoiversion": "1.2.3\n", + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withMinVersion( + &semver.Version{ + Major: 1, + Minor: 2, + Patch: 3, + }, + ), + ), + }, + { + name: "chezmoiversion_multiple", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + ".chezmoiversion": "1.2.3\n", + "foo": map[string]interface{}{ + ".chezmoiversion": "2.3.4\n", + }, + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + withEntries(map[string]SourceStateEntry{ + "foo": &SourceStateDir{ + path: "/home/user/.local/share/chezmoi/foo", + attributes: DirAttributes{ + Name: "foo", + }, + targetStateEntry: &TargetStateDir{ + perm: 0o755, + }, + }, + }), + withMinVersion( + &semver.Version{ + Major: 2, + Minor: 3, + Patch: 4, + }, + ), + ), + }, + { + name: "ignore_dir", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + ".ignore": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + ), + }, + { + name: "ignore_file", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": map[string]interface{}{ + ".ignore": "", + }, + }, + expectedSourceState: NewSourceState( + WithSourcePath("/home/user/.local/share/chezmoi"), + ), + }, + } { + t.Run(tc.name, func(t *testing.T) { + fs, cleanup, err := vfst.NewTestFS(tc.root) + require.NoError(t, err) + defer cleanup() + + sourceStateOptions := []SourceStateOption{ + WithFileSystem(NewFSFileSystem(fs)), + WithSourcePath("/home/user/.local/share/chezmoi"), + } + sourceStateOptions = append(sourceStateOptions, tc.sourceStateOptions...) + s := NewSourceState(sourceStateOptions...) + err = s.Read() + if tc.expectedError != "" { + assert.Error(t, err) + assert.Equal(t, tc.expectedError, err.Error()) + return + } + require.NoError(t, err) + require.NoError(t, s.Evaluate()) + require.NoError(t, tc.expectedSourceState.Evaluate()) + s.fs = nil + assert.Equal(t, tc.expectedSourceState, s) + }) + } +} + +func TestSourceStateRemove(t *testing.T) { + for _, tc := range []struct { + name string + root interface{} + tests []vfst.Test + }{ + { + name: "empty", + root: map[string]interface{}{ + "/home/user/.local/share/chezmoi": &vfst.Dir{Perm: 0o755}, + }, + }, + { + name: "dir", + root: map[string]interface{}{ + "/home/user": map[string]interface{}{ + "dir": &vfst.Dir{Perm: 0o755}, + "file": "", + "symlink": &vfst.Symlink{Target: "file"}, + ".local/share/chezmoi": map[string]interface{}{ + ".chezmoiremove": "dir\n", + }, + }, + }, + tests: []vfst.Test{ + vfst.TestPath("/home/user/dir", + vfst.TestDoesNotExist, + ), + vfst.TestPath("/home/user/file", + vfst.TestModeIsRegular, + ), + vfst.TestPath("/home/user/symlink", + vfst.TestModeType(os.ModeSymlink), + ), + }, + }, + { + name: "file", + root: map[string]interface{}{ + "/home/user": map[string]interface{}{ + "dir": &vfst.Dir{Perm: 0o755}, + "file": "", + "symlink": &vfst.Symlink{Target: "file"}, + ".local/share/chezmoi": map[string]interface{}{ + ".chezmoiremove": "file\n", + }, + }, + }, + tests: []vfst.Test{ + vfst.TestPath("/home/user/dir", + vfst.TestIsDir, + ), + vfst.TestPath("/home/user/file", + vfst.TestDoesNotExist, + ), + vfst.TestPath("/home/user/symlink", + vfst.TestModeType(os.ModeSymlink), + ), + }, + }, + { + name: "symlink", + root: map[string]interface{}{ + "/home/user": map[string]interface{}{ + "dir": &vfst.Dir{Perm: 0o755}, + "file": "", + "symlink": &vfst.Symlink{Target: "file"}, + ".local/share/chezmoi": map[string]interface{}{ + ".chezmoiremove": "symlink\n", + }, + }, + }, + tests: []vfst.Test{ + vfst.TestPath("/home/user/dir", + vfst.TestIsDir, + ), + vfst.TestPath("/home/user/file", + vfst.TestModeIsRegular, + ), + vfst.TestPath("/home/user/symlink", + vfst.TestDoesNotExist, + ), + }, + }, + { + name: "exclude_pattern", + root: map[string]interface{}{ + "/home/user": map[string]interface{}{ + "foo": "", + "bar": "", + "baz": "", + ".local/share/chezmoi": map[string]interface{}{ + ".chezmoiremove": "b*\n!*z\n", + }, + }, + }, + tests: []vfst.Test{ + vfst.TestPath("/home/user/foo", + vfst.TestModeIsRegular, + ), + vfst.TestPath("/home/user/bar", + vfst.TestDoesNotExist, + ), + vfst.TestPath("/home/user/baz", + vfst.TestModeIsRegular, + ), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + fs, cleanup, err := vfst.NewTestFS(tc.root) + require.NoError(t, err) + defer cleanup() + + s := NewSourceState( + WithFileSystem(NewFSFileSystem(fs)), + WithSourcePath("/home/user/.local/share/chezmoi"), + ) + require.NoError(t, s.Read()) + require.NoError(t, s.Evaluate()) + + require.NoError(t, s.Remove(NewFSFileSystem(fs), "/home/user")) + + vfst.RunTests(t, fs, "", tc.tests) + }) + } +} + +func withAdd(t *testing.T, pattern string, include bool) PatternSetOption { + return func(ps *PatternSet) { + require.NoError(t, ps.Add(pattern, include)) + } +} + +func withEntries(entries map[string]SourceStateEntry) SourceStateOption { + return func(s *SourceState) { + s.entries = entries + } +} + +func withIgnore(ignore *PatternSet) SourceStateOption { + return func(s *SourceState) { + s.ignore = ignore + } +} + +func withMinVersion(minVersion *semver.Version) SourceStateOption { + return func(s *SourceState) { + s.minVersion = minVersion + } +} + +func withRemove(remove *PatternSet) SourceStateOption { + return func(s *SourceState) { + s.remove = remove + } +} + +func withTemplates(templates map[string]*template.Template) SourceStateOption { + return func(s *SourceState) { + s.templates = templates + } +} diff --git a/v2/chezmoi/sourcestateentry.go b/v2/chezmoi/sourcestateentry.go new file mode 100644 index 000000000000..5932220595db --- /dev/null +++ b/v2/chezmoi/sourcestateentry.go @@ -0,0 +1,77 @@ +package chezmoi + +import ( + "os" +) + +// A SourceStateEntry represents the state of an entry in the source state. +type SourceStateEntry interface { + Evaluate() error + Path() string + TargetStateEntry() TargetStateEntry + Write(fs FileSystem, umask os.FileMode) error +} + +// A SourceStateDir represents the state of a directory in the source state. +type SourceStateDir struct { + path string + attributes DirAttributes + targetStateEntry TargetStateEntry +} + +// A SourceStateFile represents the state of a file in the source state. +type SourceStateFile struct { + *lazyContents + path string + attributes FileAttributes + targetStateEntry TargetStateEntry +} + +// Evaluate evaluates s and returns any error. +func (s *SourceStateDir) Evaluate() error { + return nil +} + +// Path returns s's path. +func (s *SourceStateDir) Path() string { + return s.path +} + +// TargetStateEntry returns s's target state entry. +func (s *SourceStateDir) TargetStateEntry() TargetStateEntry { + return s.targetStateEntry +} + +// Write writes s to sourceStateDir. +func (s *SourceStateDir) Write(sourceStateDir FileSystem, umask os.FileMode) error { + return sourceStateDir.Mkdir(s.path, 0o777&^umask) +} + +// Evaluate evaluates s and returns any error. +func (s *SourceStateFile) Evaluate() error { + _, err := s.ContentsSHA256() + return err +} + +// Path returns s's path. +func (s *SourceStateFile) Path() string { + return s.path +} + +// TargetStateEntry returns s's target state entry. +func (s *SourceStateFile) TargetStateEntry() TargetStateEntry { + return s.targetStateEntry +} + +// Write writes s to sourceStateDir. +func (s *SourceStateFile) Write(sourceStateDir FileSystem, umask os.FileMode) error { + contents, err := s.Contents() + if err != nil { + return err + } + currContents, err := sourceStateDir.ReadFile(s.path) + if err != nil && !os.IsNotExist(err) { + return err + } + return sourceStateDir.WriteFile(s.path, contents, 0o666&^umask, currContents) +} diff --git a/v2/chezmoi/stringset.go b/v2/chezmoi/stringset.go new file mode 100644 index 000000000000..491b433f5bac --- /dev/null +++ b/v2/chezmoi/stringset.go @@ -0,0 +1,33 @@ +package chezmoi + +// A StringSet is a set of strings. +type StringSet map[string]struct{} + +// NewStringSet returns a new StringSet containing elements. +func NewStringSet(elements ...string) StringSet { + s := make(StringSet) + s.Add(elements...) + return s +} + +// Add adds elements to s. +func (s StringSet) Add(elements ...string) { + for _, element := range elements { + s[element] = struct{}{} + } +} + +// Contains returns true if element is in s. +func (s StringSet) Contains(element string) bool { + _, ok := s[element] + return ok +} + +// Elements returns all the elements of s. +func (s StringSet) Elements() []string { + elements := make([]string, 0, len(s)) + for element := range s { + elements = append(elements, element) + } + return elements +} diff --git a/v2/chezmoi/tarfilesystem.go b/v2/chezmoi/tarfilesystem.go new file mode 100644 index 000000000000..0cd8d7db89c0 --- /dev/null +++ b/v2/chezmoi/tarfilesystem.go @@ -0,0 +1,123 @@ +package chezmoi + +import ( + "archive/tar" + "io" + "os" + "os/exec" + "os/user" + "strconv" + "time" +) + +// A TARFileSystem is a FileSystem that writes to a TAR archive. +type TARFileSystem struct { + *EmptyFileSystemReader + w *tar.Writer + headerTemplate tar.Header + umask os.FileMode +} + +// NewTARFileSystem returns a new TARFileSystem that writes a TAR file to w. +func NewTARFileSystem(w io.Writer, headerTemplate tar.Header, umask os.FileMode) *TARFileSystem { + return &TARFileSystem{ + w: tar.NewWriter(w), + headerTemplate: headerTemplate, + umask: umask, + } +} + +// Chmod implements FileSystem.Chmod. +func (fs *TARFileSystem) Chmod(name string, mode os.FileMode) error { + return os.ErrPermission +} + +// Close closes m. +func (fs *TARFileSystem) Close() error { + return fs.w.Close() +} + +// IdempotentCmdOutput implements FileSystem.IdempotentCmdOutput. +func (fs *TARFileSystem) IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) { + return cmd.Output() +} + +// Mkdir implements FileSystem.Mkdir. +func (fs *TARFileSystem) Mkdir(name string, perm os.FileMode) error { + header := fs.headerTemplate + header.Typeflag = tar.TypeDir + header.Name = name + header.Mode = int64(perm &^ fs.umask) + return fs.w.WriteHeader(&header) +} + +// RemoveAll implements FileSystem.RemoveAll. +func (fs *TARFileSystem) RemoveAll(name string) error { + return os.ErrPermission +} + +// Rename implements FileSystem.Rename. +func (fs *TARFileSystem) Rename(oldpath, newpath string) error { + return os.ErrPermission +} + +// RunCmd implements FileSystem.RunCmd. +func (fs *TARFileSystem) RunCmd(cmd *exec.Cmd) error { + // FIXME need to work out what to do with scripts + return nil +} + +// WriteFile implements FileSystem.WriteFile. +func (fs *TARFileSystem) WriteFile(filename string, data []byte, perm os.FileMode, currData []byte) error { + header := fs.headerTemplate + header.Typeflag = tar.TypeReg + header.Name = filename + header.Size = int64(len(data)) + header.Mode = int64(perm &^ fs.umask) + if err := fs.w.WriteHeader(&header); err != nil { + return err + } + _, err := fs.w.Write(data) + return err +} + +// WriteSymlink implements FileSystem.WriteSymlink. +func (fs *TARFileSystem) WriteSymlink(oldname, newname string) error { + header := fs.headerTemplate + header.Typeflag = tar.TypeSymlink + header.Name = newname + header.Linkname = oldname + return fs.w.WriteHeader(&header) +} + +// TARHeaderTemplate returns a tar.Header template populated with the current +// user and time. +func TARHeaderTemplate() tar.Header { + // Attempt to lookup the current user. Ignore errors because the default + // zero values are reasonable. + var ( + uid int + gid int + Uname string + Gname string + ) + if currentUser, err := user.Current(); err == nil { + uid, _ = strconv.Atoi(currentUser.Uid) + gid, _ = strconv.Atoi(currentUser.Gid) + Uname = currentUser.Username + if group, err := user.LookupGroupId(currentUser.Gid); err == nil { + Gname = group.Name + } + } + + now := time.Now() + return tar.Header{ + Uid: uid, + Gid: gid, + Uname: Uname, + Gname: Gname, + ModTime: now, + AccessTime: now, + ChangeTime: now, + } +} diff --git a/v2/chezmoi/tarfilesystem_test.go b/v2/chezmoi/tarfilesystem_test.go new file mode 100644 index 000000000000..c4e6b0d87713 --- /dev/null +++ b/v2/chezmoi/tarfilesystem_test.go @@ -0,0 +1,3 @@ +package chezmoi + +var _ FileSystem = &TARFileSystem{} diff --git a/v2/chezmoi/targetstateentry.go b/v2/chezmoi/targetstateentry.go new file mode 100644 index 000000000000..2521e9185072 --- /dev/null +++ b/v2/chezmoi/targetstateentry.go @@ -0,0 +1,285 @@ +package chezmoi + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path" +) + +// A TargetStateEntry represents the state of an entry in the target state. +type TargetStateEntry interface { + Apply(fs FileSystem, destStateEntry DestStateEntry) error + Equal(destStateEntry DestStateEntry) (bool, error) + Evaluate() error +} + +// A TargetStateAbsent represents the absence of an entry in the target state. +type TargetStateAbsent struct{} + +// A TargetStateDir represents the state of a directory in the target state. +type TargetStateDir struct { + perm os.FileMode + exact bool +} + +// A TargetStateFile represents the state of a file in the target state. +type TargetStateFile struct { + perm os.FileMode + *lazyContents +} + +// A TargetStateScript represents the state of a script. +// FIXME maybe scripts should be handled specially +type TargetStateScript struct { + name string + *lazyContents +} + +// A TargetStateSymlink represents the state of a symlink in the target state. +type TargetStateSymlink struct { + *lazyLinkname +} + +// Apply updates destStateEntry to match t. +func (t *TargetStateAbsent) Apply(fs FileSystem, destStateEntry DestStateEntry) error { + if _, ok := destStateEntry.(*DestStateAbsent); ok { + return nil + } + return fs.RemoveAll(destStateEntry.Path()) +} + +// Equal returns true if destStateEntry matches t. +func (t *TargetStateAbsent) Equal(destStateEntry DestStateEntry) (bool, error) { + _, ok := destStateEntry.(*DestStateAbsent) + return ok, nil +} + +// Evaluate evaluates t. +func (t *TargetStateAbsent) Evaluate() error { + return nil +} + +// Apply updates destStateEntry to match t. It does not recurse. +func (t *TargetStateDir) Apply(fs FileSystem, destStateEntry DestStateEntry) error { + if destStateDir, ok := destStateEntry.(*DestStateDir); ok { + if destStateDir.perm == t.perm { + return nil + } + return fs.Chmod(destStateDir.Path(), t.perm) + } + if err := destStateEntry.Remove(fs); err != nil { + return err + } + return fs.Mkdir(destStateEntry.Path(), t.perm) +} + +// Equal returns true if destStateEntry matches t. It does not recurse. +func (t *TargetStateDir) Equal(destStateEntry DestStateEntry) (bool, error) { + destStateDir, ok := destStateEntry.(*DestStateDir) + if !ok { + return false, nil + } + return destStateDir.perm == t.perm, nil +} + +// Evaluate evaluates t. +func (t *TargetStateDir) Evaluate() error { + return nil +} + +// Apply updates destStateEntry to match t. +func (t *TargetStateFile) Apply(fs FileSystem, destStateEntry DestStateEntry) error { + var destContents []byte + destIsFileAndPermMatches := false + if destStateFile, ok := destStateEntry.(*DestStateFile); ok { + // Compare file contents using only their SHA256 sums. This is so that + // we can compare last-written states without storing the full contents + // of each file written. + destContentsSHA256, err := destStateFile.ContentsSHA256() + if err != nil { + return err + } + contentsSHA256, err := t.ContentsSHA256() + if err != nil { + return err + } + if bytes.Equal(destContentsSHA256, contentsSHA256) { + if destStateFile.perm == t.perm { + return nil + } + return fs.Chmod(destStateFile.Path(), t.perm) + } + destContents, err = destStateFile.Contents() + if err != nil { + return err + } + destIsFileAndPermMatches = destStateFile.perm == t.perm + } + contents, err := t.Contents() + if err != nil { + return err + } + // If the destination entry is a file and its permissions match the target + // state then we can rely on destDir.WriteFile to replace the file, possibly + // atomically. Otherwise we must remove the destination entry before writing + // the new file. If the destination entry is not a file then it must be + // removed as fs.WriteFile will not overwrite non-files. If the + // destination entry is a file but the permissions do not matchq then we + // must remove the file first because there is no way atomically update the + // permissions and the content simultaneously. + // + // FIXME update destDir.WriteFile to truncate, update perms, then write content + if !destIsFileAndPermMatches { + if err := destStateEntry.Remove(fs); err != nil { + return err + } + } + return fs.WriteFile(destStateEntry.Path(), contents, t.perm, destContents) +} + +// Equal returns true if destStateEntry matches t. +func (t *TargetStateFile) Equal(destStateEntry DestStateEntry) (bool, error) { + destStateFile, ok := destStateEntry.(*DestStateFile) + if !ok { + return false, nil + } + if destStateFile.perm != t.perm { + return false, nil + } + destContentsSHA256, err := destStateFile.ContentsSHA256() + if err != nil { + return false, err + } + contentsSHA256, err := t.ContentsSHA256() + if err != nil { + return false, err + } + return bytes.Equal(destContentsSHA256, contentsSHA256), nil +} + +// Evaluate evaluates t. +func (t *TargetStateFile) Evaluate() error { + _, err := t.ContentsSHA256() + return err +} + +// Apply does nothing for scripts. +// FIXME maybe this should call Run? +func (t *TargetStateScript) Apply(fs FileSystem, destStateEntry DestStateEntry) error { + return nil +} + +// Equal returns true if destStateEntry matches t. +func (t *TargetStateScript) Equal(destStateEntry DestStateEntry) (bool, error) { + // Scripts are independent of the destination state. + // FIXME maybe the destination state should store the sha256 sums of executed scripts + return true, nil +} + +// Evaluate evaluates t. +func (t *TargetStateScript) Evaluate() error { + _, err := t.ContentsSHA256() + return err +} + +// Run runs t. +func (t *TargetStateScript) Run(fs FileSystem) error { + contents, err := t.Contents() + if err != nil { + return err + } + if len(bytes.TrimSpace(contents)) == 0 { + // Don't execute empty scripts. + return nil + } + + // FIXME once_ + // FIXME verbose and dry run -- maybe handled by destDir? + + // Write the temporary script file. Put the randomness at the front of the + // filename to preserve any file extension for Windows scripts. + f, err := ioutil.TempFile("", "*."+path.Base(t.name)) + if err != nil { + return err + } + defer func() { + _ = os.RemoveAll(f.Name()) + }() + + // Make the script private before writing it in case it contains any + // secrets. + if err := f.Chmod(0o700); err != nil { + return err + } + if _, err := f.Write(contents); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + + // Run the temporary script file. + //nolint:gosec + c := exec.Command(f.Name()) + // c.Dir = path.Join(applyOptions.DestDir, filepath.Dir(s.targetName)) // FIXME + c.Stdin = os.Stdin + c.Stdout = os.Stdout + c.Stderr = os.Stderr + if err := fs.RunCmd(c); err != nil { // FIXME + return err + } + + // FIXME record run if once_ + + return nil +} + +// Apply updates destStateEntry to match t. +func (t *TargetStateSymlink) Apply(fs FileSystem, destStateEntry DestStateEntry) error { + if destStateSymlink, ok := destStateEntry.(*DestStateSymlink); ok { + destLinkname, err := destStateSymlink.Linkname() + if err != nil { + return err + } + linkname, err := t.Linkname() + if err != nil { + return err + } + if destLinkname == linkname { + return nil + } + } + linkname, err := t.Linkname() + if err != nil { + return err + } + if err := destStateEntry.Remove(fs); err != nil { + return err + } + return fs.WriteSymlink(linkname, destStateEntry.Path()) +} + +// Equal returns true if destStateEntry matches t. +func (t *TargetStateSymlink) Equal(destStateEntry DestStateEntry) (bool, error) { + destStateSymlink, ok := destStateEntry.(*DestStateSymlink) + if !ok { + return false, nil + } + destLinkname, err := destStateSymlink.Linkname() + if err != nil { + return false, err + } + linkname, err := t.Linkname() + if err != nil { + return false, nil + } + return destLinkname == linkname, nil +} + +// Evaluate evaluates t. +func (t *TargetStateSymlink) Evaluate() error { + _, err := t.Linkname() + return err +} diff --git a/v2/chezmoi/targetstatentry_test.go b/v2/chezmoi/targetstatentry_test.go new file mode 100644 index 000000000000..2d4f5115d0c8 --- /dev/null +++ b/v2/chezmoi/targetstatentry_test.go @@ -0,0 +1,173 @@ +package chezmoi + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + "github.com/twpayne/go-vfs/vfst" +) + +func TestTargetStateEntryApplyAndEqual(t *testing.T) { + for _, tc1 := range []struct { + name string + targetStateEntry TargetStateEntry + }{ + { + name: "absent", + targetStateEntry: &TargetStateAbsent{}, + }, + { + name: "dir", + targetStateEntry: &TargetStateDir{ + perm: 0o755, + }, + }, + { + name: "file", + targetStateEntry: &TargetStateFile{ + perm: 0o644, + lazyContents: &lazyContents{ + contents: []byte("bar"), + }, + }, + }, + { + name: "file_empty", + targetStateEntry: &TargetStateFile{ + perm: 0o644, + }, + }, + { + name: "file_empty_ok", + targetStateEntry: &TargetStateFile{ + perm: 0o644, + }, + }, + { + name: "symlink", + targetStateEntry: &TargetStateSymlink{ + lazyLinkname: &lazyLinkname{ + linkname: "bar", + }, + }, + }, + } { + t.Run(tc1.name, func(t *testing.T) { + for _, tc2 := range []struct { + name string + root interface{} + }{ + { + name: "not_present", + root: map[string]interface{}{ + "/home/user": &vfst.Dir{Perm: 0o755}, + }, + }, + { + name: "existing_dir", + root: map[string]interface{}{ + "/home/user/foo": &vfst.Dir{Perm: 0o755}, + }, + }, + { + name: "existing_dir_chmod", + root: map[string]interface{}{ + "/home/user/foo": &vfst.Dir{Perm: 0o644}, + }, + }, + { + name: "existing_file_empty", + root: map[string]interface{}{ + "/home/user/foo": "", + }, + }, + { + name: "existing_file_contents", + root: map[string]interface{}{ + "/home/user/foo": "baz", + }, + }, + { + name: "existing_file_chmod", + root: map[string]interface{}{ + "/home/user/foo": &vfst.File{ + Perm: 0o755, + }, + }, + }, + { + name: "existing_symlink", + root: map[string]interface{}{ + "/home/user/bar": "", + "/home/user/foo": &vfst.Symlink{Target: "bar"}, + }, + }, + { + name: "existing_symlink_broken", + root: map[string]interface{}{ + "/home/user/foo": &vfst.Symlink{Target: "bar"}, + }, + }, + } { + t.Run(tc2.name, func(t *testing.T) { + fs, cleanup, err := vfst.NewTestFS(tc2.root) + require.NoError(t, err) + defer cleanup() + + // Read the initial destination state entry from fs. + destStateEntry, err := NewDestStateEntry(fs, "/home/user/foo") + require.NoError(t, err) + + // Apply the target state entry. + require.NoError(t, tc1.targetStateEntry.Apply(NewFSFileSystem(fs), destStateEntry)) + + // Verify that the destination state entry matches the + // desired state. + vfst.RunTests(t, fs, "", vfst.TestPath("/home/user/foo", targetStateTest(t, tc1.targetStateEntry)...)) + + // Read the updated destination state entry from fs and + // verify that it is equal to the target state entry. + newDestStateEntry, err := NewDestStateEntry(fs, "/home/user/foo") + require.NoError(t, err) + equal, err := tc1.targetStateEntry.Equal(newDestStateEntry) + require.NoError(t, err) + require.True(t, equal) + }) + } + }) + } +} + +func targetStateTest(t *testing.T, ts TargetStateEntry) []vfst.PathTest { + switch ts := ts.(type) { + case *TargetStateAbsent: + return []vfst.PathTest{ + vfst.TestDoesNotExist, + } + case *TargetStateDir: + return []vfst.PathTest{ + vfst.TestIsDir, + vfst.TestModePerm(ts.perm), + } + case *TargetStateFile: + expectedContents, err := ts.Contents() + require.NoError(t, err) + return []vfst.PathTest{ + vfst.TestModeIsRegular, + vfst.TestModePerm(ts.perm), + vfst.TestContents(expectedContents), + } + case *TargetStateScript: + return nil // FIXME how to verify scripts? + case *TargetStateSymlink: + expectedLinkname, err := ts.Linkname() + require.NoError(t, err) + return []vfst.PathTest{ + vfst.TestModeType(os.ModeSymlink), + vfst.TestSymlinkTarget(expectedLinkname), + } + default: + return nil + } +} diff --git a/v2/chezmoi/verbosefilesystem.go b/v2/chezmoi/verbosefilesystem.go new file mode 100644 index 000000000000..6bde762c02ae --- /dev/null +++ b/v2/chezmoi/verbosefilesystem.go @@ -0,0 +1,213 @@ +package chezmoi + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path" + "strings" + + "github.com/pkg/diff" +) + +// A VerboseFileSystem wraps a FileSystem and logs all of the actions it executes and +// any errors as pseudo shell commands. +type VerboseFileSystem struct { + fs FileSystem + w io.Writer + colored bool + maxDiffDataSize int +} + +// NewVerboseFileSystem returns a new VerboseFileSystem. +func NewVerboseFileSystem(w io.Writer, m FileSystem, colored bool, maxDiffDataSize int) *VerboseFileSystem { + return &VerboseFileSystem{ + fs: m, + w: w, + colored: colored, + maxDiffDataSize: maxDiffDataSize, + } +} + +// Chmod implements FileSystem.Chmod. +func (fs *VerboseFileSystem) Chmod(name string, mode os.FileMode) error { + action := fmt.Sprintf("chmod %o %s", mode, MaybeShellQuote(name)) + err := fs.fs.Chmod(name, mode) + if err == nil { + _, _ = fmt.Fprintln(fs.w, action) + } else { + _, _ = fmt.Fprintf(fs.w, "%s: %v\n", action, err) + } + return err +} + +// Glob implements FileSystem.Glob. +func (fs *VerboseFileSystem) Glob(pattern string) ([]string, error) { + return fs.fs.Glob(pattern) +} + +// IdempotentCmdOutput implements FileSystem.IdempotentCmdOutput. +func (fs *VerboseFileSystem) IdempotentCmdOutput(cmd *exec.Cmd) ([]byte, error) { + action := cmdString(cmd) + output, err := fs.fs.IdempotentCmdOutput(cmd) + if err != nil { + _, _ = fmt.Fprintf(fs.w, "%s: %v\n", action, err) + } + return output, err +} + +// Lstat implements FileSystem.Lstat. +func (fs *VerboseFileSystem) Lstat(name string) (os.FileInfo, error) { + return fs.fs.Lstat(name) +} + +// ReadDir implements FileSystem.ReadDir. +func (fs *VerboseFileSystem) ReadDir(dirname string) ([]os.FileInfo, error) { + return fs.fs.ReadDir(dirname) +} + +// ReadFile implements FileSystem.ReadFile. +func (fs *VerboseFileSystem) ReadFile(filename string) ([]byte, error) { + return fs.fs.ReadFile(filename) +} + +// Readlink implements FileSystem.Readlink. +func (fs *VerboseFileSystem) Readlink(name string) (string, error) { + return fs.fs.Readlink(name) +} + +// Mkdir implements FileSystem.Mkdir. +func (fs *VerboseFileSystem) Mkdir(name string, perm os.FileMode) error { + action := fmt.Sprintf("mkdir -m %o %s", perm, MaybeShellQuote(name)) + err := fs.fs.Mkdir(name, perm) + if err == nil { + _, _ = fmt.Fprintln(fs.w, action) + } else { + _, _ = fmt.Fprintf(fs.w, "%s: %v\n", action, err) + } + return err +} + +// RemoveAll implements FileSystem.RemoveAll. +func (fs *VerboseFileSystem) RemoveAll(name string) error { + action := fmt.Sprintf("rm -rf %s", MaybeShellQuote(name)) + err := fs.fs.RemoveAll(name) + if err == nil { + _, _ = fmt.Fprintln(fs.w, action) + } else { + _, _ = fmt.Fprintf(fs.w, "%s: %v\n", action, err) + } + return err +} + +// Rename implements FileSystem.Rename. +func (fs *VerboseFileSystem) Rename(oldpath, newpath string) error { + action := fmt.Sprintf("mv %s %s", MaybeShellQuote(oldpath), MaybeShellQuote(newpath)) + err := fs.fs.Rename(oldpath, newpath) + if err == nil { + _, _ = fmt.Fprintln(fs.w, action) + } else { + _, _ = fmt.Fprintf(fs.w, "%s: %v\n", action, err) + } + return err +} + +// RunCmd implements FileSystem.RunCmd. +func (fs *VerboseFileSystem) RunCmd(cmd *exec.Cmd) error { + action := cmdString(cmd) + err := fs.fs.RunCmd(cmd) + if err == nil { + _, _ = fmt.Fprintln(fs.w, action) + } else { + _, _ = fmt.Fprintf(fs.w, "%s: %v\n", action, err) + } + return err +} + +// Stat implements FileSystem.Stat. +func (fs *VerboseFileSystem) Stat(name string) (os.FileInfo, error) { + return fs.fs.Stat(name) +} + +// WriteFile implements FileSystem.WriteFile. +func (fs *VerboseFileSystem) WriteFile(name string, data []byte, perm os.FileMode, currData []byte) error { + action := fmt.Sprintf("install -m %o /dev/null %s", perm, MaybeShellQuote(name)) + err := fs.fs.WriteFile(name, data, perm, currData) + if err == nil { + _, _ = fmt.Fprintln(fs.w, action) + // Don't print diffs if either file is binary. + if isBinary(currData) || isBinary(data) { + return nil + } + // Don't print diffs if either file is too large. + if fs.maxDiffDataSize != 0 { + if len(currData) > fs.maxDiffDataSize || len(data) > fs.maxDiffDataSize { + return nil + } + } + aLines, err := splitLines(currData) + if err != nil { + return err + } + bLines, err := splitLines(data) + if err != nil { + return err + } + ab := diff.Strings(aLines, bLines) + e := diff.Myers(context.Background(), ab).WithContextSize(3) + opts := []diff.WriteOpt{ + diff.Names( + path.Join("a", name), + path.Join("b", name), + ), + } + if fs.colored { + opts = append(opts, diff.TerminalColor()) + } + if _, err := e.WriteUnified(fs.w, ab, opts...); err != nil { + return err + } + } else { + _, _ = fmt.Fprintf(fs.w, "%s: %v\n", action, err) + } + return err +} + +// WriteSymlink implements FileSystem.WriteSymlink. +func (fs *VerboseFileSystem) WriteSymlink(oldname, newname string) error { + action := fmt.Sprintf("ln -sf %s %s", MaybeShellQuote(oldname), MaybeShellQuote(newname)) + err := fs.fs.WriteSymlink(oldname, newname) + if err == nil { + _, _ = fmt.Fprintln(fs.w, action) + } else { + _, _ = fmt.Fprintf(fs.w, "%s: %v\n", action, err) + } + return err +} + +// cmdString returns a string representation of cmd. +func cmdString(cmd *exec.Cmd) string { + s := ShellQuoteArgs(append([]string{cmd.Path}, cmd.Args[1:]...)) + if cmd.Dir == "" { + return s + } + return fmt.Sprintf("( cd %s && %s )", MaybeShellQuote(cmd.Dir), s) +} + +func isBinary(data []byte) bool { + return len(data) != 0 && !strings.HasPrefix(http.DetectContentType(data), "text/") +} + +func splitLines(data []byte) ([]string, error) { + var lines []string + s := bufio.NewScanner(bytes.NewReader(data)) + for s.Scan() { + lines = append(lines, s.Text()) + } + return lines, s.Err() +} diff --git a/v2/chezmoi/verbosefilesystem_test.go b/v2/chezmoi/verbosefilesystem_test.go new file mode 100644 index 000000000000..28cded56d5ff --- /dev/null +++ b/v2/chezmoi/verbosefilesystem_test.go @@ -0,0 +1,3 @@ +package chezmoi + +var _ FileSystem = &VerboseFileSystem{}