From 1632f28d1bcf20cd9297fc7bcaf4233ea0c924e1 Mon Sep 17 00:00:00 2001 From: Dominik Schulz Date: Mon, 24 Oct 2022 21:29:27 +0200 Subject: [PATCH] Add .gitconfig parser This commit adds yet another config handler for gopass. It is based on the format used by git itself. This has the potential to address a lot of long standing issues, but it also causes a lot of changes to how we handle configuration, so bugs are inevitable. Fixes #1567 Fixes #1764 Fixes #1819 Fixes #1878 Fixes #2387 Fixes #2418 RELEASE_NOTES=[BREAKING] New config format based on git config. Signed-off-by: Dominik Schulz Co-authored-by: Yolan Romailler address comments Signed-off-by: Dominik Schulz --- .github/workflows/autorelease.yml | 1 + CHANGELOG.md | 2 + Makefile | 2 +- docs/config.md | 67 ++- docs/setup.md | 4 +- fish.completion | 36 -- helpers/changelog/main.go | 8 + helpers/man/main.go | 3 +- helpers/postrel/main.go | 24 +- helpers/release/main.go | 46 +- internal/action/action_test.go | 16 +- internal/action/aliases.go | 60 +-- internal/action/audit_test.go | 1 + internal/action/binary_test.go | 5 + internal/action/clone.go | 5 - internal/action/clone_test.go | 9 +- internal/action/commands.go | 27 +- internal/action/commands_test.go | 1 + internal/action/completion_test.go | 1 + internal/action/config.go | 61 +-- internal/action/config_test.go | 76 ++-- internal/action/copy_test.go | 6 +- internal/action/create.go | 2 +- internal/action/create_test.go | 5 +- internal/action/delete_test.go | 1 + internal/action/edit_test.go | 2 + internal/action/env_test.go | 5 + internal/action/find_test.go | 7 +- internal/action/fsck.go | 17 +- internal/action/fsck_test.go | 2 + internal/action/generate.go | 25 +- internal/action/generate_test.go | 17 +- internal/action/grep_test.go | 1 + internal/action/history_test.go | 6 +- internal/action/init.go | 6 - internal/action/init_test.go | 1 + internal/action/insert_test.go | 10 +- internal/action/list.go | 3 +- internal/action/list_test.go | 10 +- internal/action/mount.go | 8 - internal/action/mount_test.go | 1 + internal/action/move_test.go | 1 + internal/action/otp.go | 2 +- internal/action/otp_test.go | 1 + internal/action/process_test.go | 1 + internal/action/rcs_test.go | 1 + internal/action/recipients_test.go | 2 + internal/action/repl.go | 6 +- internal/action/setup.go | 17 +- internal/action/show.go | 23 +- internal/action/show_test.go | 48 +- internal/action/sync.go | 30 +- internal/action/sync_test.go | 1 + internal/action/templates_test.go | 1 + internal/action/unclip_test.go | 1 + internal/backend/crypto/age/askpass.go | 3 +- internal/backend/crypto/age/context.go | 25 -- internal/backend/crypto/age/ssh.go | 2 - internal/backend/crypto/plain/backend.go | 5 - internal/backend/crypto/plain/backend_test.go | 2 - internal/backend/storage/gitfs/config.go | 43 +- internal/backend/storage/gitfs/git.go | 28 +- internal/config/config.go | 306 ++++++++----- internal/config/context.go | 42 +- internal/config/docs_test.go | 291 ++++++++++++ internal/config/legacy.go | 339 ++------------ internal/config/legacy/config.go | 153 +++++++ internal/config/{ => legacy}/config_test.go | 18 +- internal/config/{ => legacy}/io.go | 17 +- internal/config/{ => legacy}/io_test.go | 2 +- internal/config/legacy/legacy.go | 329 ++++++++++++++ internal/config/legacy/location.go | 65 +++ internal/config/location.go | 20 +- internal/config/location_test.go | 14 +- internal/config/utils.go | 18 + internal/create/wizard.go | 6 +- internal/notify/notify_darwin.go | 4 +- internal/notify/notify_dbus.go | 4 +- internal/notify/notify_windows.go | 4 +- internal/store/leaf/fsck_test.go | 2 - internal/store/leaf/list_test.go | 2 - internal/store/leaf/move_test.go | 5 - internal/store/leaf/read.go | 3 +- internal/store/leaf/recipients.go | 3 +- internal/store/leaf/recipients_test.go | 1 - internal/store/leaf/write.go | 5 + internal/store/root/convert.go | 10 +- internal/store/root/init.go | 20 +- internal/store/root/init_test.go | 4 +- internal/store/root/mount.go | 10 +- internal/store/root/store.go | 8 +- internal/store/root/store_test.go | 10 +- main.go | 14 +- main_test.go | 7 +- pkg/clipboard/clipboard_others.go | 4 +- pkg/clipboard/clipboard_windows.go | 4 +- pkg/ctxutil/ctxutil.go | 78 ---- pkg/ctxutil/ctxutil_test.go | 46 -- pkg/gitconfig/config.go | 379 ++++++++++++++++ pkg/gitconfig/config_test.go | 132 ++++++ pkg/gitconfig/configs.go | 363 +++++++++++++++ pkg/gitconfig/doc.go | 61 +++ pkg/gitconfig/gitconfig.go | 15 + pkg/gitconfig/gitconfig_test.go | 424 ++++++++++++++++++ pkg/gitconfig/utils.go | 35 ++ pkg/gitconfig/utils_test.go | 49 ++ pkg/gopass/api/api.go | 2 +- pkg/pwgen/cryptic.go | 5 +- pkg/pwgen/cryptic_test.go | 3 +- pkg/pwgen/pwrules/aliases.go | 149 ++---- pkg/pwgen/pwrules/change.go | 6 +- pkg/pwgen/pwrules/pwrules.go | 5 +- tests/config_test.go | 47 +- tests/find_test.go | 4 +- tests/gptest/gunit.go | 25 +- tests/gptest/unit.go | 33 +- tests/show_test.go | 25 +- tests/tester.go | 20 +- zsh.completion | 7 - 119 files changed, 3164 insertions(+), 1326 deletions(-) create mode 100644 internal/config/docs_test.go create mode 100644 internal/config/legacy/config.go rename internal/config/{ => legacy}/config_test.go (76%) rename internal/config/{ => legacy}/io.go (93%) rename internal/config/{ => legacy}/io_test.go (99%) create mode 100644 internal/config/legacy/legacy.go create mode 100644 internal/config/legacy/location.go create mode 100644 internal/config/utils.go create mode 100644 pkg/gitconfig/config.go create mode 100644 pkg/gitconfig/config_test.go create mode 100644 pkg/gitconfig/configs.go create mode 100644 pkg/gitconfig/doc.go create mode 100644 pkg/gitconfig/gitconfig.go create mode 100644 pkg/gitconfig/gitconfig_test.go create mode 100644 pkg/gitconfig/utils.go create mode 100644 pkg/gitconfig/utils_test.go diff --git a/.github/workflows/autorelease.yml b/.github/workflows/autorelease.yml index fd7a47ff35..dd9ae9e4a9 100644 --- a/.github/workflows/autorelease.yml +++ b/.github/workflows/autorelease.yml @@ -86,6 +86,7 @@ jobs: run: | for D in dist/*.deb; do curl -H"X-Filename: ${D}" -H"X-Apikey: ${APIKEY}" -XPOST --data-binary @$D https://packages.gopass.pw/repos/gopass/upload + curl -H"X-Filename: ${D}" -H"X-Apikey: ${APIKEY}" -XPOST --data-binary @$D https://packages.gopass.pw/repos/gopass-unstable/upload done env: APIKEY: ${{ secrets.APT_APIKEY }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 211ffe2c91..07020b133f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +# Changelog + ## 1.14.10 / 2022-11-09 * [BUGFIX] Correctly handle key removal on Windows (#2372, #2371) diff --git a/Makefile b/Makefile index ff656694e2..84033b57bb 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,7 @@ test-win: $(GOPASS_OUTPUT) $(GO) test -test.short -run '(Test|Example)' $(pkg) || exit 1;) test-integration: $(GOPASS_OUTPUT) - cd tests && GOPASS_BINARY=$(PWD)/$(GOPASS_OUTPUT) GOPASS_TEST_DIR=$(PWD)/tests $(GO) test -v + cd tests && GOPASS_BINARY=$(PWD)/$(GOPASS_OUTPUT) GOPASS_TEST_DIR=$(PWD)/tests $(GO) test -v $(TESTFLAGS) crosscompile: @echo -n ">> CROSSCOMPILE linux/amd64" diff --git a/docs/config.md b/docs/config.md index d5e9040eb3..7313766e77 100644 --- a/docs/config.md +++ b/docs/config.md @@ -26,9 +26,17 @@ Some configuration options are only available through setting environment variab | `GOPASS_GPG_BINARY` | `string` | Set this to the absolute path to the GPG binary if you need to override the value returned by `gpgconf`, e.g. [QubesOS](https://www.qubes-os.org/doc/split-gpg/). | | `GOPASS_PW_DEFAULT_LENGTH` | `int` | Set to any integer value larger than zero to define a different default length in the `generate` command. By default the length is 24 characters. | | `GOPASS_AUTOSYNC_INTERVAL` | `int` | Set this to the number of days between autosync runs. | -| `GOPASS_NO_AUTOSYNC` | `bool` | Set this to `true` to disable autosync. | +| `GOPASS_NO_AUTOSYNC` | `bool` | Set this to `true` to disable autosync. Deprecated. Please use `core.autosync` | +| `GOPASS_CONFIG_NOSYSTEM` | `bool` | Do not read `/etc/gopass/config` (if it exists) | +| `GOPASS_CONFIG_NO_MIGRATE` | `bool` | Do not attempt to migrate old gopass configs | +| `GOPASS_CPU_PROFILE` | `string` | Path to write a CPU Profile to. Use `go tool pprof` to visualize. | +| `GOPASS_FORCE_CHECK` | `string` | (internal) Force the updater to check for updates. Used for testing. | +| `GOPASS_MEM_PROFILE` | `string` | Path to write a Memory Profile to. Use `go tool pprof` to visualize.| +| `GOPASS_UNCLIP_CHECKSUM` | `string` | (internal) Used between gopass and it's unclip helper. | +| `GOPASS_UNCLIP_NAME` | `string` | (internal) Used between gopass and it's unclip helper. | +| `PWGEN_RULES_FILE` | `string` | (internal) Used for testing the pwgen rules generator. | -Variables not exclusively used by gopass +Variables not exclusively used by gopass: | **Option** | **Type** | **Description** | | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------ | @@ -42,31 +50,48 @@ Variables not exclusively used by gopass ## Configuration Options -During start up, gopass will look for a configuration file at `$HOME/.config/gopass/config.yml` on unix-like systems or at `%APPDATA%\gopass\config.yml` on Windows. If one is not present, it will create one. If the config file already exists, it will attempt to parse it and load the settings. If this fails, the program will abort. Thus, if gopass is giving you trouble with a broken or incompatible configuration file, simply rename it or delete it. +During start up, gopass will look for a configuration file at `$HOME/.config/gopass/config` on unix-like systems or at `%APPDATA%\gopass\config` on Windows. If one is not present, it will create one. If the config file already exists, it will attempt to parse it and load the settings. If this fails, the program will abort. Thus, if gopass is giving you trouble with a broken or incompatible configuration file, simply rename it or delete it. All configuration options are also available for reading and writing through the sub-command `gopass config`. * To display all values: `gopass config` * To display a single value: `gopass config autoclip` * To update a single value: `gopass config autoclip false` -* As many other sub-commands this command accepts a `--store` flag to operate on a given sub-store, provided the sub-store is a remote one. Support for different local configurations per mount was dropped in v1.9.3. +* As many other sub-commands this command accepts a `--store` flag to operate on a given sub-store, provided the sub-store is a remote one. + +### Configuration format + +`gopass` uses a configuration format inspired by and mostly compatible with the configuration format used by git. We support +different configuration sources that take precedence over each other, just like [git](https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-config.html). + +#### Configuration precendence + +* Hard-coded presets apply if nothing else if set +* System-wide configuration file allows operators or package maintainers to supply system-wide defaults in /etc/gopass/config +* User-wide (aka. global) configuration allows to set per-user settings. This is the closest equivalent to the old gopass configs. Located in `$HOME/.config/gopass/config` +* Per-store (aka. local) configuration allow to set per-store settings, e.g. read-only. Located in `/config`. +* Per-store unversioned (aka `config.worktree`) configuration allows to override versioned per-store settings, e.g. disabling read-only. Located in `/config.worktree` +* Environment variables (or command line flags) override all other values. Read from `GOPASS_CONFIG_KEY_n` and `GOPASS_CONFIG_VALUE_n` up to `GOPASS_CONFIG_COUNT`. Command line flags take precedence over environment variables. + +### Configuration options This is a list of available options: -| **Option** | **Type** | Description | -| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `askformore` | `bool` | If enabled - it will ask to add more data after use of `generate` command. DEPRECATED in v1.10.0 | -| `autoclip` | `bool` | Always copy the password created by `gopass generate`. Only applies to generate. | -| `autoimport` | `bool` | Import missing keys stored in the pass repository without asking. | -| `autosync` | `bool` | Always do a `git push` after a commit to the store. Makes sure your local changes are always available on your git remote. DEPRECATED in v1.10.0 | -| `concurrency` | `int` | Number of threads to use for batch operations (such as reencrypting). DEPRECATED in v1.9.3 | -| `cliptimeout` | `int` | How many seconds the secret is stored when using `-c`. | -| `exportkeys` | `bool` | Export public keys of all recipients to the store. | -| `recipient_hash` | `map` | Map of recipient ids to their hashes. DEPRECATED in v1.10.0 | -| `usesymbols` | `bool` | If enabled - it will use symbols when generating passwords. DEPRECATED in v1.9.3 | -| `nocolor` | `bool` | Do not use color. | -| `nopager` | `bool` | Do not invoke a pager to display long lists. | -| `notifications` | `bool` | Enable desktop notifications. | -| `parsing` | `bool` | Enable parsing of output to have key-value and yaml secrets. | -| `path` | `string` | Path to the root store. | -| `safecontent` | `bool` | Only output _safe content_ (i.e. everything but the first line of a secret) to the terminal. Use _copy_ (`-c`) to retrieve the password in the clipboard, or _force_ (`-f`) to still print it. | +| **Option** | **Type** | Description | *Default* | +| ---------------- | -------- | ----------- | --------- | +| `core.autoclip` | `bool` | Always copy the password created by `gopass generate`. Only applies to generate. | `false` | +| `core.autoimport` | `bool` | Import missing keys stored in the pass repository without asking. | `false` | +| `core.autosync` | `bool` | Always do a `git push` after a commit to the store. Makes sure your local changes are always available on your git remote. | `true` | +| `core.cliptimeout` | `int` | How many seconds the secret is stored when using `-c`. | `45` | +| `core.exportkeys` | `bool` | Export public keys of all recipients to the store. | `true` | +| `core.nocolor` | `bool` | Do not use color. | `false` | +| `core.nopager` | `bool` | Do not invoke a pager to display long lists. | `false` | +| `core.notifications` | `bool` | Enable desktop notifications. | `true` | +| `core.parsing` | `bool` | Enable parsing of output to have key-value and yaml secrets. | `true` | +| `core.readonly` | `bool` | Disable writing to a store. Note: This is just a convenience option to prevent accidential writes. Enforcement can only happen on a central server (if repos are set up around a central one). | `false` | +| `mounts.path` | `string` | Path to the root store. | `$XDG_DATA_HOME/gopass/stores/root` | +| `core.showsafecontent` | `bool` | Only output *safe content* (i.e. everything but the first line of a secret) to the terminal. Use *copy* (`-c`) to retrieve the password in the clipboard, or *force* (`-f`) to still print it. | `false` | +| `age.usekeychain` | `bool` | Use the OS keychain to cache age passphrases. | `false` | +| `domain-alias.` | `string` | Alias from domain to the string value of this entry. | `` | +| `core.showautoclip` | `bool` | Use autoclip for gopass show by default. | `false` | +| `autosync.interval` | `int` | AutoSync interval in days. | `3` | diff --git a/docs/setup.md b/docs/setup.md index 003022f806..2ae37e53ec 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -134,7 +134,7 @@ If this fails with an error: "Inappropriate ioctl for device" run the following If you are using CSH or TCSH: -``` +```bash setenv GPG_TTY `tty` ``` @@ -198,6 +198,8 @@ $ sudo apt update $ sudo apt install gopass gopass-archive-keyring ``` +Note: We also have an unstable track that sometimes contains pre-release versions. Use `https://packages.gopass.pw/repos/gopass-unstable` if you want to help with early testing. + #### Manual download First, find the latest .deb release from the repository [releases page](https://github.com/gopasspw/gopass/releases). Then, download and install it: diff --git a/fish.completion b/fish.completion index cf0e844e9c..74b342bf88 100644 --- a/fish.completion +++ b/fish.completion @@ -51,42 +51,6 @@ complete -c $PROG -f -n '__fish_gopass_uses_command age identities -l chars -d " complete -c $PROG -f -n '__fish_gopass_uses_command age identities -l help -d "show help"' complete -c $PROG -f -n '__fish_gopass_uses_command age identities -l version -d "print the version"' complete -c $PROG -f -n '__fish_gopass_needs_command' -a alias -d 'Command: Manage domain aliases' -complete -c $PROG -f -n '__fish_gopass_uses_command alias' -a add -d 'Subcommand: Add a new alias' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l yes -d "Always answer yes to yes/no questions"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l clip -d "Copy the password value into the clipboard"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l alsoclip -d "Copy the password and show everything"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l qr -d "Print the password as a QR Code"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l unsafe -d "Display unsafe content (e.g. the password) even if safecontent is enabled"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l password -d "Display only the password. Takes precedence over all other flags."' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l revision -d "Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -<N> to select the Nth oldest revision of this entry."' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l noparsing -d "Do not parse the output."' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l chars -d "Print specific characters from the secret"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l help -d "show help"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias add -l version -d "print the version"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias' -a remove -d 'Subcommand: Remove an alias from a domain' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l yes -d "Always answer yes to yes/no questions"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l clip -d "Copy the password value into the clipboard"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l alsoclip -d "Copy the password and show everything"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l qr -d "Print the password as a QR Code"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l unsafe -d "Display unsafe content (e.g. the password) even if safecontent is enabled"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l password -d "Display only the password. Takes precedence over all other flags."' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l revision -d "Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -<N> to select the Nth oldest revision of this entry."' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l noparsing -d "Do not parse the output."' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l chars -d "Print specific characters from the secret"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l help -d "show help"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias remove -l version -d "print the version"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias' -a delete -d 'Subcommand: Delete an entire domain' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l yes -d "Always answer yes to yes/no questions"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l clip -d "Copy the password value into the clipboard"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l alsoclip -d "Copy the password and show everything"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l qr -d "Print the password as a QR Code"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l unsafe -d "Display unsafe content (e.g. the password) even if safecontent is enabled"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l password -d "Display only the password. Takes precedence over all other flags."' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l revision -d "Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -<N> to select the Nth oldest revision of this entry."' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l noparsing -d "Do not parse the output."' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l chars -d "Print specific characters from the secret"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l help -d "show help"' -complete -c $PROG -f -n '__fish_gopass_uses_command alias delete -l version -d "print the version"' complete -c $PROG -f -n '__fish_gopass_needs_command' -a audit -d 'Command: Decrypt all secrets and scan for weak or leaked passwords' complete -c $PROG -f -n '__fish_gopass_needs_command' -a cat -d 'Command: Decode and print content of a binary secret to stdout, or encode and insert from stdin' complete -c $PROG -f -n '__fish_gopass_needs_command' -a clone -d 'Command: Clone a password store from a git repository' diff --git a/helpers/changelog/main.go b/helpers/changelog/main.go index e7855831a8..6ddbe29efb 100644 --- a/helpers/changelog/main.go +++ b/helpers/changelog/main.go @@ -5,6 +5,10 @@ // Changelog implements the changelog extractor that is called by the autorelease GitHub action // and used to extract the changelog from the CHANGELOG.md file. It's content is then used to // populate the release description on GitHub. +// +// This tool will extract every line between the first and the second subheading (##). +// This way the changelog can have a common header under the top most heading (#) and we +// still only get the content of the latest release in the GitHub release notes. package main import ( @@ -32,6 +36,10 @@ func main() { in = true } + if !in { + continue + } + fmt.Println(line) } } diff --git a/helpers/man/main.go b/helpers/man/main.go index d152dfa520..bb2fbbb240 100644 --- a/helpers/man/main.go +++ b/helpers/man/main.go @@ -45,6 +45,7 @@ func main() { if err := cmd.Run(); err != nil { os.Exit(cmd.ProcessState.ExitCode()) } + return } @@ -54,7 +55,7 @@ func main() { } version := semver.MustParse(strings.TrimSpace(string(vs))) - action, err := ap.New(&config.Config{}, version) + action, err := ap.New(config.New(), version) if err != nil { panic(err) } diff --git a/helpers/postrel/main.go b/helpers/postrel/main.go index a4a9ff2e9d..68e9e9439a 100644 --- a/helpers/postrel/main.go +++ b/helpers/postrel/main.go @@ -152,13 +152,22 @@ func (g *ghClient) createMilestones(ctx context.Context, v semver.Version) error return err } + // create a milestone for the next patch version if err := g.createMilestone(ctx, v.String(), 1, ms); err != nil { return err } + // create a milestone for the next+1 patch version v.IncrementPatch() + if err := g.createMilestone(ctx, v.String(), 2, ms); err != nil { + return err + } - return g.createMilestone(ctx, v.String(), 2, ms) + // create a milestone for the next minor version + v.IncrementMinor() + v.Patch = 0 + + return g.createMilestone(ctx, v.String(), 90, ms) } func (g *ghClient) createMilestone(ctx context.Context, title string, offset int, ms []*github.Milestone) error { @@ -607,9 +616,11 @@ func (u *repoUpdater) createPR(ctx context.Context, title, from, toOrg, toRepo s fmt.Printf("❌ Creating GitHub PR failed: %s", err) fmt.Printf("Request: %+v\n", newPR) fmt.Printf("Response: %+v\n", resp) + return err } fmt.Printf("✅ GitHub PR created: %s\n", pr.GetHTMLURL()) + return err } @@ -655,6 +666,7 @@ SCAN: if repl != nil { fmt.Fprintln(fout, *repl) } + continue SCAN } } @@ -680,6 +692,7 @@ func (r *repo) commitMsg() string { if r.msg != "" { return r.msg } + return "gopass: update to " + r.ver.String() + "\nNote: This is an auto-generated change as part of the gopass release process.\n" } @@ -706,6 +719,7 @@ func (r *repo) updatePrepare() error { if err := r.gitBranchDel(); err != nil { return fmt.Errorf("git branch -d failed: %w", err) } + return r.gitBranch() } @@ -726,6 +740,7 @@ func (r *repo) gitCoMaster() error { cmd.Stderr = os.Stderr cmd.Dir = r.dir fmt.Printf("Running command: %s\n", cmd) + return cmd.Run() } @@ -735,6 +750,7 @@ func (r *repo) gitBranch() error { cmd.Stderr = os.Stderr cmd.Dir = r.dir fmt.Printf("Running command: %s\n", cmd) + return cmd.Run() } @@ -744,6 +760,7 @@ func (r *repo) gitBranchDel() error { cmd.Stderr = os.Stderr cmd.Dir = r.dir fmt.Printf("Running command: %s\n", cmd) + return cmd.Run() } @@ -756,8 +773,10 @@ func (r *repo) gitPom() error { cmd.Dir = r.dir if err := cmd.Run(); err != nil { fmt.Println(buf.String()) + return err } + return nil } @@ -767,6 +786,7 @@ func (r *repo) gitPush(remote string) error { cmd.Stderr = os.Stderr cmd.Dir = r.dir fmt.Printf("Running command: %s\n", cmd) + return cmd.Run() } @@ -788,6 +808,7 @@ func (r *repo) gitCommit(files ...string) error { cmd.Stderr = os.Stderr cmd.Dir = r.dir fmt.Printf("Running command: %s\n", cmd) + return cmd.Run() } @@ -799,6 +820,7 @@ func (r *repo) isGitClean() bool { if err != nil { panic(err) } + return strings.TrimSpace(string(buf)) == "" } diff --git a/helpers/release/main.go b/helpers/release/main.go index 0841abe9d1..bdb915c856 100644 --- a/helpers/release/main.go +++ b/helpers/release/main.go @@ -10,8 +10,8 @@ package main import ( + "bufio" "fmt" - "io" "os" "os/exec" "regexp" @@ -68,13 +68,16 @@ func main() { fmt.Println() fmt.Println("🌟 Preparing a new gopass release.") fmt.Println("☝ Checking pre-conditions ...") + + prevVer, nextVer := getVersions() + // - check that workdir is clean if !isGitClean() { panic("❌ git is dirty") } fmt.Println("✅ git is clean") - if sv := os.Getenv("PATCH_RELEASE"); sv == "" { + if len(nextVer.Pre) < 1 { // - check out master if err := gitCoMaster(); err != nil { panic(err) @@ -92,8 +95,6 @@ func main() { } fmt.Println("✅ git is still clean") - prevVer, nextVer := getVersions() - fmt.Println() fmt.Printf("✅ New version will be: %s\n", nextVer.String()) fmt.Println() @@ -288,6 +289,7 @@ func gitCommit(v semver.Version) error { cmd = exec.Command("git", "commit", "-s", "-m", "Tag v"+v.String(), "-m", "RELEASE_NOTES=n/a") cmd.Stderr = os.Stderr + return cmd.Run() } @@ -305,22 +307,30 @@ func writeChangelog(prev, next semver.Version) error { } defer fh.Close() - fmt.Fprintf(fh, "## %s / %s\n\n", next.String(), time.Now().UTC().Format("2006-01-02")) - for _, e := range cl { - fmt.Fprint(fh, "* ") - fmt.Fprintln(fh, e) - } - fmt.Fprintln(fh) - ofh, err := os.Open("CHANGELOG.md") if err != nil { return err } defer ofh.Close() - // then appending any existing content from the old file and ... - if _, err := io.Copy(fh, ofh); err != nil { - return err + scanner := bufio.NewScanner(ofh) + + var written bool + for scanner.Scan() { + line := scanner.Text() + + // insert the new section before the last entry + if strings.HasPrefix(line, "## ") && !written { + fmt.Fprintf(fh, "## %s / %s\n\n", next.String(), time.Now().UTC().Format("2006-01-02")) + for _, e := range cl { + fmt.Fprint(fh, "* ") + fmt.Fprintln(fh, e) + } + fmt.Fprintln(fh) + } + + // all existing lines are just copied over + fmt.Fprintln(fh, line) } // renaming the new file to the old file @@ -330,12 +340,14 @@ func writeChangelog(prev, next semver.Version) error { func updateCompletion() error { cmd := exec.Command("make", "completion") cmd.Stderr = os.Stderr + return cmd.Run() } func updateManpage() error { cmd := exec.Command("make", "man") cmd.Stderr = os.Stderr + return cmd.Run() } @@ -359,6 +371,7 @@ func writeVersionGo(v semver.Version) error { return err } defer fh.Close() + return tmpl.Execute(fh, tplPayload{ Major: v.Major, Minor: v.Minor, @@ -371,6 +384,7 @@ func isGitClean() bool { if err != nil { panic(err) } + return strings.TrimSpace(string(buf)) == "" } @@ -379,6 +393,7 @@ func versionFile() (semver.Version, error) { if err != nil { return semver.Version{}, err } + return semver.Parse(strings.TrimSpace(string(buf))) } @@ -387,10 +402,12 @@ func gitVersion() (semver.Version, error) { if err != nil { return semver.Version{}, err } + lines := strings.Split(strings.TrimSpace(string(buf)), "\n") if len(lines) < 1 { return semver.Version{}, fmt.Errorf("no output") } + return semver.Parse(strings.TrimPrefix(lines[len(lines)-1], "v")) } @@ -454,6 +471,7 @@ func changelogEntries(since semver.Version) ([]string, error) { } sort.Strings(notes) + return notes, nil } diff --git a/internal/action/action_test.go b/internal/action/action_test.go index 712cb69505..0f319c41e6 100644 --- a/internal/action/action_test.go +++ b/internal/action/action_test.go @@ -20,8 +20,11 @@ import ( ) func newMock(ctx context.Context, path string) (*Action, error) { - cfg := config.Load() - cfg.Path = path + cfg := config.NewNoWrites() + if err := cfg.SetPath(path); err != nil { + return nil, err + } + ctx = cfg.WithConfig(ctx) if !backend.HasCryptoBackend(ctx) { ctx = backend.WithCryptoBackend(ctx, backend.Plain) @@ -54,6 +57,7 @@ func TestAction(t *testing.T) { act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) //nolint:ineffassign actName := "action.test" @@ -71,7 +75,7 @@ func TestNew(t *testing.T) { t.Parallel() td := t.TempDir() - cfg := config.New() + cfg := config.NewNoWrites() sv := semver.Version{} t.Run("init a new store", func(t *testing.T) { //nolint:paralleltest @@ -80,9 +84,9 @@ func TestNew(t *testing.T) { }) t.Run("init an existing plain store", func(t *testing.T) { //nolint:paralleltest - cfg.Path = filepath.Join(td, "store") - assert.NoError(t, os.MkdirAll(cfg.Path, 0o700)) - assert.NoError(t, os.WriteFile(filepath.Join(cfg.Path, plain.IDFile), []byte("foobar"), 0o600)) + require.NoError(t, cfg.SetPath(filepath.Join(td, "store"))) + assert.NoError(t, os.MkdirAll(cfg.Path(), 0o700)) + assert.NoError(t, os.WriteFile(filepath.Join(cfg.Path(), plain.IDFile), []byte("foobar"), 0o600)) _, err := New(cfg, sv) assert.NoError(t, err) }) diff --git a/internal/action/aliases.go b/internal/action/aliases.go index 32e721c49b..c2ae07561b 100644 --- a/internal/action/aliases.go +++ b/internal/action/aliases.go @@ -4,9 +4,7 @@ import ( "sort" "strings" - "github.com/gopasspw/gopass/internal/action/exit" "github.com/gopasspw/gopass/internal/out" - "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/gopasspw/gopass/pkg/pwgen/pwrules" "github.com/urfave/cli/v2" ) @@ -14,7 +12,7 @@ import ( // AliasesPrint prints all cofigured aliases. func (s *Action) AliasesPrint(c *cli.Context) error { out.Printf(c.Context, "Configured aliases:") - aliases := pwrules.AllAliases() + aliases := pwrules.AllAliases(c.Context) keys := make([]string, 0, len(aliases)) for k := range aliases { keys = append(keys, k) @@ -27,59 +25,3 @@ func (s *Action) AliasesPrint(c *cli.Context) error { return nil } - -// AliasesAdd adds a single alias to a domain. -func (s *Action) AliasesAdd(c *cli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) - domain := c.Args().First() - alias := c.Args().Get(1) - - if domain == "" || alias == "" { - return exit.Error(exit.Usage, nil, "Usage: %s alias add ", s.Name) - } - - if err := pwrules.AddCustomAlias(domain, alias); err != nil { - return err - } - - out.Printf(ctx, "Added alias %q to domain %q", alias, domain) - - return nil -} - -// AliasesRemove removes a single alias from a domain. -func (s *Action) AliasesRemove(c *cli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) - domain := c.Args().First() - alias := c.Args().Get(1) - - if domain == "" || alias == "" { - return exit.Error(exit.Usage, nil, "Usage: %s alias remove ", s.Name) - } - - if err := pwrules.RemoveCustomAlias(domain, alias); err != nil { - return err - } - - out.Printf(ctx, "Remove alias %q from domain %q", alias, domain) - - return nil -} - -// AliasesDelete remove an alias mapping for a domain. -func (s *Action) AliasesDelete(c *cli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) - domain := c.Args().First() - - if domain == "" { - return exit.Error(exit.Usage, nil, "Usage: %s alias delete ", s.Name) - } - - if err := pwrules.DeleteCustomAlias(domain); err != nil { - return err - } - - out.Printf(ctx, "Remove aliases for domain %q", domain) - - return nil -} diff --git a/internal/action/audit_test.go b/internal/action/audit_test.go index 6e32bb9ecd..f607022bf1 100644 --- a/internal/action/audit_test.go +++ b/internal/action/audit_test.go @@ -24,6 +24,7 @@ func TestAudit(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/binary_test.go b/internal/action/binary_test.go index c4eb17af44..b4118ec460 100644 --- a/internal/action/binary_test.go +++ b/internal/action/binary_test.go @@ -32,6 +32,7 @@ func TestBinary(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) assert.Error(t, act.Cat(gptest.CliCtx(ctx, t))) assert.Error(t, act.BinaryCopy(gptest.CliCtx(ctx, t))) @@ -58,6 +59,7 @@ func TestBinaryCat(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) infile := filepath.Join(u.Dir, "input.txt") writeBinfile(t, infile) @@ -111,6 +113,7 @@ func TestBinaryCopy(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) t.Run("copy textfile", func(t *testing.T) { //nolint:paralleltest defer buf.Reset() @@ -173,6 +176,7 @@ func TestBinarySum(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) infile := filepath.Join(u.Dir, "input.raw") @@ -198,6 +202,7 @@ func TestBinaryGet(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) data := []byte("1\n2\n3\n") assert.NoError(t, act.insertStdin(ctx, "x", data, false)) diff --git a/internal/action/clone.go b/internal/action/clone.go index 109703d1a1..d2ddd8f883 100644 --- a/internal/action/clone.go +++ b/internal/action/clone.go @@ -149,11 +149,6 @@ func (s *Action) clone(ctx context.Context, repo, mount, path string) error { return err } - // save new mount in config file. - if err := s.cfg.Save(); err != nil { - return exit.Error(exit.IO, err, "Failed to update config: %s", err) - } - // try to init repo config. out.Noticef(ctx, "Configuring %s repository ...", sb) diff --git a/internal/action/clone_test.go b/internal/action/clone_test.go index ab417d0bd2..06cd88fcd0 100644 --- a/internal/action/clone_test.go +++ b/internal/action/clone_test.go @@ -51,6 +51,7 @@ func TestClone(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf @@ -98,12 +99,13 @@ func TestCloneBackendIsStoredForMount(t *testing.T) { //nolint:paralleltest ctx = ctxutil.WithAlwaysYes(ctx, true) ctx = ctxutil.WithInteractive(ctx, false) - cfg := config.Load() - cfg.Path = u.StoreDir("") + cfg := config.NewNoWrites() + require.NoError(t, cfg.SetPath(u.StoreDir(""))) act, err := newAction(cfg, semver.Version{}, false) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) c := gptest.CliCtx(ctx, t) require.NoError(t, act.IsInitialized(c)) @@ -113,7 +115,7 @@ func TestCloneBackendIsStoredForMount(t *testing.T) { //nolint:paralleltest c = gptest.CliCtxWithFlags(ctx, t, map[string]string{"check-keys": "false"}, repo, "the-project") assert.NoError(t, act.Clone(c)) - require.NotNil(t, act.cfg.Mounts["the-project"]) + require.Contains(t, act.cfg.Mounts(), "the-project") } func TestCloneGetGitConfig(t *testing.T) { //nolint:paralleltest @@ -132,6 +134,7 @@ func TestCloneGetGitConfig(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) name, email, err := act.cloneGetGitConfig(ctx, "foobar") assert.NoError(t, err) diff --git a/internal/action/commands.go b/internal/action/commands.go index 7d2b4ed2ac..b81bbc5bd3 100644 --- a/internal/action/commands.go +++ b/internal/action/commands.go @@ -68,29 +68,6 @@ func (s *Action) GetCommands() []*cli.Command { Description: "Manages domain aliases. Note: this command might change or go away.", Action: s.AliasesPrint, Hidden: true, - Subcommands: []*cli.Command{ - { - Name: "add", - Action: s.AliasesAdd, - Usage: "Add a new alias", - ArgsUsage: "[alias] [domain]", - Description: "Adds a new alias", - }, - { - Name: "remove", - Action: s.AliasesRemove, - Usage: "Remove an alias from a domain", - ArgsUsage: "[alias] [domain]", - Description: "Remove an alias from a domain", - }, - { - Name: "delete", - Action: s.AliasesDelete, - Usage: "Delete an entire domain", - ArgsUsage: "[alias]", - Description: "Delete an entire domain", - }, - }, }, { Name: "audit", @@ -981,7 +958,7 @@ func (s *Action) GetCommands() []*cli.Command { for _, be := range backend.CryptoRegistry.Backends() { bc, ok := be.(commander) if !ok { - debug.Log("Backend %s does not implement commander interface\n", be) + // Backend does not implement commander interface continue } @@ -993,7 +970,7 @@ func (s *Action) GetCommands() []*cli.Command { for _, be := range backend.StorageRegistry.Backends() { bc, ok := be.(storeCommander) if !ok { - debug.Log("Backend %s does not implement commander interface\n", be) + // Backend does not implement commander interface continue } diff --git a/internal/action/commands_test.go b/internal/action/commands_test.go index 8d17daf305..e2b141e0fb 100644 --- a/internal/action/commands_test.go +++ b/internal/action/commands_test.go @@ -48,6 +48,7 @@ func TestCommands(t *testing.T) { act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) //nolint:ineffassign for _, cmd := range act.GetCommands() { cmd := cmd diff --git a/internal/action/completion_test.go b/internal/action/completion_test.go index 22d49774a5..63bcefdf2f 100644 --- a/internal/action/completion_test.go +++ b/internal/action/completion_test.go @@ -62,6 +62,7 @@ func TestComplete(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) app := cli.NewApp() app.Commands = []*cli.Command{ diff --git a/internal/action/config.go b/internal/action/config.go index f95fa02e1f..4a96ce017e 100644 --- a/internal/action/config.go +++ b/internal/action/config.go @@ -3,25 +3,27 @@ package action import ( "context" "fmt" - "sort" "github.com/gopasspw/gopass/internal/action/exit" "github.com/gopasspw/gopass/internal/out" + "github.com/gopasspw/gopass/internal/set" "github.com/gopasspw/gopass/pkg/ctxutil" + "github.com/gopasspw/gopass/pkg/debug" "github.com/urfave/cli/v2" ) // Config handles changes to the gopass configuration. func (s *Action) Config(c *cli.Context) error { ctx := ctxutil.WithGlobalFlags(c) + store := c.String("store") if c.Args().Len() < 1 { - s.printConfigValues(ctx) + s.printConfigValues(ctx, store) return nil } if c.Args().Len() == 1 { - s.printConfigValues(ctx, c.Args().Get(0)) + s.printConfigValues(ctx, store, c.Args().Get(0)) return nil } @@ -30,45 +32,27 @@ func (s *Action) Config(c *cli.Context) error { return exit.Error(exit.Usage, nil, "Usage: %s config key value", s.Name) } - if err := s.setConfigValue(ctx, c.Args().Get(0), c.Args().Get(1)); err != nil { + if err := s.setConfigValue(ctx, store, c.Args().Get(0), c.Args().Get(1)); err != nil { return exit.Error(exit.Unknown, err, "Error setting config value") } return nil } -func (s *Action) printConfigValues(ctx context.Context, needles ...string) { - m := s.cfg.ConfigMap() - for _, k := range filterMap(m, needles) { +func (s *Action) printConfigValues(ctx context.Context, store string, needles ...string) { + for _, k := range set.SortedFiltered(s.cfg.Keys(store), func(e string) bool { + return contains(needles, e) + }) { + v := s.cfg.GetM(store, k) // if only a single key is requested, print only the value // useful for scriping, e.g. `$ cd $(gopass config path)`. if len(needles) == 1 { - out.Printf(ctx, "%s", m[k]) + out.Printf(ctx, "%s", v) continue } - out.Printf(ctx, "%s: %s", k, m[k]) + out.Printf(ctx, "%s = %s", k, v) } - - for alias, path := range s.cfg.Mounts { - if len(needles) < 1 { - out.Printf(ctx, "mount %q => %q", alias, path) - } - } -} - -func filterMap(haystack map[string]string, needles []string) []string { - out := make([]string, 0, len(haystack)) - for k := range haystack { - if !contains(needles, k) { - continue - } - out = append(out, k) - } - - sort.Strings(out) - - return out } func contains(haystack []string, needle string) bool { @@ -85,27 +69,20 @@ func contains(haystack []string, needle string) bool { return false } -func (s *Action) setConfigValue(ctx context.Context, key, value string) error { - if err := s.cfg.SetConfigValue(key, value); err != nil { +func (s *Action) setConfigValue(ctx context.Context, store, key, value string) error { + debug.Log("setting %s to %s for %q", key, value, store) + + if err := s.cfg.Set(store, key, value); err != nil { return fmt.Errorf("failed to set config value %q: %w", key, err) } - s.printConfigValues(ctx, key) + s.printConfigValues(ctx, store, key) return nil } func (s *Action) configKeys() []string { - cm := s.cfg.ConfigMap() - keys := make([]string, 0, len(cm)+1) - for k := range cm { - keys = append(keys, k) - } - - keys = append(keys, "remote") - sort.Strings(keys) - - return keys + return s.cfg.Keys("") } // ConfigComplete will print the list of valid config keys. diff --git a/internal/action/config_test.go b/internal/action/config_test.go index 970ba23224..e47529a4e6 100644 --- a/internal/action/config_test.go +++ b/internal/action/config_test.go @@ -23,6 +23,7 @@ func TestConfig(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf @@ -37,38 +38,38 @@ func TestConfig(t *testing.T) { //nolint:paralleltest c := gptest.CliCtx(ctx, t) assert.NoError(t, act.Config(c)) - want := `autoclip: true -autoimport: true -cliptimeout: 45 -exportkeys: true -keychain: false -nopager: false -notifications: true -parsing: true -` - want += "path: " + u.StoreDir("") + "\n" - want += `safecontent: false + want := `core.autoclip = true +core.autoimport = true +core.autosync = true +core.cliptimeout = 45 +core.exportkeys = true +core.nopager = true +core.notifications = true +core.parsing = true ` + want += "mounts.path = " + u.StoreDir("") + "\n" assert.Equal(t, want, buf.String()) }) t.Run("set valid config value", func(t *testing.T) { //nolint:paralleltest defer buf.Reset() - assert.NoError(t, act.setConfigValue(ctx, "nopager", "true")) + assert.NoError(t, act.setConfigValue(ctx, "", "core.nopager", "true")) + + // should print accepted config value assert.Equal(t, "true", strings.TrimSpace(buf.String()), "action.setConfigValue") }) t.Run("set invalid config value", func(t *testing.T) { //nolint:paralleltest defer buf.Reset() - assert.Error(t, act.setConfigValue(ctx, "foobar", "true")) + assert.Error(t, act.setConfigValue(ctx, "", "foobar", "true")) }) t.Run("print single config value", func(t *testing.T) { //nolint:paralleltest defer buf.Reset() - act.printConfigValues(ctx, "nopager") + act.printConfigValues(ctx, "", "core.nopager") want := "true" assert.Equal(t, want, strings.TrimSpace(buf.String()), "action.printConfigValues") @@ -77,27 +78,24 @@ parsing: true t.Run("print all config values", func(t *testing.T) { //nolint:paralleltest defer buf.Reset() - act.printConfigValues(ctx) - want := `autoclip: true -autoimport: true -cliptimeout: 45 -exportkeys: true -keychain: false -nopager: true -notifications: true -parsing: true + act.printConfigValues(ctx, "") + want := `core.autoclip = true +core.autoimport = true +core.autosync = true +core.cliptimeout = 45 +core.exportkeys = true +core.nopager = true +core.notifications = true +core.parsing = true ` - want += "path: " + u.StoreDir("") + "\n" - want += `safecontent: false` + want += "mounts.path = " + u.StoreDir("") assert.Equal(t, want, strings.TrimSpace(buf.String()), "action.printConfigValues") - - delete(act.cfg.Mounts, "foo") }) t.Run("show autoimport value", func(t *testing.T) { //nolint:paralleltest defer buf.Reset() - c := gptest.CliCtx(ctx, t, "autoimport") + c := gptest.CliCtx(ctx, t, "core.autoimport") assert.NoError(t, act.Config(c)) assert.Equal(t, "true", strings.TrimSpace(buf.String())) }) @@ -105,7 +103,7 @@ parsing: true t.Run("disable autoimport", func(t *testing.T) { //nolint:paralleltest defer buf.Reset() - c := gptest.CliCtx(ctx, t, "autoimport", "false") + c := gptest.CliCtx(ctx, t, "core.autoimport", "false") assert.NoError(t, act.Config(c)) assert.Equal(t, "false", strings.TrimSpace(buf.String())) }) @@ -114,17 +112,15 @@ parsing: true defer buf.Reset() act.ConfigComplete(gptest.CliCtx(ctx, t)) - want := `autoclip -autoimport -cliptimeout -exportkeys -keychain -nopager -notifications -parsing -path -remote -safecontent + want := `core.autoclip +core.autoimport +core.autosync +core.cliptimeout +core.exportkeys +core.nopager +core.notifications +core.parsing +mounts.path ` assert.Equal(t, want, buf.String()) }) diff --git a/internal/action/copy_test.go b/internal/action/copy_test.go index 5668f80fbc..300833bb87 100644 --- a/internal/action/copy_test.go +++ b/internal/action/copy_test.go @@ -26,8 +26,9 @@ func TestCopy(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) - act.cfg.AutoClip = false + require.NoError(t, act.cfg.Set("", "core.autoclip", "false")) buf := &bytes.Buffer{} out.Stdout = buf @@ -109,8 +110,9 @@ func TestCopyGpg(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) - act.cfg.AutoClip = false + require.NoError(t, act.cfg.Set("", "core.autoclip", "false")) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/create.go b/internal/action/create.go index a2ed0f2323..0f8e2a0114 100644 --- a/internal/action/create.go +++ b/internal/action/create.go @@ -49,7 +49,7 @@ func (s *Action) createPrintOrCopy(ctx context.Context, c *cli.Context, name, pa return nil } - if err := clipboard.CopyTo(ctx, name, []byte(password), s.cfg.ClipTimeout); err != nil { + if err := clipboard.CopyTo(ctx, name, []byte(password), s.cfg.GetInt("core.cliptimeout")); err != nil { return exit.Error(exit.IO, err, "failed to copy to clipboard: %s", err) } diff --git a/internal/action/create_test.go b/internal/action/create_test.go index 506acc4444..ab4519b710 100644 --- a/internal/action/create_test.go +++ b/internal/action/create_test.go @@ -22,13 +22,14 @@ func TestCreate(t *testing.T) { //nolint:paralleltest ctx := context.Background() ctx = ctxutil.WithAlwaysYes(ctx, true) - ctx = ctxutil.WithNotifications(ctx, false) act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) - act.cfg.ClipTimeout = 1 + require.NoError(t, act.cfg.Set("", "core.notifications", "false")) + require.NoError(t, act.cfg.Set("", "core.cliptimeout", "1")) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/delete_test.go b/internal/action/delete_test.go index db6dcd4f1d..6d6041e350 100644 --- a/internal/action/delete_test.go +++ b/internal/action/delete_test.go @@ -24,6 +24,7 @@ func TestDelete(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/edit_test.go b/internal/action/edit_test.go index 289054cf9b..d0c838f4ee 100644 --- a/internal/action/edit_test.go +++ b/internal/action/edit_test.go @@ -23,6 +23,7 @@ func TestEdit(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf @@ -53,6 +54,7 @@ func TestEditUpdate(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/env_test.go b/internal/action/env_test.go index 62e19eb660..7c781a9d5a 100644 --- a/internal/action/env_test.go +++ b/internal/action/env_test.go @@ -26,6 +26,7 @@ func TestEnvLeafHappyPath(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf @@ -58,6 +59,7 @@ func TestEnvLeafHappyPathKeepCase(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf @@ -93,6 +95,7 @@ func TestEnvSecretNotFound(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) // Command-line would be: "gopass env non-existing true". assert.EqualError(t, act.Env(gptest.CliCtx(ctx, t, "non-existing", "true")), @@ -109,6 +112,7 @@ func TestEnvProgramNotFound(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) wanted := "exec: \"non-existing\": executable file not found in " if runtime.GOOS == "windows" { @@ -133,6 +137,7 @@ func TestEnvProgramNotSpecified(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) // Command-line would be: "gopass env foo". assert.EqualError(t, act.Env(gptest.CliCtx(ctx, t, "foo")), diff --git a/internal/action/find_test.go b/internal/action/find_test.go index 7a22ec456a..3cce995883 100644 --- a/internal/action/find_test.go +++ b/internal/action/find_test.go @@ -29,8 +29,9 @@ func TestFind(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) - act.cfg.AutoClip = false + require.NoError(t, act.cfg.Set("", "core.autoclip", "false")) buf := &bytes.Buffer{} out.Stdout = buf @@ -65,7 +66,7 @@ func TestFind(t *testing.T) { //nolint:paralleltest buf.Reset() // testing the safecontent case - ctx = ctxutil.WithShowSafeContent(ctx, true) + require.NoError(t, act.cfg.Set("", "core.showsafecontent", "true")) c.Context = ctx assert.NoError(t, act.Find(c)) buf.Reset() @@ -85,7 +86,7 @@ func TestFind(t *testing.T) { //nolint:paralleltest buf.Reset() // stopping with the safecontent tests - ctx = ctxutil.WithShowSafeContent(ctx, false) + require.NoError(t, act.cfg.Set("", "core.showsafecontent", "false")) // find yo c = gptest.CliCtx(ctx, t, "yo") diff --git a/internal/action/fsck.go b/internal/action/fsck.go index 99ff09246b..1acd018c4e 100644 --- a/internal/action/fsck.go +++ b/internal/action/fsck.go @@ -5,10 +5,11 @@ import ( "path/filepath" "github.com/gopasspw/gopass/internal/action/exit" - "github.com/gopasspw/gopass/internal/config" + "github.com/gopasspw/gopass/internal/config/legacy" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/internal/store/leaf" "github.com/gopasspw/gopass/internal/tree" + "github.com/gopasspw/gopass/pkg/appdir" "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/gopasspw/gopass/pkg/fsutil" "github.com/gopasspw/gopass/pkg/termio" @@ -27,17 +28,13 @@ func (s *Action) Fsck(c *cli.Context) error { } out.Printf(ctx, "Checking password store integrity ...") - // make sure config is in the right place. - // we may have loaded it from one of the fallback locations. - if err := s.cfg.Save(); err != nil { - return exit.Error(exit.Config, err, "failed to save config: %s", err) - } // clean up any previous config locations. - oldCfg := filepath.Join(config.Homedir(), ".gopass.yml") - if fsutil.IsFile(oldCfg) { - if err := os.Remove(oldCfg); err != nil { - out.Errorf(ctx, "Failed to remove old gopass config %s: %s", oldCfg, err) + for _, oldCfg := range append(legacy.ConfigLocations(), filepath.Join(appdir.UserHome(), ".gopass.yml")) { + if fsutil.IsFile(oldCfg) { + if err := os.Remove(oldCfg); err != nil { + out.Errorf(ctx, "Failed to remove old gopass config %s: %s", oldCfg, err) + } } } diff --git a/internal/action/fsck_test.go b/internal/action/fsck_test.go index 7f11b9fc1e..97012bc656 100644 --- a/internal/action/fsck_test.go +++ b/internal/action/fsck_test.go @@ -26,6 +26,7 @@ func TestFsck(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf @@ -87,6 +88,7 @@ func TestFsckGpg(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/generate.go b/internal/action/generate.go index 032b2d19b5..29db3f3521 100644 --- a/internal/action/generate.go +++ b/internal/action/generate.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/gopasspw/gopass/internal/action/exit" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/internal/tree" "github.com/gopasspw/gopass/pkg/clipboard" @@ -138,13 +139,13 @@ func (s *Action) generateCopyOrPrint(ctx context.Context, c *cli.Context, name, // copy to clipboard if: // - explicitly requested with -c // - autoclip=true, but only if output is not being redirected. - if IsClip(ctx) || (s.cfg.AutoClip && ctxutil.IsTerminal(ctx)) { - if err := clipboard.CopyTo(ctx, name, []byte(password), s.cfg.ClipTimeout); err != nil { + if IsClip(ctx) || (s.cfg.GetBool("core.autoclip") && ctxutil.IsTerminal(ctx)) { + if err := clipboard.CopyTo(ctx, name, []byte(password), s.cfg.GetInt("core.cliptimeout")); err != nil { return exit.Error(exit.IO, err, "failed to copy to clipboard: %s", err) } // if autoclip is on and we're not printing the password to the terminal // at least leave a notice that we did indeed copy it. - if s.cfg.AutoClip && !c.Bool("print") { + if s.cfg.GetBool("core.autoclip") && !c.Bool("print") { out.Print(ctx, "Copied to clipboard") return nil @@ -157,8 +158,8 @@ func (s *Action) generateCopyOrPrint(ctx context.Context, c *cli.Context, name, return nil } - if c.IsSet("print") && !c.Bool("print") && ctxutil.IsShowSafeContent(ctx) { - debug.Log("safecontent suppresing printing") + if c.IsSet("print") && !c.Bool("print") && config.Bool(ctx, "core.showsafecontent") { + debug.Log("safecontent suppressing printing") return nil } @@ -172,10 +173,10 @@ func (s *Action) generateCopyOrPrint(ctx context.Context, c *cli.Context, name, return nil } -func hasPwRuleForSecret(name string) (string, pwrules.Rule) { +func hasPwRuleForSecret(ctx context.Context, name string) (string, pwrules.Rule) { for name != "" && name != "." { d := path.Base(name) - if r, found := pwrules.LookupRule(d); found { + if r, found := pwrules.LookupRule(ctx, d); found { return d, r } name = path.Dir(name) @@ -186,7 +187,7 @@ func hasPwRuleForSecret(name string) (string, pwrules.Rule) { // generatePassword will run through the password generation steps. func (s *Action) generatePassword(ctx context.Context, c *cli.Context, length, name string) (string, error) { - if domain, rule := hasPwRuleForSecret(name); domain != "" && !c.Bool("force") { + if domain, rule := hasPwRuleForSecret(ctx, name); domain != "" && !c.Bool("force") { return s.generatePasswordForRule(ctx, c, length, name, domain, rule) } @@ -283,7 +284,7 @@ func (s *Action) generatePasswordForRule(ctx context.Context, c *cli.Context, le iv = clamp(rule.Minlen, rule.Maxlen, iv) - pw := pwgen.NewCrypticForDomain(iv, domain).Password() + pw := pwgen.NewCrypticForDomain(ctx, iv, domain).Password() if pw == "" { return "", fmt.Errorf("failed to generate password for %s", domain) } @@ -355,7 +356,7 @@ func (s *Action) generateSetPassword(ctx context.Context, name, key, password st var sec gopass.Secret sec = secrets.New() sec.SetPassword(password) - if u := hasChangeURL(name); u != "" { + if u := hasChangeURL(ctx, name); u != "" { _ = sec.Set("password-change-url", u) } @@ -375,10 +376,10 @@ func (s *Action) generateSetPassword(ctx context.Context, name, key, password st return ctx, nil } -func hasChangeURL(name string) string { +func hasChangeURL(ctx context.Context, name string) string { p := strings.Split(name, "/") for i := len(p) - 1; i > 0; i-- { - if u := pwrules.LookupChangeURL(p[i]); u != "" { + if u := pwrules.LookupChangeURL(ctx, p[i]); u != "" { return u } } diff --git a/internal/action/generate_test.go b/internal/action/generate_test.go index 1e566a6278..d51e1068ee 100644 --- a/internal/action/generate_test.go +++ b/internal/action/generate_test.go @@ -23,7 +23,7 @@ import ( func TestRuleLookup(t *testing.T) { t.Parallel() - domain, _ := hasPwRuleForSecret("foo/gopass.pw") + domain, _ := hasPwRuleForSecret(context.Background(), "foo/gopass.pw") assert.Equal(t, "", domain) } @@ -38,8 +38,9 @@ func TestGenerate(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) - act.cfg.AutoClip = false + require.NoError(t, act.cfg.Set("", "core.autoclip", "false")) buf := &bytes.Buffer{} out.Stdout = buf @@ -155,11 +156,11 @@ func TestGenerate(t *testing.T) { //nolint:paralleltest // generate --force foobar 24 w/ autoclip and output redirection t.Run("generate --force foobar 24", func(t *testing.T) { //nolint:paralleltest - ov := act.cfg.AutoClip + ov := act.cfg.Get("core.autoclip") defer func() { - act.cfg.AutoClip = ov + require.NoError(t, act.cfg.Set("", "core.autoclip", ov)) }() - act.cfg.AutoClip = true + require.NoError(t, act.cfg.Set("", "core.autoclip", "true")) ctx := ctxutil.WithTerminal(ctx, false) assert.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true"}, "foobar", "24"))) assert.Contains(t, buf.String(), "Not printing secrets by default") @@ -168,11 +169,11 @@ func TestGenerate(t *testing.T) { //nolint:paralleltest // generate --force foobar 24 w/ autoclip and no output redirection t.Run("generate --force foobar 24", func(t *testing.T) { //nolint:paralleltest - ov := act.cfg.AutoClip + ov := act.cfg.Get("core.autoclip") defer func() { - act.cfg.AutoClip = ov + require.NoError(t, act.cfg.Set("", "core.autoclip", ov)) }() - act.cfg.AutoClip = true + require.NoError(t, act.cfg.Set("", "core.autoclip", "true")) ctx := ctxutil.WithTerminal(ctx, true) assert.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true"}, "foobar", "24"))) assert.Contains(t, buf.String(), "Copied to clipboard") diff --git a/internal/action/grep_test.go b/internal/action/grep_test.go index 2adec410cd..ca8d67a44d 100644 --- a/internal/action/grep_test.go +++ b/internal/action/grep_test.go @@ -24,6 +24,7 @@ func TestGrep(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/history_test.go b/internal/action/history_test.go index 6bb8412151..24d174d481 100644 --- a/internal/action/history_test.go +++ b/internal/action/history_test.go @@ -33,11 +33,13 @@ func TestHistory(t *testing.T) { //nolint:paralleltest ctx = backend.WithCryptoBackend(ctx, backend.Plain) ctx = backend.WithStorageBackend(ctx, backend.GitFS) - cfg := config.New() - cfg.Path = u.StoreDir("") + cfg := config.NewNoWrites() + require.NoError(t, cfg.SetPath(u.StoreDir(""))) + act, err := newAction(cfg, semver.Version{}, false) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) t.Run("can initialize", func(t *testing.T) { //nolint:paralleltest require.NoError(t, act.IsInitialized(gptest.CliCtx(ctx, t))) diff --git a/internal/action/init.go b/internal/action/init.go index 1720e1d909..70c01a0879 100644 --- a/internal/action/init.go +++ b/internal/action/init.go @@ -165,12 +165,6 @@ func (s *Action) init(ctx context.Context, alias, path string, keys ...string) e debug.Log("not initializing RCS backend ...") } - // write config. - debug.Log("Writing configuration to %q", s.cfg.ConfigPath) - if err := s.cfg.Save(); err != nil { - return exit.Error(exit.Config, err, "failed to write config: %s", err) - } - out.Printf(ctx, "🏁 Password store %s initialized for:", path) s.printRecipients(ctx, alias) diff --git a/internal/action/init_test.go b/internal/action/init_test.go index eaab677603..1a001ba36f 100644 --- a/internal/action/init_test.go +++ b/internal/action/init_test.go @@ -30,6 +30,7 @@ func TestInit(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/insert_test.go b/internal/action/insert_test.go index 24379b01d3..81a6bb6160 100644 --- a/internal/action/insert_test.go +++ b/internal/action/insert_test.go @@ -26,8 +26,9 @@ func TestInsert(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) - act.cfg.AutoClip = false + require.NoError(t, act.cfg.Set("", "core.autoclip", "true")) buf := &bytes.Buffer{} out.Stdout = buf @@ -88,7 +89,7 @@ func TestInsert(t *testing.T) { //nolint:paralleltest t.Run("insert zab#key", func(t *testing.T) { //nolint:paralleltest ctx = ctxutil.WithInteractive(ctx, false) - ctx = ctxutil.WithShowSafeContent(ctx, true) + require.NoError(t, act.cfg.Set("", "core.showsafecontent", "true")) assert.NoError(t, act.insertYAML(ctx, "zab", "key", []byte("foobar"), nil)) assert.NoError(t, act.show(ctx, gptest.CliCtx(ctx, t), "zab", false)) assert.Contains(t, buf.String(), "key: foobar") @@ -118,7 +119,7 @@ func TestInsert(t *testing.T) { //nolint:paralleltest t.Run("insert baz via stdin w/ yaml and no input parsing", func(t *testing.T) { //nolint:paralleltest ctx = ctxutil.WithShowParsing(ctx, false) - ctx = ctxutil.WithShowSafeContent(ctx, false) + require.NoError(t, act.cfg.Set("", "core.showsafecontent", "false")) assert.NoError(t, act.insertStdin(ctx, "baz", []byte("foobar\n---\nuser: name\nother: 0123"), false)) buf.Reset() @@ -142,8 +143,9 @@ func TestInsertStdin(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) - act.cfg.AutoClip = false + require.NoError(t, act.cfg.Set("", "core.autoclip", "false")) buf := &bytes.Buffer{} ibuf := &bytes.Buffer{} diff --git a/internal/action/list.go b/internal/action/list.go index d8fdc1e10c..bf3a425efc 100644 --- a/internal/action/list.go +++ b/internal/action/list.go @@ -11,6 +11,7 @@ import ( "github.com/fatih/color" "github.com/gopasspw/gopass/internal/action/exit" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/store/leaf" "github.com/gopasspw/gopass/internal/tree" "github.com/gopasspw/gopass/pkg/ctxutil" @@ -106,7 +107,7 @@ func (s *Action) listFiltered(ctx context.Context, l *tree.Root, limit int, flat // redirectPager returns a redirected io.Writer if the output would exceed // the terminal size. func redirectPager(ctx context.Context, subtree *tree.Root) (io.Writer, *bytes.Buffer) { - if ctxutil.IsNoPager(ctx) { + if config.Bool(ctx, "core.nopager") { return stdout, nil } _, rows, err := term.GetSize(0) diff --git a/internal/action/list_test.go b/internal/action/list_test.go index 4d96d73f9d..42278f48ab 100644 --- a/internal/action/list_test.go +++ b/internal/action/list_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/fatih/color" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/internal/tree" "github.com/gopasspw/gopass/pkg/ctxutil" @@ -27,6 +28,7 @@ func TestList(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf @@ -126,6 +128,7 @@ func TestListLimit(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf @@ -218,14 +221,17 @@ func TestRedirectPager(t *testing.T) { //nolint:paralleltest var buf *bytes.Buffer var subtree *tree.Root + cfg := config.NewNoWrites() + ctx = cfg.WithConfig(ctx) + // no pager - ctx = ctxutil.WithNoPager(ctx, true) + require.NoError(t, cfg.Set("", "core.nopager", "true")) so, buf := redirectPager(ctx, subtree) assert.Nil(t, buf) assert.NotNil(t, so) // no term - ctx = ctxutil.WithNoPager(ctx, false) + require.NoError(t, cfg.Set("", "core.nopager", "false")) so, buf = redirectPager(ctx, subtree) assert.Nil(t, buf) assert.NotNil(t, so) diff --git a/internal/action/mount.go b/internal/action/mount.go index 65686c533e..7e27bf57ee 100644 --- a/internal/action/mount.go +++ b/internal/action/mount.go @@ -28,10 +28,6 @@ func (s *Action) MountRemove(c *cli.Context) error { out.Errorf(ctx, "Failed to remove mount: %s", err) } - if err := s.cfg.Save(); err != nil { - return exit.Error(exit.Config, err, "failed to write config: %s", err) - } - out.Printf(ctx, "Password Store %s umounted", c.Args().Get(0)) return nil @@ -105,10 +101,6 @@ func (s *Action) MountAdd(c *cli.Context) error { return exit.Error(exit.Mount, err, "failed to add mount %q to %q: %s", alias, localPath, err) } - if err := s.cfg.Save(); err != nil { - return exit.Error(exit.Config, err, "failed to save config: %s", err) - } - out.Printf(ctx, "Mounted %s as %s", alias, localPath) return nil diff --git a/internal/action/mount_test.go b/internal/action/mount_test.go index 24985aa471..c0b8cef103 100644 --- a/internal/action/mount_test.go +++ b/internal/action/mount_test.go @@ -24,6 +24,7 @@ func TestMounts(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/move_test.go b/internal/action/move_test.go index 47ac7c9d75..bb3d242471 100644 --- a/internal/action/move_test.go +++ b/internal/action/move_test.go @@ -24,6 +24,7 @@ func TestMove(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/otp.go b/internal/action/otp.go index ff134489e7..b5b49c00c0 100644 --- a/internal/action/otp.go +++ b/internal/action/otp.go @@ -158,7 +158,7 @@ func (s *Action) otp(ctx context.Context, name, qrf string, clip, pw, recurse bo debug.Log("OTP period: %ds", two.Period()) if clip { - if err := clipboard.CopyTo(ctx, fmt.Sprintf("token for %s", name), []byte(token), s.cfg.ClipTimeout); err != nil { + if err := clipboard.CopyTo(ctx, fmt.Sprintf("token for %s", name), []byte(token), s.cfg.GetInt("core.cliptimeout")); err != nil { return exit.Error(exit.IO, err, "failed to copy to clipboard: %s", err) } diff --git a/internal/action/otp_test.go b/internal/action/otp_test.go index a7d1c529fe..ea4fd64cf1 100644 --- a/internal/action/otp_test.go +++ b/internal/action/otp_test.go @@ -27,6 +27,7 @@ func TestOTP(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/process_test.go b/internal/action/process_test.go index bad569d806..8703593e10 100644 --- a/internal/action/process_test.go +++ b/internal/action/process_test.go @@ -35,6 +35,7 @@ func TestProcess(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) sec := secrets.New() assert.NoError(t, sec.Set("username", "admin")) diff --git a/internal/action/rcs_test.go b/internal/action/rcs_test.go index a19402f52b..8045b8ab90 100644 --- a/internal/action/rcs_test.go +++ b/internal/action/rcs_test.go @@ -24,6 +24,7 @@ func TestGit(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/recipients_test.go b/internal/action/recipients_test.go index c59c0c58c4..588464f572 100644 --- a/internal/action/recipients_test.go +++ b/internal/action/recipients_test.go @@ -26,6 +26,7 @@ func TestRecipients(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf @@ -99,6 +100,7 @@ func TestRecipientsGpg(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/repl.go b/internal/action/repl.go index 12877bbd4a..54a5bcefff 100644 --- a/internal/action/repl.go +++ b/internal/action/repl.go @@ -168,11 +168,7 @@ READ: continue default: } - // need to reinitialize the config to pick up any changes from the - // previous iteration - // TODO: this means the context will grow with every loop. Eventually - // this might lead to memory issues so we should see if we can optimize it. - c.Context = s.cfg.WithContext(c.Context) + if err := c.App.RunContext(c.Context, append([]string{"gopass"}, args...)); err != nil { continue } diff --git a/internal/action/setup.go b/internal/action/setup.go index 02054fc692..6a42f8beab 100644 --- a/internal/action/setup.go +++ b/internal/action/setup.go @@ -90,7 +90,15 @@ func (s *Action) Setup(c *cli.Context) error { } // assume local setup by default, remotes can be added easily later. - return s.initLocal(ctx) + if err := s.initLocal(ctx); err != nil { + debug.Log("Setup failed. initLocal error: %s", err) + + return err + } + + debug.Log("Setup finished. All systems go. 🚀") + + return nil } func (s *Action) initCheckPrivateKeys(ctx context.Context, crypto backend.Crypto) error { @@ -292,12 +300,7 @@ func (s *Action) initLocal(ctx context.Context) error { out.Warningf(ctx, "Failed to add passage mount: %s", err) } - // save config. - if err := s.cfg.Save(); err != nil { - return fmt.Errorf("failed to save config: %w", err) - } - - out.OKf(ctx, "Configuration written to %s", s.cfg.Path) + out.OKf(ctx, "Configuration written") return nil } diff --git a/internal/action/show.go b/internal/action/show.go index 660a56d124..38838842a9 100644 --- a/internal/action/show.go +++ b/internal/action/show.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/gopasspw/gopass/internal/action/exit" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/notify" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/internal/store" @@ -49,7 +50,7 @@ func showParseArgs(c *cli.Context) context.Context { } if c.IsSet("noparsing") { - ctx = ctxutil.WithShowParsing(ctx, !c.Bool("noparsing")) + _ = config.FromContext(ctx).SetEnv("core.parsing", fmt.Sprintf("%t", !c.Bool("noparsing"))) } if c.IsSet("chars") { @@ -182,8 +183,8 @@ func (s *Action) showHandleOutput(ctx context.Context, name string, sec gopass.S } if pw == "" && body == "" { - if ctxutil.IsShowSafeContent(ctx) && !ctxutil.IsForce(ctx) { - out.Warning(ctx, "safecontent=true. Use -f to display password, if any") + if config.Bool(ctx, "core.showsafecontent") && !ctxutil.IsForce(ctx) { + out.Warning(ctx, "core.showsafecontent=true. Use -f to display password, if any") } return exit.Error(exit.NotFound, store.ErrEmptySecret, store.ErrEmptySecret.Error()) @@ -195,8 +196,8 @@ func (s *Action) showHandleOutput(ctx context.Context, name string, sec gopass.S } } - if IsClip(ctx) && pw != "" { - if err := clipboard.CopyTo(ctx, name, []byte(pw), s.cfg.ClipTimeout); err != nil { + if (IsClip(ctx) || config.Bool(ctx, "core.showautoclip")) && pw != "" { + if err := clipboard.CopyTo(ctx, name, []byte(pw), s.cfg.GetInt("core.cliptimeout")); err != nil { return err } } @@ -210,7 +211,7 @@ func (s *Action) showHandleOutput(ctx context.Context, name string, sec gopass.S header := fmt.Sprintf("Secret: %s\n", name) if HasKey(ctx) { header += fmt.Sprintf("Key: %s\n", GetKey(ctx)) - } else if ctxutil.IsShowParsing(ctx) { + } else if config.Bool(ctx, "core.parsing") { out.Warning(ctx, "Parsing is enabled. Use -n to disable.") } out.Print(ctx, header) @@ -224,7 +225,7 @@ func (s *Action) showHandleOutput(ctx context.Context, name string, sec gopass.S func (s *Action) showGetContent(ctx context.Context, sec gopass.Secret) (string, string, error) { // YAML key. - if HasKey(ctx) && ctxutil.IsShowParsing(ctx) { + if HasKey(ctx) && config.Bool(ctx, "core.parsing") { key := GetKey(ctx) values, found := sec.Values(key) if !found { @@ -255,8 +256,8 @@ func (s *Action) showGetContent(ctx context.Context, sec gopass.Secret) (string, } // everything but the first line. - if ctxutil.IsShowSafeContent(ctx) && !ctxutil.IsForce(ctx) { - body := showSafeContent(ctx, sec) + if config.Bool(ctx, "core.showsafecontent") && !ctxutil.IsForce(ctx) { + body := showSafeContent(sec) if IsAlsoClip(ctx) { return pw, body, nil } @@ -268,7 +269,7 @@ func (s *Action) showGetContent(ctx context.Context, sec gopass.Secret) (string, return sec.Password(), fullBody, nil } -func showSafeContent(ctx context.Context, sec gopass.Secret) string { +func showSafeContent(sec gopass.Secret) string { var sb strings.Builder for i, k := range sec.Keys() { sb.WriteString(k) @@ -327,7 +328,7 @@ func (s *Action) hasAliasDomain(ctx context.Context, name string) string { p := strings.Split(name, "/") for i := len(p) - 1; i > 0; i-- { d := p[i] - for _, alias := range pwrules.LookupAliases(d) { + for _, alias := range pwrules.LookupAliases(ctx, d) { sn := append(p[0:i], alias) sn = append(sn, p[i+1:]...) aliasName := strings.Join(sn, "/") diff --git a/internal/action/show_test.go b/internal/action/show_test.go index 58cb099ed5..416f0d0602 100644 --- a/internal/action/show_test.go +++ b/internal/action/show_test.go @@ -29,6 +29,7 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) color.NoColor = true buf := &bytes.Buffer{} @@ -39,6 +40,13 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest out.Stdout = os.Stdout }() + // first add another entry in a subdir + sec := secrets.NewKV() + sec.SetPassword("123") + assert.NoError(t, sec.Set("bar", "zab")) + assert.NoError(t, act.Store.Set(ctx, "bar/baz", sec)) + buf.Reset() + t.Run("show foo", func(t *testing.T) { //nolint:paralleltest defer buf.Reset() c := gptest.CliCtx(ctx, t, "foo") @@ -54,21 +62,15 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest }) t.Run("show dir", func(t *testing.T) { //nolint:paralleltest - // first add another entry in a subdir - sec := secrets.NewKV() - sec.SetPassword("123") - assert.NoError(t, sec.Set("bar", "zab")) - assert.NoError(t, act.Store.Set(ctx, "bar/baz", sec)) - buf.Reset() - c := gptest.CliCtx(ctx, t, "bar") assert.NoError(t, act.Show(c)) assert.Equal(t, "bar/\n└── baz\n\n", buf.String()) buf.Reset() }) + require.NoError(t, act.cfg.Set("", "core.showsafecontent", "true")) + t.Run("show twoliner with safecontent enabled", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowSafeContent(ctx, true) c := gptest.CliCtx(ctx, t, "bar/baz") assert.NoError(t, act.Show(c)) @@ -79,8 +81,6 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest }) t.Run("show foo with safecontent enabled, should error out", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowSafeContent(ctx, true) - c := gptest.CliCtx(ctx, t, "foo") assert.NoError(t, act.Show(c)) assert.NotContains(t, buf.String(), "secret") @@ -95,7 +95,6 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest }) t.Run("show twoliner with safecontent enabled, but with the clip flag, which should copy just the secret", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowSafeContent(ctx, true) c := gptest.CliCtxWithFlags(ctx, t, map[string]string{"clip": "true"}, "bar/baz") assert.NoError(t, act.Show(c)) @@ -113,7 +112,6 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest assert.NoError(t, act.Store.Set(ctx, "unsafe/keys", sec)) buf.Reset() - ctx := ctxutil.WithShowSafeContent(ctx, true) c := gptest.CliCtx(ctx, t, "unsafe/keys") assert.NoError(t, act.Show(c)) assert.Contains(t, buf.String(), "*****") @@ -123,7 +121,6 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest }) t.Run("show twoliner with safecontent enabled", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowSafeContent(ctx, true) c := gptest.CliCtx(ctx, t, "bar/baz") assert.NoError(t, act.Show(c)) @@ -134,8 +131,7 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest }) t.Run("show twoliner with parsing disabled and safecontent enabled", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowSafeContent(ctx, true) - ctx = ctxutil.WithShowParsing(ctx, false) + require.NoError(t, act.cfg.SetEnv("core.parsing", "false")) c := gptest.CliCtx(ctx, t, "bar/baz") assert.NoError(t, act.Show(c)) @@ -146,8 +142,10 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest buf.Reset() }) + require.NoError(t, act.cfg.Set("", "core.showsafecontent", "false")) + t.Run("show key with parsing enabled", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowParsing(ctx, true) + require.NoError(t, act.cfg.SetEnv("core.parsing", "true")) c := gptest.CliCtx(ctx, t, "bar/baz", "bar") assert.NoError(t, act.Show(c)) @@ -156,7 +154,7 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest }) t.Run("show key with parsing disabled", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowParsing(ctx, false) + require.NoError(t, act.cfg.SetEnv("core.parsing", "false")) c := gptest.CliCtx(ctx, t, "bar/baz", "bar") assert.NoError(t, act.Show(c)) @@ -165,7 +163,7 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest }) t.Run("show nonexisting key with parsing enabled", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowParsing(ctx, true) + require.NoError(t, act.cfg.SetEnv("core.parsing", "true")) c := gptest.CliCtx(ctx, t, "bar/baz", "nonexisting") assert.Error(t, act.Show(c)) @@ -173,19 +171,19 @@ func TestShowMulti(t *testing.T) { //nolint:paralleltest }) t.Run("show keys with mixed case", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowParsing(ctx, true) + require.NoError(t, act.cfg.SetEnv("core.parsing", "true")) - assert.NoError(t, act.insertStdin(ctx, "baz", []byte("foobar\nOther: meh\nuser: name\nbody text"), false)) + assert.NoError(t, act.insertStdin(ctx, "baz2", []byte("foobar\nOther: meh\nuser: name\nbody text"), false)) buf.Reset() - c := gptest.CliCtx(ctx, t, "baz", "Other") + c := gptest.CliCtx(ctx, t, "baz2", "Other") assert.NoError(t, act.Show(c)) assert.Equal(t, "meh", buf.String()) buf.Reset() }) t.Run("show value with format strings", func(t *testing.T) { //nolint:paralleltest - ctx := ctxutil.WithShowParsing(ctx, true) + require.NoError(t, act.cfg.SetEnv("core.parsing", "true")) pw := "some-chars-are-odd-%s-%p-%q" @@ -218,6 +216,7 @@ func TestShowAutoClip(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) color.NoColor = true stdoutBuf := &bytes.Buffer{} @@ -244,7 +243,7 @@ func TestShowAutoClip(t *testing.T) { //nolint:paralleltest // terminal=false ctx = ctxutil.WithTerminal(ctx, false) // initialize context with config values, also detects if we're running in a terminal - ctx = act.Store.WithContext(ctx) + ctx = act.Store.WithStoreConfig(ctx) c := gptest.CliCtx(ctx, t, "foo") assert.NoError(t, act.Show(c)) @@ -348,6 +347,7 @@ func TestShowHandleRevision(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) color.NoColor = true buf := &bytes.Buffer{} @@ -375,6 +375,7 @@ func TestShowHandleError(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) color.NoColor = true buf := &bytes.Buffer{} @@ -403,6 +404,7 @@ func TestShowPrintQR(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) //nolint:ineffassign color.NoColor = true buf := &bytes.Buffer{} diff --git a/internal/action/sync.go b/internal/action/sync.go index 55f76ae5f7..56e7aa0e20 100644 --- a/internal/action/sync.go +++ b/internal/action/sync.go @@ -10,6 +10,7 @@ import ( "github.com/fatih/color" "github.com/gopasspw/gopass/internal/backend" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/diff" "github.com/gopasspw/gopass/internal/notify" "github.com/gopasspw/gopass/internal/out" @@ -32,6 +33,8 @@ func init() { return } + debug.Log("GOPASS_AUTOSYNC_INTERVAL is deprecated. Please use autosync.interval") + iv, err := strconv.Atoi(sv) if err != nil { return @@ -55,12 +58,24 @@ func (s *Action) autoSync(ctx context.Context) error { } if sv := os.Getenv("GOPASS_NO_AUTOSYNC"); sv != "" { + out.Warning(ctx, "GOPASS_NO_AUTOSYNC is deprecated. Please set core.autosync = false.") + + return nil + } + + if !config.Bool(ctx, "core.autosync") { return nil } ls := s.rem.LastSeen("autosync") debug.Log("autosync - last seen: %s", ls) - if time.Since(ls) > time.Duration(autosyncIntervalDays)*24*time.Hour { + syncInterval := autosyncIntervalDays + + if s.cfg.IsSet("autosync.interval") { + syncInterval = s.cfg.GetInt("autosync.interval") + } + + if time.Since(ls) > time.Duration(syncInterval)*24*time.Hour { _ = s.rem.Reset("autosync") err := s.sync(ctx, "") @@ -128,6 +143,14 @@ func (s *Action) sync(ctx context.Context, store string) error { // syncMount syncs a single mount. func (s *Action) syncMount(ctx context.Context, mp string) error { + // using GetM here to get the value for this mount, it might be different + // than the global value + if as := s.cfg.GetM(mp, "core.autosync"); as == "false" { + debug.Log("not syncing %s, autosync is disabled for this mount", mp) + + return nil + } + ctxno := out.WithNewline(ctx, false) name := mp if mp == "" { @@ -181,11 +204,12 @@ func (s *Action) syncMount(ctx context.Context, mp string) error { } syncPrintDiff(ctxno, l, ln) - debug.Log("Syncing Mount %s. Exportkeys: %t", mp, ctxutil.IsExportKeys(ctx)) + exportKeys := s.cfg.GetBool("core.exportkeys") + debug.Log("Syncing Mount %s. Exportkeys: %t", mp, exportKeys) if err := syncImportKeys(ctxno, sub, name); err != nil { return err } - if ctxutil.IsExportKeys(ctx) { + if exportKeys { if err := syncExportKeys(ctxno, sub, name); err != nil { return err } diff --git a/internal/action/sync_test.go b/internal/action/sync_test.go index 38c4d8bb45..c997c3d5a6 100644 --- a/internal/action/sync_test.go +++ b/internal/action/sync_test.go @@ -30,6 +30,7 @@ func TestSync(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) t.Run("default", func(t *testing.T) { //nolint:paralleltest defer buf.Reset() diff --git a/internal/action/templates_test.go b/internal/action/templates_test.go index aea69c07b5..91f5713902 100644 --- a/internal/action/templates_test.go +++ b/internal/action/templates_test.go @@ -27,6 +27,7 @@ func TestTemplates(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) buf := &bytes.Buffer{} out.Stdout = buf diff --git a/internal/action/unclip_test.go b/internal/action/unclip_test.go index 115b3afcf4..5645606a1c 100644 --- a/internal/action/unclip_test.go +++ b/internal/action/unclip_test.go @@ -30,6 +30,7 @@ func TestUnclip(t *testing.T) { //nolint:paralleltest act, err := newMock(ctx, u.StoreDir("")) require.NoError(t, err) require.NotNil(t, act) + ctx = act.cfg.WithConfig(ctx) t.Run("unlcip should fail", func(t *testing.T) { assert.Error(t, act.Unclip(gptest.CliCtxWithFlags(ctx, t, map[string]string{"timeout": "0"}))) diff --git a/internal/backend/crypto/age/askpass.go b/internal/backend/crypto/age/askpass.go index cdb6ab0a48..6fc0d5eaf2 100644 --- a/internal/backend/crypto/age/askpass.go +++ b/internal/backend/crypto/age/askpass.go @@ -7,6 +7,7 @@ import ( "time" "github.com/gopasspw/gopass/internal/cache" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/pkg/debug" "github.com/gopasspw/gopass/pkg/pinentry/cli" "github.com/nbutton23/zxcvbn-go" @@ -82,7 +83,7 @@ func newAskPass(ctx context.Context) *askPass { cache: cache.NewInMemTTL[string, string](time.Hour, 24*time.Hour), } - if IsUseKeychain(ctx) { + if config.Bool(ctx, "age.usekeychain") { if err := keyring.Set("gopass", "sentinel", "empty"); err == nil { debug.Log("using OS keychain to cache age credentials") a.cache = newOsKeyring() diff --git a/internal/backend/crypto/age/context.go b/internal/backend/crypto/age/context.go index 5b41c7ae90..c5e7acb9a9 100644 --- a/internal/backend/crypto/age/context.go +++ b/internal/backend/crypto/age/context.go @@ -6,7 +6,6 @@ type contextKey int const ( ctxKeyOnlyNative contextKey = iota - ctxKeyUseKeychain ) // WithOnlyNative will return a context with the flag for only native set. @@ -24,27 +23,3 @@ func IsOnlyNative(ctx context.Context) bool { return bv } - -// WithUseKeychain returns a context with the value of use keychain -// set. -func WithUseKeychain(ctx context.Context, bv bool) context.Context { - return context.WithValue(ctx, ctxKeyUseKeychain, bv) -} - -// IsUseKeychain returns the value of use keychain. -func IsUseKeychain(ctx context.Context) bool { - bv, ok := ctx.Value(ctxKeyUseKeychain).(bool) - if !ok { - return false - } - - return bv -} - -// HasUseKeychain returns true if a value for use keychain -// was set in the context. -func HasUseKeychain(ctx context.Context) bool { - _, ok := ctx.Value(ctxKeyUseKeychain).(bool) - - return ok -} diff --git a/internal/backend/crypto/age/ssh.go b/internal/backend/crypto/age/ssh.go index 8af3480ec8..aa02653b7c 100644 --- a/internal/backend/crypto/age/ssh.go +++ b/internal/backend/crypto/age/ssh.go @@ -54,11 +54,9 @@ func (a *Age) getSSHIdentities(ctx context.Context) (map[string]age.Identity, er recp, id, err := a.parseSSHIdentity(ctx, fn) if err != nil { - // debug.Log("Failed to parse SSH identity %s: %s", fn, err) continue } - // debug.Log("parsed SSH identity %s from %s", recp, fn) ids[recp] = id } sshCache = ids diff --git a/internal/backend/crypto/plain/backend.go b/internal/backend/crypto/plain/backend.go index 58afb5b160..2e10d37416 100644 --- a/internal/backend/crypto/plain/backend.go +++ b/internal/backend/crypto/plain/backend.go @@ -97,11 +97,6 @@ func (m *Mocker) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) return ciphertext, nil } -// ExportPublicKey does nothing. -func (m *Mocker) ExportPublicKey(context.Context, string) ([]byte, error) { - return nil, nil -} - // ImportPublicKey does nothing. func (m *Mocker) ImportPublicKey(context.Context, []byte) error { return nil diff --git a/internal/backend/crypto/plain/backend_test.go b/internal/backend/crypto/plain/backend_test.go index ca47c0b692..b4e5fa3a65 100644 --- a/internal/backend/crypto/plain/backend_test.go +++ b/internal/backend/crypto/plain/backend_test.go @@ -51,8 +51,6 @@ func TestPlain(t *testing.T) { _, err = m.FindIdentities(ctx) assert.NoError(t, err) - buf, err = m.ExportPublicKey(ctx, "") - assert.NoError(t, err) assert.NoError(t, m.ImportPublicKey(ctx, buf)) assert.Equal(t, semver.Version{}, m.Version(ctx)) diff --git a/internal/backend/storage/gitfs/config.go b/internal/backend/storage/gitfs/config.go index 11a7c03132..ff864d899d 100644 --- a/internal/backend/storage/gitfs/config.go +++ b/internal/backend/storage/gitfs/config.go @@ -4,13 +4,11 @@ import ( "context" "fmt" "os" - "os/exec" "path/filepath" "strings" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/internal/store" - "github.com/gopasspw/gopass/pkg/debug" ) const ( @@ -88,7 +86,8 @@ func (g *Git) InitConfig(ctx context.Context, userName, userEmail string) error // ConfigSet sets a local config value. func (g *Git) ConfigSet(ctx context.Context, key, value string) error { - return g.Cmd(ctx, "gitConfigSet", "config", "--local", key, value) + // return g.Cmd(ctx, "gitConfigSet", "config", "--local", key, value) + return g.cfg.SetLocal(key, value) } // ConfigGet returns a given config value. @@ -97,19 +96,14 @@ func (g *Git) ConfigGet(ctx context.Context, key string) (string, error) { return "", store.ErrGitNotInit } - buf := &strings.Builder{} + value := g.cfg.Get(key) + if value == "" { + g.cfg.Reload() - cmd := exec.CommandContext(ctx, "git", "config", "--get", key) - cmd.Dir = g.fs.Path() - cmd.Stdout = buf - cmd.Stderr = os.Stderr - - debug.Log("%s %+v", cmd.Path, cmd.Args) - if err := cmd.Run(); err != nil { - return "", err + value = g.cfg.Get(key) } - return strings.TrimSpace(buf.String()), nil + return value, nil } // ConfigList returns all git config settings. @@ -118,26 +112,9 @@ func (g *Git) ConfigList(ctx context.Context) (map[string]string, error) { return nil, store.ErrGitNotInit } - buf := &strings.Builder{} - - cmd := exec.CommandContext(ctx, "git", "config", "--list") - cmd.Dir = g.fs.Path() - cmd.Stdout = buf - cmd.Stderr = os.Stderr - - debug.Log("%s %+v", cmd.Path, cmd.Args) - if err := cmd.Run(); err != nil { - return nil, err - } - - lines := strings.Split(buf.String(), "\n") - kv := make(map[string]string, len(lines)) - for _, line := range lines { - key, val, found := strings.Cut(strings.TrimSpace(line), "=") - if !found { - continue - } - kv[key] = val + kv := make(map[string]string, 23) + for _, k := range g.cfg.List("") { + kv[k] = g.cfg.Get(k) } return kv, nil diff --git a/internal/backend/storage/gitfs/git.go b/internal/backend/storage/gitfs/git.go index 2f50e07817..294a0823da 100644 --- a/internal/backend/storage/gitfs/git.go +++ b/internal/backend/storage/gitfs/git.go @@ -21,6 +21,7 @@ import ( "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/gopasspw/gopass/pkg/debug" "github.com/gopasspw/gopass/pkg/fsutil" + "github.com/gopasspw/gopass/pkg/gitconfig" ) type contextKey int @@ -43,17 +44,20 @@ func getPathOverride(ctx context.Context, def string) string { // Git is a cli based git backend. type Git struct { - fs *fs.Store + fs *fs.Store + cfg *gitconfig.Configs } // New creates a new git cli based git backend. func New(path string) (*Git, error) { - if !fsutil.IsDir(filepath.Join(path, ".git")) { + gitDir := filepath.Join(path, ".git") + if !fsutil.IsDir(gitDir) { return nil, fmt.Errorf("git repo does not exist") } return &Git{ - fs: fs.New(path), + fs: fs.New(path), + cfg: gitconfig.New().LoadAll(gitDir), }, nil } @@ -61,13 +65,16 @@ func New(path string) (*Git, error) { // configured for this clone repo. func Clone(ctx context.Context, repo, path, userName, userEmail string) (*Git, error) { g := &Git{ - fs: fs.New(path), + fs: fs.New(path), + cfg: gitconfig.New(), } if err := g.Cmd(withPathOverride(ctx, filepath.Dir(path)), "Clone", "clone", repo, path); err != nil { return nil, err } + g.cfg.LoadAll(filepath.Join(path, ".git")) + // initialize the local git config. if err := g.InitConfig(ctx, userName, userEmail); err != nil { return g, fmt.Errorf("failed to configure git: %w", err) @@ -80,8 +87,10 @@ func Clone(ctx context.Context, repo, path, userName, userEmail string) (*Git, e // Init initializes this store's git repo. func Init(ctx context.Context, path, userName, userEmail string) (*Git, error) { g := &Git{ - fs: fs.New(path), + fs: fs.New(path), + cfg: gitconfig.New(), } + // the git repo may be empty (i.e. no branches, cloned from a fresh remote) // or already initialized. Only run git init if the folder is completely empty. if !g.IsInitialized() { @@ -91,6 +100,8 @@ func Init(ctx context.Context, path, userName, userEmail string) (*Git, error) { out.Printf(ctx, "git initialized at %s", g.fs.Path()) } + g.cfg.LoadAll(filepath.Join(path, ".git")) + if !ctxutil.IsGitInit(ctx) { return g, nil } @@ -284,6 +295,8 @@ func (g *Git) PushPull(ctx context.Context, op, remote, branch string) error { return nil } if !g.IsInitialized() { + debug.Log("Git in %s is not initialized. Can not push/pull", g.Path()) + return store.ErrGitNotInit } @@ -295,7 +308,10 @@ func (g *Git) PushPull(ctx context.Context, op, remote, branch string) error { remote = g.defaultRemote(ctx, branch) } - if v, err := g.ConfigGet(ctx, "remote."+remote+".url"); err != nil || v == "" { + urlKey := "remote." + remote + ".url" + if v, err := g.ConfigGet(ctx, urlKey); err != nil || v == "" { + debug.Log("No value for %q found in config. Keys: %+v", urlKey, g.cfg.Keys()) + return store.ErrGitNoRemote } diff --git a/internal/config/config.go b/internal/config/config.go index 270bc38412..dc3e3896bd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,152 +2,228 @@ package config import ( "fmt" - "path/filepath" - "reflect" + "os" "strconv" "strings" + + "github.com/gopasspw/gopass/pkg/debug" + "github.com/gopasspw/gopass/pkg/gitconfig" ) var ( - // ErrConfigNotFound is returned on load if the config was not found. - ErrConfigNotFound = fmt.Errorf("config not found") - // ErrConfigNotParsed is returned on load if the config could not be decoded. - ErrConfigNotParsed = fmt.Errorf("config not parseable") + envPrefix = "GOPASS_CONFIG_" + systemConfig = "/etc/gopass/config" ) -// Config is the current config struct. -type Config struct { - AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. - AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. - ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. - ExportKeys bool `yaml:"exportkeys"` // automatically export public keys of all recipients. - NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. - Notifications bool `yaml:"notifications"` // enable desktop notifications. - Parsing bool `yaml:"parsing"` // allows to switch off all output parsing. - Path string `yaml:"path"` - SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. - Mounts map[string]string `yaml:"mounts"` - UseKeychain bool `yaml:"keychain"` // use OS keychain for age +func newGitconfig() *gitconfig.Configs { + c := gitconfig.New() + c.EnvPrefix = envPrefix + c.GlobalConfig = os.Getenv("GOPASS_CONFIG") + c.SystemConfig = systemConfig - ConfigPath string `yaml:"-"` + return c +} - // Catches all undefined files and must be empty after parsing. - XXX map[string]any `yaml:",inline"` +var defaults = map[string]string{ + "core.autosync": "true", + "core.cliptimeout": "45", + "core.exportkeys": "true", + "core.notifications": "true", + "core.parsing": "true", } -// New creates a new config with sane default values. +// Config is a gopass config handler. +type Config struct { + root *gitconfig.Configs + cfgs map[string]*gitconfig.Configs +} + +// New initializes a new gopass config. It will handle legacy configs as well. func New() *Config { - return &Config{ - AutoImport: false, - ClipTimeout: 45, - ExportKeys: true, - Mounts: make(map[string]string), - Notifications: true, - Parsing: true, - Path: PwStoreDir(""), - ConfigPath: configLocation(), - } + return newWithOptions(false) } -// CheckOverflow implements configer. It will check for any extra config values not. -// handled by the current struct. -func (c *Config) CheckOverflow() error { - return checkOverflow(c.XXX) +// NewNoWrites initializes a new config that does not allow writes. For use in tests. +func NewNoWrites() *Config { + return newWithOptions(true) } -// Config will return a current config. -func (c *Config) Config() *Config { +func newWithOptions(noWrites bool) *Config { + c := &Config{ + cfgs: make(map[string]*gitconfig.Configs, 42), + } + + // if there is no per-user gitconfig we try to migrate + // an existing config. But we will leave it around for + // gopass fsck to (optionaly) clean it up. + if nm := os.Getenv("GOPASS_CONFIG_NO_MIGRATE"); !gitconfig.HasGlobalConfig() && nm == "" { + if err := migrateConfigs(); err != nil { + debug.Log("failed to migrate: %s", err) + } + } + + // load the global config to get the root path + c.root = newGitconfig().LoadAll("") + c.root.NoWrites = noWrites + + rootPath := c.root.Get("mounts.path") + if rootPath == "" { + if err := c.SetPath(PwStoreDir("")); err != nil { + debug.Log("failed to set path: %s", err) + } + } + // load again, this might add a per-store config from the root store + c.root.LoadAll(rootPath) + c.root.NoWrites = noWrites + + if rootPath := c.root.Get("mounts.path"); rootPath == "" { + if err := c.SetPath(PwStoreDir("")); err != nil { + debug.Log("failed to set path: %s", err) + } + } + + // set global defaults + c.root.Preset = gitconfig.NewFromMap(defaults) + + for _, m := range c.Mounts() { + c.cfgs[m] = newGitconfig().LoadAll(c.MountPath(m)) + c.cfgs[m].NoWrites = noWrites + } + return c } -// SetConfigValue will try to set the given key to the value in the config struct. -func (c *Config) SetConfigValue(key, value string) error { - if err := c.setConfigValue(key, value); err != nil { - return err +// IsSet returns true if the key is set in the root config. +func (c *Config) IsSet(key string) bool { + return c.root.IsSet(key) +} + +// Get returns the given key from the root config. +func (c *Config) Get(key string) string { + return c.root.Get(key) +} + +// GetM returns the given key from the mount or the root config if mount is empty. +func (c *Config) GetM(mount, key string) string { + if mount == "" { + return c.root.Get(key) + } + + if cfg := c.cfgs[mount]; cfg != nil { + return cfg.Get(key) } - return c.Save() + return "" } -// setConfigValue will try to set the given key to the value in the config struct. -func (c *Config) setConfigValue(key, value string) error { - value = strings.ToLower(value) - o := reflect.ValueOf(c).Elem() - for i := 0; i < o.NumField(); i++ { - jsonArg := o.Type().Field(i).Tag.Get("yaml") - if jsonArg == "" || jsonArg == "-" { - continue - } - if jsonArg != key { - continue - } - f := o.Field(i) - switch f.Kind() { //nolint:exhaustive - case reflect.String: - f.SetString(value) - - return nil - case reflect.Bool: - switch { - case value == "true" || value == "on": - f.SetBool(true) - - return nil - case value == "false" || value == "off": - f.SetBool(false) - - return nil - default: - return fmt.Errorf("not a bool: %s", value) - } - case reflect.Int: - iv, err := strconv.Atoi(value) - if err != nil { - return fmt.Errorf("failed to convert %q to integer: %w", value, err) - } - f.SetInt(int64(iv)) - - return nil - default: - continue - } +// GetBool returns true if the value of the key evaluates to "true". +// Otherwise it returns false. +func (c *Config) GetBool(key string) bool { + if strings.ToLower(strings.TrimSpace(c.Get(key))) == "true" { + return true + } + + return false +} + +// GetInt returns the integer value of the key if it can be parsed. +// Otherwise it returns 0. +func (c *Config) GetInt(key string) int { + iv, err := strconv.Atoi(c.Get(key)) + if err != nil { + return 0 } - return fmt.Errorf("unknown config option %q", key) + return iv } -func (c *Config) String() string { - return fmt.Sprintf("%#v", c) +// Set tries to set the key to the given value. +// The mount option is necessary to discern between +// the per-user (global) and possible per-directory (local) +// config files. +// +// - If mount is empty the setting will be written to the per-user config (global) +// - If mount has the special value "" the setting will be written to the per-directory config of the root store (local) +// - If mount has any other value we will attempt to write the setting to the per-directory config of this mount. +// - If the mount point does not exist we will return nil. +func (c *Config) Set(mount, key, value string) error { + if mount == "" { + return c.root.SetGlobal(key, value) + } + + if mount == "" { + return c.root.SetLocal(key, value) + } + + if cfg := c.cfgs[mount]; cfg != nil { + return cfg.SetLocal(key, value) + } + + return nil } -// Directory returns the directory this config is using. -func (c *Config) Directory() string { - return filepath.Dir(c.Path) +// SetEnv overrides a key in the non-persistent layer. +func (c *Config) SetEnv(key, value string) error { + return c.root.SetEnv(key, value) } -// ConfigMap returns a map of stringified config values for easy printing. -func (c *Config) ConfigMap() map[string]string { - m := make(map[string]string, 20) - o := reflect.ValueOf(c).Elem() - for i := 0; i < o.NumField(); i++ { - jsonArg := o.Type().Field(i).Tag.Get("yaml") - if jsonArg == "" || jsonArg == "-" { - continue - } - f := o.Field(i) - var strVal string - switch f.Kind() { //nolint:exhaustive - case reflect.String: - strVal = f.String() - case reflect.Bool: - strVal = fmt.Sprintf("%t", f.Bool()) - case reflect.Int: - strVal = fmt.Sprintf("%d", f.Int()) - default: - continue - } - m[jsonArg] = strVal +// Path returns the root store path. +func (c *Config) Path() string { + return c.Get("mounts.path") +} + +// MountPath returns the mount store path. +func (c *Config) MountPath(mountPoint string) string { + return c.Get(mpk(mountPoint)) +} + +// SetPath is a short cut to set the root store path. +func (c *Config) SetPath(path string) error { + return c.Set("", "mounts.path", path) +} + +// SetMountPath is a short cut to set a mount to a path. +func (c *Config) SetMountPath(mount, path string) error { + return c.Set("", mpk(mount), path) +} + +// mpk for mountPathKey. +func mpk(mount string) string { + return fmt.Sprintf("mounts.%s.path", mount) +} + +// Mounts returns all mount points from the root config. +// Note: Any mounts in local configs are ignored. +func (c *Config) Mounts() []string { + return c.root.ListSubsections("mounts") +} + +// Unset deletes the key from the given config. +func (c *Config) Unset(mount, key string) error { + if mount == "" { + return c.root.UnsetGlobal(key) + } + + if mount == "" { + return c.root.UnsetLocal(key) + } + + if cfg := c.cfgs[mount]; cfg != nil { + return cfg.UnsetLocal(key) + } + + return nil +} + +// Keys returns all keys in the given config. +func (c *Config) Keys(mount string) []string { + if mount == "" { + return c.root.Keys() + } + + if cfg := c.cfgs[mount]; cfg != nil { + return cfg.Keys() } - return m + return nil } diff --git a/internal/config/context.go b/internal/config/context.go index c09cfa46fc..47515b6f66 100644 --- a/internal/config/context.go +++ b/internal/config/context.go @@ -3,40 +3,28 @@ package config import ( "context" - "github.com/gopasspw/gopass/internal/backend/crypto/age" - "github.com/gopasspw/gopass/pkg/ctxutil" + "github.com/gopasspw/gopass/pkg/gitconfig" ) -// WithContext returns a context with all config options set for this store -// config, iff they have not been already set in the context. -func (c *Config) WithContext(ctx context.Context) context.Context { - if !c.AutoImport { - ctx = ctxutil.WithImportFunc(ctx, nil) - } - - if !ctxutil.HasExportKeys(ctx) { - ctx = ctxutil.WithExportKeys(ctx, c.ExportKeys) - } - - if !ctxutil.HasNoPager(ctx) { - ctx = ctxutil.WithNoPager(ctx, c.NoPager) - } +type contextKey int - if !ctxutil.HasNotifications(ctx) { - ctx = ctxutil.WithNotifications(ctx, c.Notifications) - } +const ( + ctxKeyConfig contextKey = iota +) - if !ctxutil.HasShowSafeContent(ctx) { - ctx = ctxutil.WithShowSafeContent(ctx, c.SafeContent) - } +func (c *Config) WithConfig(ctx context.Context) context.Context { + return context.WithValue(ctx, ctxKeyConfig, c) +} - if !ctxutil.HasShowParsing(ctx) { - ctx = ctxutil.WithShowParsing(ctx, c.Parsing) +func FromContext(ctx context.Context) *Config { + if c, found := ctx.Value(ctxKeyConfig).(*Config); found && c != nil { + return c } - if !age.HasUseKeychain(ctx) { - ctx = age.WithUseKeychain(ctx, c.UseKeychain) + c := &Config{ + root: newGitconfig().LoadAll(""), } + c.root.Preset = gitconfig.NewFromMap(defaults) - return ctx + return c } diff --git a/internal/config/docs_test.go b/internal/config/docs_test.go new file mode 100644 index 0000000000..bef0e4a9f2 --- /dev/null +++ b/internal/config/docs_test.go @@ -0,0 +1,291 @@ +package config + +import ( + "bufio" + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/gopasspw/gopass/internal/set" + "golang.org/x/exp/maps" +) + +// ignoredEnvs is a list of environment variables that are used by gopass +// but originate from elsewhere. They should be well known and properly +// documented already. +var ignoredEnvs = set.Map([]string{ + "APPDATA", + "GIT_AUTHOR_EMAIL", + "GIT_AUTHOR_NAME", + "GNUPGHOME", + "GOPATH", + "GOPASS_CONFIG_NOSYSTEM", // name assembled, tests can't catch it + "GOPASS_DEBUG_FILES", // indirect usage + "GOPASS_DEBUG_FUNCS", // indirect usage + "GOPASS_GPG_OPTS", // indirect usage + "GOPASS_UMASK", // indirect usage + "PASSWORD_STORE_UMASK", // indirect usage + "GPG_TTY", + "HOME", + "LOCALAPPDATA", + "XDG_CACHE_HOME", + "XDG_CONFIG_HOME", + "XDG_DATA_HOME", +}) + +func TestConfigOptsInDocs(t *testing.T) { + t.Parallel() + + documented := documentedOpts(t) + used := usedOpts(t) + + t.Logf("Config options documented in doc: %+v", documented) + t.Logf("Config options used in the code: %+v", used) + + for k := range documented { + if !used[k] { + t.Errorf("Documented but not used: %s", k) + } + } + for k := range used { + if !documented[k] { + t.Errorf("Used but not documented: %s", k) + } + } +} + +func usedOpts(t *testing.T) map[string]bool { + t.Helper() + + optRE := regexp.MustCompile(`(?:\.Get(?:|Int|Bool)\(\"([a-z]+\.[a-z]+)\"\)|\.GetM\([^,]+, \"([a-z]+\.[a-z]+)\"\)|config\.Bool\(ctx, \"([a-z]+\.[a-z]+)\"\))`) + opts := make(map[string]bool, 42) + + dir := filepath.Join("..", "..") + if err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && strings.HasPrefix(info.Name(), ".") && path != dir { + return filepath.SkipDir + } + if info.IsDir() { + return nil + } + if strings.HasSuffix(info.Name(), "_test.go") { + return nil + } + if !strings.HasSuffix(info.Name(), ".go") { + return nil + } + + return usedOptsInFile(t, path, opts, optRE) + }); err != nil { + t.Errorf("failed to walk %s: %s", dir, err) + } + + return opts +} + +func usedOptsInFile(t *testing.T, fn string, opts map[string]bool, re *regexp.Regexp) error { + t.Helper() + + fh, err := os.Open(fn) + if err != nil { + return err + } + defer fh.Close() //nolint:errcheck + + scanner := bufio.NewScanner(fh) + for scanner.Scan() { + line := scanner.Text() + + if !re.MatchString(line) { + continue + } + + found := re.FindStringSubmatch(line) + // t.Logf("found: %q", found) + if len(found) < 4 { + continue + } + + if found[1] != "" { + opts[found[1]] = true + + continue + } + + if found[2] != "" { + opts[found[2]] = true + } + + if found[3] != "" { + opts[found[3]] = true + + continue + } + } + + return nil +} + +func documentedOpts(t *testing.T) map[string]bool { + t.Helper() + + fn := filepath.Join("..", "..", "docs", "config.md") + fh, err := os.Open(fn) + if err != nil { + t.Fatalf("failed to open %s: %s", fn, err) + } + defer fh.Close() //nolint:errcheck + + optRE := regexp.MustCompile(`^\| .([a-z]+\.[a-z]+).`) + + opts := make(map[string]bool, 42) + scanner := bufio.NewScanner(fh) + for scanner.Scan() { + line := scanner.Text() + + if !optRE.MatchString(line) { + continue + } + found := optRE.FindStringSubmatch(line) + if len(found) < 2 { + continue + } + opts[found[1]] = true + } + + return opts +} + +func TestEnvVarsInDocs(t *testing.T) { + t.Parallel() + + documented := documentedEnvs(t) + used := usedEnvs(t) + + t.Logf("env options documented in doc: %+v", documented) + t.Logf("env options used in the code: %+v", used) + + for _, k := range set.Sorted(maps.Keys(documented)) { + if !used[k] { + t.Errorf("Documented but not used: %s", k) + } + } + for _, k := range set.Sorted(maps.Keys(used)) { + if !documented[k] { + t.Errorf("Used but not documented: %s", k) + } + } +} + +func usedEnvs(t *testing.T) map[string]bool { + t.Helper() + + optRE := regexp.MustCompile(`os\.(?:Getenv|LookupEnv)\(\"([^"]+)\"\)`) + opts := make(map[string]bool, 42) + + dir := filepath.Join("..", "..") + if err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && strings.HasPrefix(info.Name(), ".") && path != dir { + return filepath.SkipDir + } + if info.IsDir() && (info.Name() == "helpers" || info.Name() == "tests") { + return filepath.SkipDir + } + if info.IsDir() { + return nil + } + if strings.HasSuffix(info.Name(), "_test.go") { + return nil + } + if !strings.HasSuffix(info.Name(), ".go") { + return nil + } + + return usedEnvsInFile(t, path, opts, optRE) + }); err != nil { + t.Errorf("failed to walk %s: %s", dir, err) + } + + return opts +} + +func usedEnvsInFile(t *testing.T, fn string, opts map[string]bool, re *regexp.Regexp) error { + t.Helper() + + fh, err := os.Open(fn) + if err != nil { + return err + } + defer fh.Close() //nolint:errcheck + + scanner := bufio.NewScanner(fh) + for scanner.Scan() { + line := scanner.Text() + + if !re.MatchString(line) { + continue + } + + found := re.FindStringSubmatch(line) + // t.Logf("found: %q", found) + if len(found) < 2 { + continue + } + + v := found[1] + + if ignoredEnvs[v] { + continue + } + + opts[v] = true + } + + return nil +} + +func documentedEnvs(t *testing.T) map[string]bool { + t.Helper() + + fn := filepath.Join("..", "..", "docs", "config.md") + fh, err := os.Open(fn) + if err != nil { + t.Fatalf("failed to open %s: %s", fn, err) + } + defer fh.Close() //nolint:errcheck + + optRE := regexp.MustCompile(`^\| .([A-Z0-9_]+).`) + + opts := make(map[string]bool, 42) + scanner := bufio.NewScanner(fh) + for scanner.Scan() { + line := scanner.Text() + + if !optRE.MatchString(line) { + continue + } + found := optRE.FindStringSubmatch(line) + if len(found) < 2 { + continue + } + + v := found[1] + + if ignoredEnvs[v] { + continue + } + + opts[v] = true + } + + return opts +} diff --git a/internal/config/legacy.go b/internal/config/legacy.go index e0c232e085..f11b7ed09c 100644 --- a/internal/config/legacy.go +++ b/internal/config/legacy.go @@ -1,329 +1,44 @@ package config import ( - "net/url" - "strings" -) - -// Pre1127 is a pre-1.12.7 config. -type Pre1127 struct { - AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. - AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. - ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. - ExportKeys bool `yaml:"exportkeys"` // automatically export public keys of all recipients. - NoColor bool `yaml:"nocolor"` // do not use color when outputing text. - NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. - Notifications bool `yaml:"notifications"` // enable desktop notifications. - Parsing bool `yaml:"parsing"` // allows to switch off all output parsing. - Path string `yaml:"path"` - SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. - Mounts map[string]string `yaml:"mounts"` - - ConfigPath string `yaml:"-"` - - // Catches all undefined files and must be empty after parsing. - XXX map[string]any `yaml:",inline"` -} - -// Config converts the Pre1127 config to the current config struct. -func (c *Pre1127) Config() *Config { - cfg := &Config{ - AutoClip: c.AutoClip, - AutoImport: c.AutoImport, - ClipTimeout: c.ClipTimeout, - ExportKeys: c.ExportKeys, - NoPager: c.NoPager, - Notifications: c.Notifications, - Parsing: c.Parsing, - Path: c.Path, - SafeContent: c.SafeContent, - Mounts: make(map[string]string, len(c.Mounts)), - } - - for k, v := range c.Mounts { - cfg.Mounts[k] = v - } - - return cfg -} - -// CheckOverflow implements configer. -func (c *Pre1127) CheckOverflow() error { - return checkOverflow(c.XXX) -} - -// Pre1102 is a pre-1.10.2 config. -type Pre1102 struct { - AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. - AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. - ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. - ExportKeys bool `yaml:"exportkeys"` // automatically export public keys of all recipients. - MIME bool `yaml:"mime"` // enable gopass native MIME secrets. - NoColor bool `yaml:"nocolor"` // do not use color when outputing text. - NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. - Notifications bool `yaml:"notifications"` // enable desktop notifications. - Path string `yaml:"path"` - SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. - Mounts map[string]string `yaml:"mounts"` - - // Catches all undefined files and must be empty after parsing. - XXX map[string]any `yaml:",inline"` -} - -// CheckOverflow implements configer. -func (c *Pre1102) CheckOverflow() error { - return checkOverflow(c.XXX) -} + "fmt" -// Config converts the Pre1102 config to the current config struct. -func (c *Pre1102) Config() *Config { - cfg := &Config{ - AutoClip: c.AutoClip, - AutoImport: c.AutoImport, - ClipTimeout: c.ClipTimeout, - ExportKeys: c.ExportKeys, - NoPager: c.NoPager, - Notifications: c.Notifications, - Parsing: true, - Path: c.Path, - SafeContent: c.SafeContent, - Mounts: make(map[string]string, len(c.Mounts)), - } - - for k, v := range c.Mounts { - cfg.Mounts[k] = v - } - - return cfg -} - -// Pre193 is is pre-1.9.3 config. -type Pre193 struct { - Path string `yaml:"-"` - Root *Pre193StoreConfig - Mounts map[string]*Pre193StoreConfig - - // Catches all undefined files and must be empty after parsing. - XXX map[string]any `yaml:",inline"` -} - -// Pre193StoreConfig is a pre-1.9.3 store config. -type Pre193StoreConfig struct { - AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. - AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. - AutoSync bool `yaml:"autosync"` // push to git remote after commit, pull before push if necessary. - CheckRecpHash bool `yaml:"check_recipient_hash"` - ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. - Concurrency int `yaml:"concurrency"` // allow to run multiple thread when batch processing. - EditRecipients bool `yaml:"editrecipients"` // edit recipients when confirming. - ExportKeys bool `yaml:"exportkeys"` // automatically export public keys of all recipients. - NoColor bool `yaml:"nocolor"` // do not use color when outputing text. - Confirm bool `yaml:"noconfirm"` // do not confirm recipients when encrypting. - NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. - Notifications bool `yaml:"notifications"` // enable desktop notifications. - Path string `yaml:"path"` // path to the root store. - RecipientHash map[string]string `yaml:"recipient_hash"` - SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. - UseSymbols bool `yaml:"usesymbols"` // always use symbols when generating passwords. -} + "github.com/gopasspw/gopass/internal/config/legacy" + "github.com/gopasspw/gopass/pkg/debug" +) -// CheckOverflow implements configer. -func (c *Pre193) CheckOverflow() error { - return checkOverflow(c.XXX) -} +func migrateConfigs() error { + cfg := legacy.LoadWithOptions(true, false) + if cfg == nil { + debug.Log("no legacy config found. not migrating.") -// Config converts the Pre193 config to the current config struct. -func (c *Pre193) Config() *Config { - cfg := &Config{ - AutoClip: c.Root.AutoClip, - AutoImport: c.Root.AutoImport, - ClipTimeout: c.Root.ClipTimeout, - ExportKeys: c.Root.ExportKeys, - NoPager: c.Root.NoPager, - Notifications: c.Root.Notifications, - Parsing: true, - Path: c.Root.Path, - SafeContent: c.Root.SafeContent, - Mounts: make(map[string]string, len(c.Mounts)), + return nil } - if p, err := pathFromURL(c.Root.Path); err == nil { - cfg.Path = p - } + c := newGitconfig().LoadAll(cfg.Path) - for k, v := range c.Mounts { - p, err := pathFromURL(v.Path) - if err != nil { - continue + for k, v := range cfg.ConfigMap() { + var fk string + switch k { + case "keychain": + fk = "age.usekeychain" + case "path": + fk = "mounts.path" + default: + fk = "core." + k } - cfg.Mounts[k] = p - } - - return cfg -} - -// Pre182 is the gopass config structure before version 1.8.2. -type Pre182 struct { - Path string `yaml:"-"` - Root *Pre182StoreConfig `yaml:"root"` - Mounts map[string]*Pre182StoreConfig `yaml:"mounts"` - Version string `yaml:"version"` - - // Catches all undefined files and must be empty after parsing. - XXX map[string]any `yaml:",inline"` -} - -// Pre182StoreConfig is a per-store (root or mount) config. -type Pre182StoreConfig struct { - AskForMore bool `yaml:"askformore"` // ask for more data on generate. - AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. - AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. - AutoSync bool `yaml:"autosync"` // push to git remote after commit, pull before push if necessary. - CheckRecpHash bool `yaml:"check_recipient_hash"` - ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. - Concurrency int `yaml:"concurrency"` // allow to run multiple thread when batch processing. - EditRecipients bool `yaml:"editrecipients"` // edit recipients when confirming. - NoColor bool `yaml:"nocolor"` // do not use color when outputing text. - Confirm bool `yaml:"noconfirm"` // do not confirm recipients when encrypting. - NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. - Notifications bool `yaml:"notifications"` // enable desktop notifications. - Path string `yaml:"path"` // path to the root store. - RecipientHash map[string]string `yaml:"recipient_hash"` - SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. - UseSymbols bool `yaml:"usesymbols"` // always use symbols when generating passwords. -} - -// CheckOverflow implements configer. -func (c *Pre182) CheckOverflow() error { - return checkOverflow(c.XXX) -} - -// Config converts the Pre182 config to the current config struct. -func (c *Pre182) Config() *Config { - cfg := &Config{ - AutoClip: c.Root.AutoClip, - AutoImport: c.Root.AutoImport, - ClipTimeout: c.Root.ClipTimeout, - ExportKeys: true, - NoPager: c.Root.NoPager, - Notifications: c.Root.Notifications, - Parsing: true, - Path: c.Root.Path, - SafeContent: c.Root.SafeContent, - Mounts: make(map[string]string, len(c.Mounts)), - } - - if p, err := pathFromURL(c.Root.Path); err == nil { - cfg.Path = p - } - for k, v := range c.Mounts { - p, err := pathFromURL(v.Path) - if err != nil { - continue + if err := c.SetGlobal(fk, v); err != nil { + return fmt.Errorf("failed to write new config: %w", err) } - cfg.Mounts[k] = p - } - - return cfg -} - -// Pre140 is the gopass config structure before version 1.4.0. -type Pre140 struct { - AskForMore bool `yaml:"askformore"` // ask for more data on generate. - AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. - AutoSync bool `yaml:"autosync"` // push to git remote after commit, pull before push if necessary. - ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. - Mounts map[string]string `yaml:"mounts,omitempty"` - Confirm bool `yaml:"noconfirm"` // do not confirm recipients when encrypting. - Path string `yaml:"path"` // path to the root store. - SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. - Version string `yaml:"version"` - - // Catches all undefined files and must be empty after parsing. - XXX map[string]any `yaml:",inline"` -} - -// CheckOverflow implements configer. -func (c *Pre140) CheckOverflow() error { - return checkOverflow(c.XXX) -} - -// Config converts the Pre140 config to the current config struct. -func (c *Pre140) Config() *Config { - cfg := &Config{ - AutoImport: c.AutoImport, - ClipTimeout: c.ClipTimeout, - ExportKeys: true, - Parsing: true, - Path: c.Path, - SafeContent: c.SafeContent, - Mounts: make(map[string]string, len(c.Mounts)), } - - for k, v := range c.Mounts { - cfg.Mounts[k] = v - } - - return cfg -} - -// Pre130 is the gopass config structure before version 1.3.0. Not all fields were. -// available between 1.0.0 and 1.3.0, but this struct should cover all of them. -type Pre130 struct { - AlwaysTrust bool `yaml:"alwaystrust"` // always trust public keys when encrypting. - AskForMore bool `yaml:"askformore"` // ask for more data on generate. - AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. - AutoPull bool `yaml:"autopull"` // pull from git before push. - AutoPush bool `yaml:"autopush"` // push to git remote after commit. - ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. - Debug bool `yaml:"debug"` // enable debug output. - LoadKeys bool `yaml:"loadkeys"` // load missing keys from store. - Mounts map[string]string `yaml:"mounts,omitempty"` - NoColor bool `yaml:"nocolor"` // disable colors in output. - Confirm bool `yaml:"noconfirm"` // do not confirm recipients when encrypting. - Path string `yaml:"path"` // path to the root store. - PersistKeys bool `yaml:"persistkeys"` // store recipient keys in store. - SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. - Version string `yaml:"version"` - - // Catches all undefined files and must be empty after parsing. - XXX map[string]any `yaml:",inline"` -} - -// CheckOverflow implements configer. -func (c *Pre130) CheckOverflow() error { - return checkOverflow(c.XXX) -} - -// Config converts the Pre130 config to the current config struct. -func (c *Pre130) Config() *Config { - cfg := &Config{ - AutoImport: c.AutoImport, - ClipTimeout: c.ClipTimeout, - ExportKeys: true, - Parsing: true, - Path: c.Path, - SafeContent: c.SafeContent, - Mounts: make(map[string]string, len(c.Mounts)), - } - - for k, v := range c.Mounts { - cfg.Mounts[k] = v - } - - return cfg -} - -func pathFromURL(u string) (string, error) { - if !strings.Contains(u, "://") { - return u, nil + for alias, path := range cfg.Mounts { + if err := c.SetGlobal(mpk(alias), path); err != nil { + return fmt.Errorf("failed to write new config: %w", err) + } } - up, err := url.Parse(u) - if err != nil { - return "", err - } + debug.Log("migrated legacy config from %s", cfg.ConfigPath) - return up.Path, nil + return nil } diff --git a/internal/config/legacy/config.go b/internal/config/legacy/config.go new file mode 100644 index 0000000000..a4c945f21a --- /dev/null +++ b/internal/config/legacy/config.go @@ -0,0 +1,153 @@ +package legacy + +import ( + "fmt" + "path/filepath" + "reflect" + "strconv" + "strings" +) + +var ( + // ErrConfigNotFound is returned on load if the config was not found. + ErrConfigNotFound = fmt.Errorf("config not found") + // ErrConfigNotParsed is returned on load if the config could not be decoded. + ErrConfigNotParsed = fmt.Errorf("config not parseable") +) + +// Config is the current config struct. +type Config struct { + AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. + AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. + ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. + ExportKeys bool `yaml:"exportkeys"` // automatically export public keys of all recipients. + NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. + Notifications bool `yaml:"notifications"` // enable desktop notifications. + Parsing bool `yaml:"parsing"` // allows to switch off all output parsing. + Path string `yaml:"path"` + SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. + Mounts map[string]string `yaml:"mounts"` + UseKeychain bool `yaml:"keychain"` // use OS keychain for age + + ConfigPath string `yaml:"-"` + + // Catches all undefined files and must be empty after parsing. + XXX map[string]any `yaml:",inline"` +} + +// New creates a new config with sane default values. +func New() *Config { + return &Config{ + AutoImport: false, + ClipTimeout: 45, + ExportKeys: true, + Mounts: make(map[string]string), + Notifications: true, + Parsing: true, + Path: PwStoreDir(""), + ConfigPath: configLocation(), + } +} + +// CheckOverflow implements configer. It will check for any extra config values not. +// handled by the current struct. +func (c *Config) CheckOverflow() error { + return checkOverflow(c.XXX) +} + +// Config will return a current config. +func (c *Config) Config() *Config { + return c +} + +// SetConfigValue will try to set the given key to the value in the config struct. +func (c *Config) SetConfigValue(key, value string) error { + if err := c.setConfigValue(key, value); err != nil { + return err + } + + return c.Save() +} + +// setConfigValue will try to set the given key to the value in the config struct. +func (c *Config) setConfigValue(key, value string) error { + value = strings.ToLower(value) + o := reflect.ValueOf(c).Elem() + for i := 0; i < o.NumField(); i++ { + jsonArg := o.Type().Field(i).Tag.Get("yaml") + if jsonArg == "" || jsonArg == "-" { + continue + } + if jsonArg != key { + continue + } + f := o.Field(i) + switch f.Kind() { //nolint:exhaustive + case reflect.String: + f.SetString(value) + + return nil + case reflect.Bool: + switch { + case value == "true" || value == "on": + f.SetBool(true) + + return nil + case value == "false" || value == "off": + f.SetBool(false) + + return nil + default: + return fmt.Errorf("not a bool: %s", value) + } + case reflect.Int: + iv, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("failed to convert %q to integer: %w", value, err) + } + f.SetInt(int64(iv)) + + return nil + default: + continue + } + } + + return fmt.Errorf("unknown config option %q", key) +} + +func (c *Config) String() string { + return fmt.Sprintf("%#v", c) +} + +// Directory returns the directory this config is using. +func (c *Config) Directory() string { + return filepath.Dir(c.Path) +} + +// ConfigMap returns a map of stringified config values for easy printing. +func (c *Config) ConfigMap() map[string]string { + m := make(map[string]string, 20) + o := reflect.ValueOf(c).Elem() + for i := 0; i < o.NumField(); i++ { + jsonArg := o.Type().Field(i).Tag.Get("yaml") + if jsonArg == "" || jsonArg == "-" { + continue + } + f := o.Field(i) + var strVal string + switch f.Kind() { //nolint:exhaustive + case reflect.String: + strVal = f.String() + case reflect.Bool: + strVal = fmt.Sprintf("%t", f.Bool()) + case reflect.Int: + strVal = fmt.Sprintf("%d", f.Int()) + default: + continue + } + m[jsonArg] = strVal + } + + return m +} diff --git a/internal/config/config_test.go b/internal/config/legacy/config_test.go similarity index 76% rename from internal/config/config_test.go rename to internal/config/legacy/config_test.go index 385c84664a..6d18cae186 100644 --- a/internal/config/config_test.go +++ b/internal/config/legacy/config_test.go @@ -1,4 +1,4 @@ -package config_test +package legacy_test import ( "os" @@ -7,37 +7,33 @@ import ( _ "github.com/gopasspw/gopass/internal/backend/crypto" _ "github.com/gopasspw/gopass/internal/backend/storage" - "github.com/gopasspw/gopass/internal/config" + "github.com/gopasspw/gopass/internal/config/legacy" "github.com/stretchr/testify/assert" ) -func TestHomedir(t *testing.T) { //nolint:paralleltest - assert.NotEqual(t, config.Homedir(), "") -} - func TestNewConfig(t *testing.T) { //nolint:paralleltest assert.NoError(t, os.Setenv("GOPASS_CONFIG", filepath.Join(os.TempDir(), ".gopass.yml"))) - cfg := config.New() + cfg := legacy.New() cs := cfg.String() - assert.Contains(t, cs, `&config.Config{AutoClip:false, AutoImport:false, ClipTimeout:45, ExportKeys:true, NoPager:false, Notifications:true,`) + assert.Contains(t, cs, `&legacy.Config{AutoClip:false, AutoImport:false, ClipTimeout:45, ExportKeys:true, NoPager:false, Notifications:true,`) assert.Contains(t, cs, `SafeContent:false, Mounts:map[string]string{},`) - cfg = &config.Config{ + cfg = &legacy.Config{ Mounts: map[string]string{ "foo": "", "bar": "", }, } cs = cfg.String() - assert.Contains(t, cs, `&config.Config{AutoClip:false, AutoImport:false, ClipTimeout:0, ExportKeys:false, NoPager:false, Notifications:false,`) + assert.Contains(t, cs, `&legacy.Config{AutoClip:false, AutoImport:false, ClipTimeout:0, ExportKeys:false, NoPager:false, Notifications:false,`) assert.Contains(t, cs, `SafeContent:false, Mounts:map[string]string{"bar":"", "foo":""},`) } func TestSetConfigValue(t *testing.T) { //nolint:paralleltest assert.NoError(t, os.Setenv("GOPASS_CONFIG", filepath.Join(os.TempDir(), ".gopass.yml"))) - cfg := config.New() + cfg := legacy.New() assert.NoError(t, cfg.SetConfigValue("autoclip", "true")) assert.NoError(t, cfg.SetConfigValue("cliptimeout", "900")) assert.NoError(t, cfg.SetConfigValue("path", "/tmp")) diff --git a/internal/config/io.go b/internal/config/legacy/io.go similarity index 93% rename from internal/config/io.go rename to internal/config/legacy/io.go index 47faa65e61..2a891e1fad 100644 --- a/internal/config/io.go +++ b/internal/config/legacy/io.go @@ -1,4 +1,4 @@ -package config +package legacy import ( "errors" @@ -16,21 +16,26 @@ import ( // LoadWithFallbackRelaxed will try to load the config from one of the default. // locations but also accept a more recent config. func LoadWithFallbackRelaxed() *Config { - return loadWithFallback(true) + return LoadWithOptions(true, true) } // LoadWithFallback will try to load the config from one of the default locations. func LoadWithFallback() *Config { - return loadWithFallback(false) + return LoadWithOptions(false, true) } -func loadWithFallback(relaxed bool) *Config { - for _, l := range configLocations() { +// LoadWithOptions gives more flexibility about how to load the config. +func LoadWithOptions(relaxed, useDefault bool) *Config { + for _, l := range ConfigLocations() { if cfg := loadConfig(l, relaxed); cfg != nil { return cfg } } + if !useDefault { + return nil + } + return loadDefault() } @@ -63,7 +68,7 @@ func loadConfig(l string, relaxed bool) *Config { func loadDefault() *Config { cfg := New() cfg.Path = PwStoreDir("") - debug.Log("Loaded default config: %+v", cfg) + debug.Log("Created new default config: %+v", cfg) return cfg } diff --git a/internal/config/io_test.go b/internal/config/legacy/io_test.go similarity index 99% rename from internal/config/io_test.go rename to internal/config/legacy/io_test.go index b0aa240671..c683a1ae7c 100644 --- a/internal/config/io_test.go +++ b/internal/config/legacy/io_test.go @@ -1,4 +1,4 @@ -package config +package legacy import ( "bytes" diff --git a/internal/config/legacy/legacy.go b/internal/config/legacy/legacy.go new file mode 100644 index 0000000000..346a665481 --- /dev/null +++ b/internal/config/legacy/legacy.go @@ -0,0 +1,329 @@ +package legacy + +import ( + "net/url" + "strings" +) + +// Pre1127 is a pre-1.12.7 config. +type Pre1127 struct { + AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. + AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. + ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. + ExportKeys bool `yaml:"exportkeys"` // automatically export public keys of all recipients. + NoColor bool `yaml:"nocolor"` // do not use color when outputing text. + NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. + Notifications bool `yaml:"notifications"` // enable desktop notifications. + Parsing bool `yaml:"parsing"` // allows to switch off all output parsing. + Path string `yaml:"path"` + SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. + Mounts map[string]string `yaml:"mounts"` + + ConfigPath string `yaml:"-"` + + // Catches all undefined files and must be empty after parsing. + XXX map[string]any `yaml:",inline"` +} + +// Config converts the Pre1127 config to the current config struct. +func (c *Pre1127) Config() *Config { + cfg := &Config{ + AutoClip: c.AutoClip, + AutoImport: c.AutoImport, + ClipTimeout: c.ClipTimeout, + ExportKeys: c.ExportKeys, + NoPager: c.NoPager, + Notifications: c.Notifications, + Parsing: c.Parsing, + Path: c.Path, + SafeContent: c.SafeContent, + Mounts: make(map[string]string, len(c.Mounts)), + } + + for k, v := range c.Mounts { + cfg.Mounts[k] = v + } + + return cfg +} + +// CheckOverflow implements configer. +func (c *Pre1127) CheckOverflow() error { + return checkOverflow(c.XXX) +} + +// Pre1102 is a pre-1.10.2 config. +type Pre1102 struct { + AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. + AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. + ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. + ExportKeys bool `yaml:"exportkeys"` // automatically export public keys of all recipients. + MIME bool `yaml:"mime"` // enable gopass native MIME secrets. + NoColor bool `yaml:"nocolor"` // do not use color when outputing text. + NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. + Notifications bool `yaml:"notifications"` // enable desktop notifications. + Path string `yaml:"path"` + SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. + Mounts map[string]string `yaml:"mounts"` + + // Catches all undefined files and must be empty after parsing. + XXX map[string]any `yaml:",inline"` +} + +// CheckOverflow implements configer. +func (c *Pre1102) CheckOverflow() error { + return checkOverflow(c.XXX) +} + +// Config converts the Pre1102 config to the current config struct. +func (c *Pre1102) Config() *Config { + cfg := &Config{ + AutoClip: c.AutoClip, + AutoImport: c.AutoImport, + ClipTimeout: c.ClipTimeout, + ExportKeys: c.ExportKeys, + NoPager: c.NoPager, + Notifications: c.Notifications, + Parsing: true, + Path: c.Path, + SafeContent: c.SafeContent, + Mounts: make(map[string]string, len(c.Mounts)), + } + + for k, v := range c.Mounts { + cfg.Mounts[k] = v + } + + return cfg +} + +// Pre193 is is pre-1.9.3 config. +type Pre193 struct { + Path string `yaml:"-"` + Root *Pre193StoreConfig + Mounts map[string]*Pre193StoreConfig + + // Catches all undefined files and must be empty after parsing. + XXX map[string]any `yaml:",inline"` +} + +// Pre193StoreConfig is a pre-1.9.3 store config. +type Pre193StoreConfig struct { + AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. + AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. + AutoSync bool `yaml:"autosync"` // push to git remote after commit, pull before push if necessary. + CheckRecpHash bool `yaml:"check_recipient_hash"` + ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. + Concurrency int `yaml:"concurrency"` // allow to run multiple thread when batch processing. + EditRecipients bool `yaml:"editrecipients"` // edit recipients when confirming. + ExportKeys bool `yaml:"exportkeys"` // automatically export public keys of all recipients. + NoColor bool `yaml:"nocolor"` // do not use color when outputing text. + Confirm bool `yaml:"noconfirm"` // do not confirm recipients when encrypting. + NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. + Notifications bool `yaml:"notifications"` // enable desktop notifications. + Path string `yaml:"path"` // path to the root store. + RecipientHash map[string]string `yaml:"recipient_hash"` + SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. + UseSymbols bool `yaml:"usesymbols"` // always use symbols when generating passwords. +} + +// CheckOverflow implements configer. +func (c *Pre193) CheckOverflow() error { + return checkOverflow(c.XXX) +} + +// Config converts the Pre193 config to the current config struct. +func (c *Pre193) Config() *Config { + cfg := &Config{ + AutoClip: c.Root.AutoClip, + AutoImport: c.Root.AutoImport, + ClipTimeout: c.Root.ClipTimeout, + ExportKeys: c.Root.ExportKeys, + NoPager: c.Root.NoPager, + Notifications: c.Root.Notifications, + Parsing: true, + Path: c.Root.Path, + SafeContent: c.Root.SafeContent, + Mounts: make(map[string]string, len(c.Mounts)), + } + + if p, err := pathFromURL(c.Root.Path); err == nil { + cfg.Path = p + } + + for k, v := range c.Mounts { + p, err := pathFromURL(v.Path) + if err != nil { + continue + } + cfg.Mounts[k] = p + } + + return cfg +} + +// Pre182 is the gopass config structure before version 1.8.2. +type Pre182 struct { + Path string `yaml:"-"` + Root *Pre182StoreConfig `yaml:"root"` + Mounts map[string]*Pre182StoreConfig `yaml:"mounts"` + Version string `yaml:"version"` + + // Catches all undefined files and must be empty after parsing. + XXX map[string]any `yaml:",inline"` +} + +// Pre182StoreConfig is a per-store (root or mount) config. +type Pre182StoreConfig struct { + AskForMore bool `yaml:"askformore"` // ask for more data on generate. + AutoClip bool `yaml:"autoclip"` // decide whether passwords are automatically copied or not. + AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. + AutoSync bool `yaml:"autosync"` // push to git remote after commit, pull before push if necessary. + CheckRecpHash bool `yaml:"check_recipient_hash"` + ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. + Concurrency int `yaml:"concurrency"` // allow to run multiple thread when batch processing. + EditRecipients bool `yaml:"editrecipients"` // edit recipients when confirming. + NoColor bool `yaml:"nocolor"` // do not use color when outputing text. + Confirm bool `yaml:"noconfirm"` // do not confirm recipients when encrypting. + NoPager bool `yaml:"nopager"` // do not invoke a pager to display long lists. + Notifications bool `yaml:"notifications"` // enable desktop notifications. + Path string `yaml:"path"` // path to the root store. + RecipientHash map[string]string `yaml:"recipient_hash"` + SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. + UseSymbols bool `yaml:"usesymbols"` // always use symbols when generating passwords. +} + +// CheckOverflow implements configer. +func (c *Pre182) CheckOverflow() error { + return checkOverflow(c.XXX) +} + +// Config converts the Pre182 config to the current config struct. +func (c *Pre182) Config() *Config { + cfg := &Config{ + AutoClip: c.Root.AutoClip, + AutoImport: c.Root.AutoImport, + ClipTimeout: c.Root.ClipTimeout, + ExportKeys: true, + NoPager: c.Root.NoPager, + Notifications: c.Root.Notifications, + Parsing: true, + Path: c.Root.Path, + SafeContent: c.Root.SafeContent, + Mounts: make(map[string]string, len(c.Mounts)), + } + + if p, err := pathFromURL(c.Root.Path); err == nil { + cfg.Path = p + } + + for k, v := range c.Mounts { + p, err := pathFromURL(v.Path) + if err != nil { + continue + } + cfg.Mounts[k] = p + } + + return cfg +} + +// Pre140 is the gopass config structure before version 1.4.0. +type Pre140 struct { + AskForMore bool `yaml:"askformore"` // ask for more data on generate. + AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. + AutoSync bool `yaml:"autosync"` // push to git remote after commit, pull before push if necessary. + ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. + Mounts map[string]string `yaml:"mounts,omitempty"` + Confirm bool `yaml:"noconfirm"` // do not confirm recipients when encrypting. + Path string `yaml:"path"` // path to the root store. + SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. + Version string `yaml:"version"` + + // Catches all undefined files and must be empty after parsing. + XXX map[string]any `yaml:",inline"` +} + +// CheckOverflow implements configer. +func (c *Pre140) CheckOverflow() error { + return checkOverflow(c.XXX) +} + +// Config converts the Pre140 config to the current config struct. +func (c *Pre140) Config() *Config { + cfg := &Config{ + AutoImport: c.AutoImport, + ClipTimeout: c.ClipTimeout, + ExportKeys: true, + Parsing: true, + Path: c.Path, + SafeContent: c.SafeContent, + Mounts: make(map[string]string, len(c.Mounts)), + } + + for k, v := range c.Mounts { + cfg.Mounts[k] = v + } + + return cfg +} + +// Pre130 is the gopass config structure before version 1.3.0. Not all fields were. +// available between 1.0.0 and 1.3.0, but this struct should cover all of them. +type Pre130 struct { + AlwaysTrust bool `yaml:"alwaystrust"` // always trust public keys when encrypting. + AskForMore bool `yaml:"askformore"` // ask for more data on generate. + AutoImport bool `yaml:"autoimport"` // import missing public keys w/o asking. + AutoPull bool `yaml:"autopull"` // pull from git before push. + AutoPush bool `yaml:"autopush"` // push to git remote after commit. + ClipTimeout int `yaml:"cliptimeout"` // clear clipboard after seconds. + Debug bool `yaml:"debug"` // enable debug output. + LoadKeys bool `yaml:"loadkeys"` // load missing keys from store. + Mounts map[string]string `yaml:"mounts,omitempty"` + NoColor bool `yaml:"nocolor"` // disable colors in output. + Confirm bool `yaml:"noconfirm"` // do not confirm recipients when encrypting. + Path string `yaml:"path"` // path to the root store. + PersistKeys bool `yaml:"persistkeys"` // store recipient keys in store. + SafeContent bool `yaml:"safecontent"` // avoid showing passwords in terminal. + Version string `yaml:"version"` + + // Catches all undefined files and must be empty after parsing. + XXX map[string]any `yaml:",inline"` +} + +// CheckOverflow implements configer. +func (c *Pre130) CheckOverflow() error { + return checkOverflow(c.XXX) +} + +// Config converts the Pre130 config to the current config struct. +func (c *Pre130) Config() *Config { + cfg := &Config{ + AutoImport: c.AutoImport, + ClipTimeout: c.ClipTimeout, + ExportKeys: true, + Parsing: true, + Path: c.Path, + SafeContent: c.SafeContent, + Mounts: make(map[string]string, len(c.Mounts)), + } + + for k, v := range c.Mounts { + cfg.Mounts[k] = v + } + + return cfg +} + +func pathFromURL(u string) (string, error) { + if !strings.Contains(u, "://") { + return u, nil + } + + up, err := url.Parse(u) + if err != nil { + return "", err + } + + return up.Path, nil +} diff --git a/internal/config/legacy/location.go b/internal/config/legacy/location.go new file mode 100644 index 0000000000..8399142721 --- /dev/null +++ b/internal/config/legacy/location.go @@ -0,0 +1,65 @@ +package legacy + +import ( + "os" + "path/filepath" + "strings" + + "github.com/gopasspw/gopass/pkg/appdir" + "github.com/gopasspw/gopass/pkg/debug" + "github.com/gopasspw/gopass/pkg/fsutil" +) + +// configLocation returns the location of the config file +// (a YAML file that contains values such as the path to the password store). +func configLocation() string { + // First, check for the "GOPASS_CONFIG" environment variable. + if cf := os.Getenv("GOPASS_CONFIG"); cf != "" { + return cf + } + + // Second, check for the "XDG_CONFIG_HOME" environment variable + // (which is part of the XDG Base Directory Specification for Linux and + // other Unix-like operating sytstems) + return filepath.Join(appdir.UserConfig(), "config.yml") +} + +// ConfigLocations returns the possible locations of gopass config files, +// in decreasing priority. +func ConfigLocations() []string { + l := []string{} + if cf := os.Getenv("GOPASS_CONFIG"); cf != "" { + l = append(l, cf) + } + l = append(l, filepath.Join(appdir.UserConfig(), "config.yml")) + l = append(l, filepath.Join(appdir.UserHome(), ".config", "gopass", "config.yml")) + l = append(l, filepath.Join(appdir.UserHome(), ".gopass.yml")) + + return l +} + +// PwStoreDir reads the password store dir from the environment +// or returns the default location if the env is not set. +func PwStoreDir(mount string) string { + if mount != "" { + cleanName := strings.ReplaceAll(mount, string(filepath.Separator), "-") + + return fsutil.CleanPath(filepath.Join(appdir.UserData(), "stores", cleanName)) + } + // PASSWORD_STORE_DIR support is discouraged. + if d := os.Getenv("PASSWORD_STORE_DIR"); d != "" { + if gh := os.Getenv("GOPASS_HOMEDIR"); gh == "" { + debug.Log("using value of PASSWORD_STORE_DIR: %s", d) + + return fsutil.CleanPath(d) + } + } + + if ld := filepath.Join(appdir.UserHome(), ".password-store"); fsutil.IsDir(ld) { + debug.Log("re-using existing legacy dir for root store: %s", ld) + + return ld + } + + return fsutil.CleanPath(filepath.Join(appdir.UserData(), "stores", "root")) +} diff --git a/internal/config/location.go b/internal/config/location.go index 98d0c380e5..d4e8988992 100644 --- a/internal/config/location.go +++ b/internal/config/location.go @@ -8,24 +8,8 @@ import ( "github.com/gopasspw/gopass/pkg/appdir" "github.com/gopasspw/gopass/pkg/debug" "github.com/gopasspw/gopass/pkg/fsutil" - homedir "github.com/mitchellh/go-homedir" ) -// Homedir returns the users home dir or an empty string if the lookup fails. -func Homedir() string { - if hd := os.Getenv("GOPASS_HOMEDIR"); hd != "" { - return hd - } - hd, err := homedir.Dir() - if err != nil { - debug.Log("Failed to get homedir: %s\n", err) - - return "" - } - - return hd -} - // configLocation returns the location of the config file // (a YAML file that contains values such as the path to the password store). func configLocation() string { @@ -48,8 +32,8 @@ func configLocations() []string { l = append(l, cf) } l = append(l, filepath.Join(appdir.UserConfig(), "config.yml")) - l = append(l, filepath.Join(Homedir(), ".config", "gopass", "config.yml")) - l = append(l, filepath.Join(Homedir(), ".gopass.yml")) + l = append(l, filepath.Join(appdir.UserHome(), ".config", "gopass", "config.yml")) + l = append(l, filepath.Join(appdir.UserHome(), ".gopass.yml")) return l } diff --git a/internal/config/location_test.go b/internal/config/location_test.go index 3ffe5793ee..b18343e8b5 100644 --- a/internal/config/location_test.go +++ b/internal/config/location_test.go @@ -5,6 +5,7 @@ import ( "runtime" "testing" + "github.com/gopasspw/gopass/pkg/appdir" "github.com/stretchr/testify/assert" ) @@ -13,12 +14,17 @@ func TestPwStoreDirNoEnv(t *testing.T) { //nolint:paralleltest t.Setenv("GOPASS_HOMEDIR", "/tmp") } + baseDir := filepath.Join(appdir.UserHome(), ".local", "share", "gopass", "stores") + if runtime.GOOS == "windows" { + baseDir = filepath.Join(appdir.UserHome(), "AppData", "Local", "gopass", "stores") + } + for in, out := range map[string]string{ - "": filepath.Join(Homedir(), ".local", "share", "gopass", "stores", "root"), - "work": filepath.Join(Homedir(), ".local", "share", "gopass", "stores", "work"), - filepath.Join("foo", "bar"): filepath.Join(Homedir(), ".local", "share", "gopass", "stores", "foo-bar"), + "": filepath.Join(baseDir, "root"), + "work": filepath.Join(baseDir, "work"), + filepath.Join("foo", "bar"): filepath.Join(baseDir, "foo-bar"), } { - assert.Equal(t, out, PwStoreDir(in), in) + assert.Equal(t, out, PwStoreDir(in), in, "mount "+in) } } diff --git a/internal/config/utils.go b/internal/config/utils.go new file mode 100644 index 0000000000..ea2f1e8371 --- /dev/null +++ b/internal/config/utils.go @@ -0,0 +1,18 @@ +package config + +import "context" + +// Bool returns a bool value from the config in the context. +func Bool(ctx context.Context, key string) bool { + return FromContext(ctx).GetBool(key) +} + +// String returns a string value from the config in the context. +func String(ctx context.Context, key string) string { + return FromContext(ctx).Get(key) +} + +// Int returns an integer value from the config in the context. +func Int(ctx context.Context, key string) int { + return FromContext(ctx).GetInt(key) +} diff --git a/internal/create/wizard.go b/internal/create/wizard.go index 11c52beef6..2f77c23816 100644 --- a/internal/create/wizard.go +++ b/internal/create/wizard.go @@ -204,7 +204,7 @@ func mkActFunc(tpl Template, s *root.Store, cb ActionCallback) func(context.Cont if wantForName[k] { nameParts = append(nameParts, hostname) } - if u := pwrules.LookupChangeURL(hostname); u != "" { + if u := pwrules.LookupChangeURL(ctx, hostname); u != "" { _ = sec.Set("password-change-url", u) } _ = sec.Set(k, sv) @@ -292,14 +292,14 @@ func generatePassword(ctx context.Context, hostname, charset string) (string, er return pwgen.GeneratePasswordCharset(length, charset), nil } - if _, found := pwrules.LookupRule(hostname); found { + if _, found := pwrules.LookupRule(ctx, hostname); found { out.Noticef(ctx, "Using password rules for %s ...", hostname) length, err := termio.AskForInt(ctx, fmtfn(4, "b", "How long?"), defaultLength) if err != nil { return "", err } - return pwgen.NewCrypticForDomain(length, hostname).Password(), nil + return pwgen.NewCrypticForDomain(ctx, length, hostname).Password(), nil } xkcd, err := termio.AskForBool(ctx, fmtfn(4, "a", "Human-pronounceable passphrase?"), false) if err != nil { diff --git a/internal/notify/notify_darwin.go b/internal/notify/notify_darwin.go index f86193677d..482c635911 100644 --- a/internal/notify/notify_darwin.go +++ b/internal/notify/notify_darwin.go @@ -8,7 +8,7 @@ import ( "os" "os/exec" - "github.com/gopasspw/gopass/pkg/ctxutil" + "github.com/gopasspw/gopass/internal/config" ) const ( @@ -23,7 +23,7 @@ var ( // Notify displays a desktop notification using osascript. func Notify(ctx context.Context, subj, msg string) error { - if os.Getenv("GOPASS_NO_NOTIFY") != "" || !ctxutil.IsNotifications(ctx) { + if os.Getenv("GOPASS_NO_NOTIFY") != "" || !config.Bool(ctx, "core.notifications") { return nil } diff --git a/internal/notify/notify_dbus.go b/internal/notify/notify_dbus.go index ac1cbaa3e7..89258b4b36 100644 --- a/internal/notify/notify_dbus.go +++ b/internal/notify/notify_dbus.go @@ -8,13 +8,13 @@ import ( "os" "github.com/godbus/dbus" - "github.com/gopasspw/gopass/pkg/ctxutil" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/pkg/debug" ) // Notify displays a desktop notification with dbus. func Notify(ctx context.Context, subj, msg string) error { - if os.Getenv("GOPASS_NO_NOTIFY") != "" || !ctxutil.IsNotifications(ctx) { + if os.Getenv("GOPASS_NO_NOTIFY") != "" || !config.Bool(ctx, "core.notifications") { debug.Log("Notifications disabled") return nil diff --git a/internal/notify/notify_windows.go b/internal/notify/notify_windows.go index 4aaf817d8a..38dadac1bc 100644 --- a/internal/notify/notify_windows.go +++ b/internal/notify/notify_windows.go @@ -8,12 +8,12 @@ import ( "os" "os/exec" - "github.com/gopasspw/gopass/pkg/ctxutil" + "github.com/gopasspw/gopass/internal/config" ) // Notify displays a desktop notification through msg func Notify(ctx context.Context, subj, msg string) error { - if os.Getenv("GOPASS_NO_NOTIFY") != "" || !ctxutil.IsNotifications(ctx) { + if os.Getenv("GOPASS_NO_NOTIFY") != "" || !config.Bool(ctx, "core.notifications") { return nil } winmsg, err := exec.LookPath("msg") diff --git a/internal/store/leaf/fsck_test.go b/internal/store/leaf/fsck_test.go index ffcfbe07d8..bd6149454b 100644 --- a/internal/store/leaf/fsck_test.go +++ b/internal/store/leaf/fsck_test.go @@ -9,7 +9,6 @@ import ( "github.com/gopasspw/gopass/internal/backend/crypto/plain" "github.com/gopasspw/gopass/internal/backend/storage/fs" "github.com/gopasspw/gopass/internal/out" - "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/gopasspw/gopass/pkg/gopass/secrets" "github.com/stretchr/testify/assert" ) @@ -18,7 +17,6 @@ func TestFsck(t *testing.T) { t.Parallel() ctx := context.Background() - ctx = ctxutil.WithExportKeys(ctx, false) obuf := &bytes.Buffer{} out.Stdout = obuf diff --git a/internal/store/leaf/list_test.go b/internal/store/leaf/list_test.go index 2eac1ff4e7..56160dc4fa 100644 --- a/internal/store/leaf/list_test.go +++ b/internal/store/leaf/list_test.go @@ -9,7 +9,6 @@ import ( plain "github.com/gopasspw/gopass/internal/backend/crypto/plain" "github.com/gopasspw/gopass/internal/backend/storage/fs" "github.com/gopasspw/gopass/internal/out" - "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/gopasspw/gopass/pkg/gopass/secrets" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,7 +18,6 @@ func TestList(t *testing.T) { t.Parallel() ctx := context.Background() - ctx = ctxutil.WithExportKeys(ctx, false) obuf := &bytes.Buffer{} out.Stdout = obuf diff --git a/internal/store/leaf/move_test.go b/internal/store/leaf/move_test.go index be001bf458..ad641b0637 100644 --- a/internal/store/leaf/move_test.go +++ b/internal/store/leaf/move_test.go @@ -9,7 +9,6 @@ import ( plain "github.com/gopasspw/gopass/internal/backend/crypto/plain" "github.com/gopasspw/gopass/internal/backend/storage/fs" "github.com/gopasspw/gopass/internal/out" - "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/gopasspw/gopass/pkg/gopass/secrets" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,7 +18,6 @@ func TestCopy(t *testing.T) { t.Parallel() ctx := context.Background() - ctx = ctxutil.WithExportKeys(ctx, false) obuf := &bytes.Buffer{} out.Stdout = obuf @@ -102,7 +100,6 @@ func TestMove(t *testing.T) { t.Parallel() ctx := context.Background() - ctx = ctxutil.WithExportKeys(ctx, false) obuf := &bytes.Buffer{} out.Stdout = obuf @@ -186,7 +183,6 @@ func TestDelete(t *testing.T) { t.Parallel() ctx := context.Background() - ctx = ctxutil.WithExportKeys(ctx, false) obuf := &bytes.Buffer{} out.Stdout = obuf @@ -252,7 +248,6 @@ func TestPrune(t *testing.T) { t.Parallel() ctx := context.Background() - ctx = ctxutil.WithExportKeys(ctx, false) obuf := &bytes.Buffer{} out.Stdout = obuf diff --git a/internal/store/leaf/read.go b/internal/store/leaf/read.go index fb646baee1..c8f444da38 100644 --- a/internal/store/leaf/read.go +++ b/internal/store/leaf/read.go @@ -3,6 +3,7 @@ package leaf import ( "context" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/internal/store" "github.com/gopasspw/gopass/pkg/ctxutil" @@ -30,7 +31,7 @@ func (s *Store) Get(ctx context.Context, name string) (gopass.Secret, error) { return nil, store.ErrDecrypt } - if !ctxutil.IsShowParsing(ctx) { + if !ctxutil.IsShowParsing(ctx) || !config.Bool(ctx, "core.parsing") { return secrets.ParsePlain(content), nil } diff --git a/internal/store/leaf/recipients.go b/internal/store/leaf/recipients.go index 8c1cb92969..59da122a3e 100644 --- a/internal/store/leaf/recipients.go +++ b/internal/store/leaf/recipients.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/google/go-cmp/cmp" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/internal/recipients" "github.com/gopasspw/gopass/internal/store" @@ -363,7 +364,7 @@ func (s *Store) saveRecipients(ctx context.Context, rs []string, msg string) err } // save all recipients public keys to the repo - if ctxutil.IsExportKeys(ctx) { + if config.Bool(ctx, "core.exportkeys") { debug.Log("updating exported keys") if _, err := s.UpdateExportedPublicKeys(ctx, rs); err != nil { out.Errorf(ctx, "Failed to export missing public keys: %s", err) diff --git a/internal/store/leaf/recipients_test.go b/internal/store/leaf/recipients_test.go index 5de97bfea4..a7d3d3c22b 100644 --- a/internal/store/leaf/recipients_test.go +++ b/internal/store/leaf/recipients_test.go @@ -89,7 +89,6 @@ func TestSaveRecipients(t *testing.T) { t.Parallel() ctx := context.Background() - ctx = ctxutil.WithExportKeys(ctx, true) tempdir := t.TempDir() diff --git a/internal/store/leaf/write.go b/internal/store/leaf/write.go index 50b840e264..2e3a465cbc 100644 --- a/internal/store/leaf/write.go +++ b/internal/store/leaf/write.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/internal/queue" "github.com/gopasspw/gopass/internal/store" @@ -20,6 +21,10 @@ func (s *Store) Set(ctx context.Context, name string, sec gopass.Byter) error { return fmt.Errorf("invalid secret name: %s", name) } + if config.FromContext(ctx).GetM(s.alias, "core.readonly") == "true" { + return fmt.Errorf("writing to %s is disabled by `core.readonly`.", s.alias) + } + p := s.Passfile(name) recipients, err := s.useableKeys(ctx, name) diff --git a/internal/store/root/convert.go b/internal/store/root/convert.go index 6d44243b67..e75551c6a8 100644 --- a/internal/store/root/convert.go +++ b/internal/store/root/convert.go @@ -24,11 +24,11 @@ func (r *Store) Convert(ctx context.Context, name string, cryptoBe backend.Crypt if name == "" { debug.Log("success. updating root path to %s", sub.Path()) - r.cfg.Path = sub.Path() - } else { - debug.Log("success. updating path for %s to %s", name, sub.Path()) - r.cfg.Mounts[name] = sub.Path() + + return r.cfg.Set("", "mounts.path", sub.Path()) } - return r.cfg.Save() + debug.Log("success. updating path for %s to %s", name, sub.Path()) + + return r.cfg.Set("", "mounts."+name+".path", sub.Path()) } diff --git a/internal/store/root/init.go b/internal/store/root/init.go index b1e24f53a5..b3df7cd9b0 100644 --- a/internal/store/root/init.go +++ b/internal/store/root/init.go @@ -54,15 +54,15 @@ func (r *Store) Init(ctx context.Context, alias, path string, ids ...string) err return fmt.Errorf("failed to initialize new sub store: %w", err) } - if alias == "" { - debug.Log("initialized root at %s", path) - r.cfg.Path = path - } else { + if alias != "" { debug.Log("mounted %s at %s", alias, path) - r.cfg.Mounts[alias] = path + + return r.cfg.SetMountPath(alias, path) } - return nil + debug.Log("initialized root at %s", path) + + return r.cfg.SetPath(path) } func (r *Store) initialize(ctx context.Context) error { @@ -72,7 +72,7 @@ func (r *Store) initialize(ctx context.Context) error { } // create the base store - path := fsutil.CleanPath(r.cfg.Path) + path := fsutil.CleanPath(r.cfg.Get("mounts.path")) if sv := os.Getenv("PASSWORD_STORE_DIR"); sv != "" { path = fsutil.CleanPath(sv) } @@ -80,7 +80,7 @@ func (r *Store) initialize(ctx context.Context) error { s, err := leaf.New(ctx, "", path) if err != nil { - return fmt.Errorf("failed to initialize the root store at %q: %w", r.cfg.Path, err) + return fmt.Errorf("failed to initialize the root store at %q: %w", r.cfg.Path(), err) } debug.Log("Root Store initialized at %s", path) @@ -88,8 +88,8 @@ func (r *Store) initialize(ctx context.Context) error { r.store = s // initialize all mounts - for alias, path := range r.cfg.Mounts { - path := fsutil.CleanPath(path) + for _, alias := range r.cfg.Mounts() { + path := fsutil.CleanPath(r.cfg.MountPath(alias)) if err := r.addMount(ctx, alias, path); err != nil { out.Errorf(ctx, "Failed to initialize mount %s (%s). Ignoring: %s", alias, path, err) diff --git a/internal/store/root/init_test.go b/internal/store/root/init_test.go index 8e28e4c745..014e254309 100644 --- a/internal/store/root/init_test.go +++ b/internal/store/root/init_test.go @@ -23,8 +23,8 @@ func TestInit(t *testing.T) { ctx = ctxutil.WithHidden(ctx, true) ctx = backend.WithCryptoBackend(ctx, backend.Plain) - cfg := config.New() - cfg.Path = u.StoreDir("rs") + cfg := config.NewNoWrites() + require.NoError(t, cfg.SetPath(u.StoreDir("rs"))) rs := New(cfg) inited, err := rs.IsInitialized(ctx) diff --git a/internal/store/root/mount.go b/internal/store/root/mount.go index 43f7ee0cb2..9b70bb044d 100644 --- a/internal/store/root/mount.go +++ b/internal/store/root/mount.go @@ -46,12 +46,10 @@ func (r *Store) addMount(ctx context.Context, alias, path string, keys ...string } r.mounts[alias] = s - if r.cfg.Mounts == nil { - r.cfg.Mounts = make(map[string]string, 1) + if err := r.cfg.SetMountPath(alias, path); err != nil { + return fmt.Errorf("failed to set mount path: %w", err) } - r.cfg.Mounts[alias] = path - debug.Log("Added mount %s -> %s (%s)", alias, path, fullPath) return nil @@ -102,7 +100,9 @@ func (r *Store) RemoveMount(ctx context.Context, alias string) error { } delete(r.mounts, alias) - delete(r.cfg.Mounts, alias) + if err := r.cfg.Unset("", "mounts."+alias+".path"); err != nil { + return err + } return nil } diff --git a/internal/store/root/store.go b/internal/store/root/store.go index 519a2419d9..2dcd3c2bf0 100644 --- a/internal/store/root/store.go +++ b/internal/store/root/store.go @@ -29,7 +29,7 @@ func New(cfg *config.Config) *Store { r := &Store{ cfg: cfg, - mounts: make(map[string]*leaf.Store, len(cfg.Mounts)), + mounts: make(map[string]*leaf.Store, len(cfg.Mounts())), } debug.Log("created store %s", r) @@ -37,9 +37,9 @@ func New(cfg *config.Config) *Store { return r } -// WithContext populates the context with the store config. -func (r *Store) WithContext(ctx context.Context) context.Context { - return r.cfg.WithContext(ctx) +// WithStoreConfig populates the context with the store config. +func (r *Store) WithStoreConfig(ctx context.Context) context.Context { + return r.cfg.WithConfig(ctx) } // Exists checks the existence of a single entry. diff --git a/internal/store/root/store_test.go b/internal/store/root/store_test.go index e6056d008f..c975f9158e 100644 --- a/internal/store/root/store_test.go +++ b/internal/store/root/store_test.go @@ -137,11 +137,11 @@ func TestListNested(t *testing.T) { func createRootStore(ctx context.Context, u *gptest.Unit) (*Store, error) { ctx = backend.WithCryptoBackendString(ctx, "plain") - s := New( - &config.Config{ - Path: u.StoreDir(""), - }, - ) + cfg := config.NewNoWrites() + if err := cfg.SetPath(u.StoreDir("")); err != nil { + return nil, err + } + s := New(cfg) if _, err := s.IsInitialized(ctx); err != nil { return nil, err diff --git a/main.go b/main.go index c4c7a70e0a..ccb412aabf 100644 --- a/main.go +++ b/main.go @@ -99,7 +99,7 @@ func main() { //nolint:wrapcheck func setupApp(ctx context.Context, sv semver.Version) (context.Context, *cli.App) { // try to read config (if it exists) - cfg := config.LoadWithFallback() + cfg := config.New() // set config values ctx = initContext(ctx, cfg) @@ -112,7 +112,7 @@ func setupApp(ctx context.Context, sv semver.Version) (context.Context, *cli.App } // set some action callbacks - if !cfg.AutoImport { + if !cfg.GetBool("core.autoimport") { ctx = ctxutil.WithImportFunc(ctx, termio.AskForKeyImport) } @@ -262,7 +262,7 @@ func (e errorWriter) Write(p []byte) (int, error) { func initContext(ctx context.Context, cfg *config.Config) context.Context { // initialize from config, may be overridden by env vars - ctx = cfg.WithContext(ctx) + ctx = cfg.WithConfig(ctx) // always trust ctx = gpg.WithAlwaysTrust(ctx, true) @@ -289,8 +289,14 @@ func initContext(ctx context.Context, cfg *config.Config) context.Context { // disable colored output on windows since cmd.exe doesn't support ANSI color // codes. Other terminal may do, but until we can figure that out better // disable this for all terms on this platform - if runtime.GOOS == "windows" { + if sv := os.Getenv("NO_COLOR"); runtime.GOOS == "windows" || sv == "true" { color.NoColor = true + } else { + // on all other platforms we should be able to use color. Only set + // this if it's in the config. + if cfg.IsSet("core.nocolor") { + color.NoColor = cfg.GetBool("core.nocolor") + } } return ctx diff --git a/main_test.go b/main_test.go index 26ed321e42..57f9a85626 100644 --- a/main_test.go +++ b/main_test.go @@ -21,6 +21,7 @@ import ( "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/gopasspw/gopass/tests/gptest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" ) @@ -111,8 +112,8 @@ func TestGetCommands(t *testing.T) { //nolint:paralleltest out.Stdout = os.Stdout }() - cfg := config.New() - cfg.Path = u.StoreDir("") + cfg := config.NewNoWrites() + require.NoError(t, cfg.SetPath(u.StoreDir(""))) clipboard.Unsupported = true @@ -177,7 +178,7 @@ func TestInitContext(t *testing.T) { t.Parallel() ctx := context.Background() - cfg := config.New() + cfg := config.NewNoWrites() ctx = initContext(ctx, cfg) assert.Equal(t, true, gpg.IsAlwaysTrust(ctx)) diff --git a/pkg/clipboard/clipboard_others.go b/pkg/clipboard/clipboard_others.go index 47dac1b1f3..ea41acc8d1 100644 --- a/pkg/clipboard/clipboard_others.go +++ b/pkg/clipboard/clipboard_others.go @@ -12,8 +12,8 @@ import ( "strconv" "syscall" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/pwschemes/argon2id" - "github.com/gopasspw/gopass/pkg/ctxutil" ) // clear will spawn a copy of gopass that waits in a detached background @@ -38,7 +38,7 @@ func clear(ctx context.Context, name string, content []byte, timeout int) error cmd.Env = append(os.Environ(), "GOPASS_UNCLIP_NAME="+name) cmd.Env = append(cmd.Env, "GOPASS_UNCLIP_CHECKSUM="+hash) - if !ctxutil.IsNotifications(ctx) { + if !config.Bool(ctx, "core.notifications") { cmd.Env = append(cmd.Env, "GOPASS_NO_NOTIFY=true") } diff --git a/pkg/clipboard/clipboard_windows.go b/pkg/clipboard/clipboard_windows.go index 2b7b7f7c31..c4404ca29d 100644 --- a/pkg/clipboard/clipboard_windows.go +++ b/pkg/clipboard/clipboard_windows.go @@ -9,8 +9,8 @@ import ( "os/exec" "strconv" + "github.com/gopasspw/gopass/internal/config" "github.com/gopasspw/gopass/internal/pwschemes/argon2id" - "github.com/gopasspw/gopass/pkg/ctxutil" ) // clear will spwan a copy of gopass that waits in a detached background @@ -26,7 +26,7 @@ func clear(ctx context.Context, name string, content []byte, timeout int) error cmd := exec.CommandContext(ctx, os.Args[0], "unclip", "--timeout", strconv.Itoa(timeout)) cmd.Env = append(os.Environ(), "GOPASS_UNCLIP_NAME="+name) cmd.Env = append(cmd.Env, "GOPASS_UNCLIP_CHECKSUM="+hash) - if !ctxutil.IsNotifications(ctx) { + if !config.Bool(ctx, "core.notifications") { cmd.Env = append(cmd.Env, "GOPASS_NO_NOTIFY=true") } return cmd.Start() diff --git a/pkg/ctxutil/ctxutil.go b/pkg/ctxutil/ctxutil.go index afc52589c0..e982f4b4d2 100644 --- a/pkg/ctxutil/ctxutil.go +++ b/pkg/ctxutil/ctxutil.go @@ -15,12 +15,9 @@ const ( ctxKeyTerminal contextKey = iota ctxKeyInteractive ctxKeyStdin - ctxKeyNoPager - ctxKeyShowSafeContent ctxKeyGitCommit ctxKeyAlwaysYes ctxKeyVerbose - ctxKeyNotifications ctxKeyProgressCallback ctxKeyAlias ctxKeyGitInit @@ -30,7 +27,6 @@ const ( ctxKeyUsername ctxKeyEmail ctxKeyImportFunc - ctxKeyExportKeys ctxKeyPasswordCallback ctxKeyPasswordPurgeCallback ctxKeyCommitTimestamp @@ -122,50 +118,6 @@ func IsStdin(ctx context.Context) bool { return bv } -// WithNoPager returns a context with the value for pager set. -func WithNoPager(ctx context.Context, bv bool) context.Context { - return context.WithValue(ctx, ctxKeyNoPager, bv) -} - -// HasNoPager returns true if a value for NoPager has been set in this context. -func HasNoPager(ctx context.Context) bool { - _, ok := ctx.Value(ctxKeyNoPager).(bool) - - return ok -} - -// IsNoPager returns the value of pager or the default (false). -func IsNoPager(ctx context.Context) bool { - bv, ok := ctx.Value(ctxKeyNoPager).(bool) - if !ok { - return false - } - - return bv -} - -// WithShowSafeContent returns a context with the value for ShowSafeContent set. -func WithShowSafeContent(ctx context.Context, bv bool) context.Context { - return context.WithValue(ctx, ctxKeyShowSafeContent, bv) -} - -// HasShowSafeContent returns true if a value for ShowSafeContent has been set in this context. -func HasShowSafeContent(ctx context.Context) bool { - _, ok := ctx.Value(ctxKeyShowSafeContent).(bool) - - return ok -} - -// IsShowSafeContent returns the value of ShowSafeContent or the default (false). -func IsShowSafeContent(ctx context.Context) bool { - bv, ok := ctx.Value(ctxKeyShowSafeContent).(bool) - if !ok { - return false - } - - return bv -} - // WithShowParsing returns a context with the value for ShowParsing set. func WithShowParsing(ctx context.Context, bv bool) context.Context { return context.WithValue(ctx, ctxKeyShowParsing, bv) @@ -244,21 +196,6 @@ func IsVerbose(ctx context.Context) bool { return is(ctx, ctxKeyVerbose, false) } -// WithNotifications returns a context with the value for Notifications set. -func WithNotifications(ctx context.Context, verbose bool) context.Context { - return context.WithValue(ctx, ctxKeyNotifications, verbose) -} - -// HasNotifications returns true if a value for Notifications has been set in this context. -func HasNotifications(ctx context.Context) bool { - return hasBool(ctx, ctxKeyNotifications) -} - -// IsNotifications returns the value of Notifications or the default (true). -func IsNotifications(ctx context.Context) bool { - return is(ctx, ctxKeyNotifications, true) -} - // WithProgressCallback returns a context with the value of ProgressCallback set. func WithProgressCallback(ctx context.Context, cb ProgressCallback) context.Context { return context.WithValue(ctx, ctxKeyProgressCallback, cb) @@ -423,21 +360,6 @@ func GetImportFunc(ctx context.Context) store.ImportCallback { return imf } -// WithExportKeys returns a context with the value for export keys set. -func WithExportKeys(ctx context.Context, d bool) context.Context { - return context.WithValue(ctx, ctxKeyExportKeys, d) -} - -// HasExportKeys returns true if Export Keys was set in the context. -func HasExportKeys(ctx context.Context) bool { - return hasBool(ctx, ctxKeyExportKeys) -} - -// IsExportKeys returns the value of export keys or the default (true). -func IsExportKeys(ctx context.Context) bool { - return is(ctx, ctxKeyExportKeys, true) -} - // PasswordCallback is a password prompt callback. type PasswordCallback func(string, bool) ([]byte, error) diff --git a/pkg/ctxutil/ctxutil_test.go b/pkg/ctxutil/ctxutil_test.go index ee91094fea..f851f0eea3 100644 --- a/pkg/ctxutil/ctxutil_test.go +++ b/pkg/ctxutil/ctxutil_test.go @@ -39,26 +39,6 @@ func TestStdin(t *testing.T) { assert.Equal(t, false, IsStdin(WithStdin(ctx, false))) } -func TestNoPager(t *testing.T) { - t.Parallel() - - ctx := context.Background() - - assert.Equal(t, false, IsNoPager(ctx)) - assert.Equal(t, true, IsNoPager(WithNoPager(ctx, true))) - assert.Equal(t, false, IsNoPager(WithNoPager(ctx, false))) -} - -func TestShowSafeContent(t *testing.T) { - t.Parallel() - - ctx := context.Background() - - assert.Equal(t, false, IsShowSafeContent(ctx)) - assert.Equal(t, true, IsShowSafeContent(WithShowSafeContent(ctx, true))) - assert.Equal(t, false, IsShowSafeContent(WithShowSafeContent(ctx, false))) -} - func TestGitCommit(t *testing.T) { t.Parallel() @@ -89,16 +69,6 @@ func TestVerbose(t *testing.T) { assert.Equal(t, false, IsVerbose(WithVerbose(ctx, false))) } -func TestNotifications(t *testing.T) { - t.Parallel() - - ctx := context.Background() - - assert.Equal(t, true, IsNotifications(ctx)) - assert.Equal(t, true, IsNotifications(WithNotifications(ctx, true))) - assert.Equal(t, false, IsNotifications(WithNotifications(ctx, false))) -} - func TestProgressCallback(t *testing.T) { t.Parallel() @@ -158,13 +128,9 @@ func TestComposite(t *testing.T) { ctx = WithTerminal(ctx, false) ctx = WithInteractive(ctx, false) ctx = WithStdin(ctx, true) - ctx = WithNoPager(ctx, true) - ctx = WithShowSafeContent(ctx, true) ctx = WithGitCommit(ctx, false) ctx = WithAlwaysYes(ctx, true) ctx = WithVerbose(ctx, true) - ctx = WithNotifications(ctx, true) - ctx = WithExportKeys(ctx, false) ctx = WithEmail(ctx, "foo@bar.com") ctx = WithUsername(ctx, "foo") ctx = WithNoNetwork(ctx, true) @@ -181,12 +147,6 @@ func TestComposite(t *testing.T) { assert.Equal(t, true, IsStdin(ctx)) assert.Equal(t, true, HasStdin(ctx)) - assert.Equal(t, true, IsNoPager(ctx)) - assert.Equal(t, true, HasNoPager(ctx)) - - assert.Equal(t, true, IsShowSafeContent(ctx)) - assert.Equal(t, true, HasShowSafeContent(ctx)) - assert.Equal(t, false, IsGitCommit(ctx)) assert.Equal(t, true, HasGitCommit(ctx)) @@ -196,12 +156,6 @@ func TestComposite(t *testing.T) { assert.Equal(t, true, IsVerbose(ctx)) assert.Equal(t, true, HasVerbose(ctx)) - assert.Equal(t, true, IsNotifications(ctx)) - assert.Equal(t, true, HasNotifications(ctx)) - - assert.Equal(t, false, IsExportKeys(ctx)) - assert.Equal(t, true, HasExportKeys(ctx)) - assert.Equal(t, "foo@bar.com", GetEmail(ctx)) assert.Equal(t, "foo", GetUsername(ctx)) diff --git a/pkg/gitconfig/config.go b/pkg/gitconfig/config.go new file mode 100644 index 0000000000..ad7791bdf7 --- /dev/null +++ b/pkg/gitconfig/config.go @@ -0,0 +1,379 @@ +package gitconfig + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/gopasspw/gopass/pkg/debug" +) + +var keyValueTpl = "\t%s = %s%s" + +// Config is a single parsed config file. It contains a reference of the input file, if any. +// It can only be populated only by reading the environment variables. +type Config struct { + path string + readonly bool // do not allow modifying values (even in memory) + noWrites bool // do not persist changes to disk (e.g. for tests) + raw strings.Builder + vars map[string]string +} + +// Unset deletes a key. +func (c *Config) Unset(key string) error { + if c.readonly { + return nil + } + + _, present := c.vars[key] + if !present { + return nil + } + + delete(c.vars, key) + + return c.rewriteRaw(key, "", func(fKey, key, value, comment string) (string, bool) { + return "", true + }) +} + +// IsSet returns true if the key was set in this config. +func (c *Config) IsSet(key string) bool { + _, present := c.vars[key] + + return present +} + +// Set updates or adds a key in the config. If possible it will also update the underlying +// config file on disk. +func (c *Config) Set(key, value string) error { + section, _, subkey := splitKey(key) + if section == "" || subkey == "" { + return fmt.Errorf("invalid key: %s", key) + } + + // can't set env vars + if c.readonly { + debug.Log("can not write to a readonly config") + + return nil + } + + if c.vars == nil { + c.vars = make(map[string]string, 16) + } + + // already present at the same value, no need to rewrite the config + if v, found := c.vars[key]; found && v == value { + debug.Log("key %q with value %q already present. No re-writing.", key, value) + + return nil + } + + _, present := c.vars[key] + c.vars[key] = value + + debug.Log("set %q to %q", key, value) + + // a new key, insert it into an existing section, if any + if !present { + debug.Log("inserting value") + + return c.insertValue(key, value) + } + + debug.Log("updating value") + + return c.rewriteRaw(key, value, func(fKey, sKey, value, comment string) (string, bool) { + return fmt.Sprintf(keyValueTpl, sKey, value, comment), false + }) +} + +func (c *Config) insertValue(key, value string) error { + debug.Log("input (%s: %s): ---------\n%s\n-----------\n", key, value, c.raw.String()) + + wSection, wSubsection, wKey := splitKey(key) + + s := bufio.NewScanner(strings.NewReader(c.raw.String())) + + lines := make([]string, 0, 128) + var section string + var subsection string + var written bool + for s.Scan() { + line := s.Text() + + lines = append(lines, line) + + if written { + continue + } + if strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, ";") { + continue + } + if strings.HasPrefix(line, "[") { + s, subs, skip := parseSectionHeader(line) + if skip { + continue + } + section = s + subsection = subs + } + + if section != wSection { + continue + } + if subsection != wSubsection { + continue + } + + lines = append(lines, fmt.Sprintf(keyValueTpl, wKey, value, "")) + written = true + } + + // not added to an existing section, so add it at the end + if !written { + sect := fmt.Sprintf("[%s]", wSection) + if wSubsection != "" { + sect = fmt.Sprintf("[%s \"%s\"]", wSection, wSubsection) + } + lines = append(lines, sect) + lines = append(lines, fmt.Sprintf(keyValueTpl, wKey, value, "")) + } + + c.raw = strings.Builder{} + c.raw.WriteString(strings.Join(lines, "\n")) + c.raw.WriteString("\n") + + debug.Log("output: ---------\n%s\n-----------\n", c.raw.String()) + + return c.flushRaw() +} + +func parseSectionHeader(line string) (section, subsection string, skip bool) { //nolint:nonamedreturns + line = strings.Trim(line, "[]") + if line == "" { + return "", "", true + } + wsp := strings.Index(line, " ") + if wsp < 0 { + return line, "", false + } + + section = line[:wsp] + subsection = line[wsp+1:] + subsection = strings.ReplaceAll(subsection, "\\", "") + subsection = strings.TrimPrefix(subsection, "\"") + subsection = strings.TrimSuffix(subsection, "\"") + + return section, subsection, false +} + +// rewriteRaw is used to rewrite the raw config copy. It is used for set and unset operations +// with different callbacks each. +func (c *Config) rewriteRaw(key, value string, cb parseFunc) error { + debug.Log("input (%s: %s): ---------\n%s\n-----------\n", key, value, c.raw.String()) + + lines := parseConfig(strings.NewReader(c.raw.String()), key, value, cb) + + c.raw = strings.Builder{} + c.raw.WriteString(strings.Join(lines, "\n")) + c.raw.WriteString("\n") + + debug.Log("output: ---------\n%s\n-----------\n", c.raw.String()) + + return c.flushRaw() +} + +func (c *Config) flushRaw() error { + if c.noWrites || c.path == "" { + debug.Log("not writing changes to disk (noWrites %t, path %q)", c.noWrites, c.path) + + return nil + } + + if err := os.MkdirAll(filepath.Dir(c.path), 0o700); err != nil { + return err + } + + debug.Log("writing config to %s: -----------\n%s\n--------------", c.path, c.raw.String()) + + return os.WriteFile(c.path, []byte(c.raw.String()), 0o600) +} + +type parseFunc func(fqkn, skn, value, comment string) (newLine string, skipLine bool) + +// parseConfig implements a simple parser for the gitconfig subset we support. +// The idea is to save all lines unaltered so we can reproduce the config +// almost exactly. Then we skip comments and extract section and subsection +// header. The next steps depend on the mode. Either we want to extract the +// values when loading (key and value empty, parseFunc adds the key-value pairs +// to the vars map), update a key (key is the target key, value the new value) +// or delete a key (parseFunc returns skip). +func parseConfig(in io.Reader, key, value string, cb parseFunc) []string { + wSection, wSubsection, wKey := splitKey(key) + + s := bufio.NewScanner(in) + + lines := make([]string, 0, 128) + var section string + var subsection string + for s.Scan() { + line := s.Text() + + lines = append(lines, line) + + if strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, ";") { + continue + } + if strings.HasPrefix(line, "[") { + s, subs, skip := parseSectionHeader(line) + if skip { + continue + } + section = s + subsection = subs + } + + if key != "" && (section != wSection && subsection != wSubsection) { + continue + } + + kvp := strings.Split(line, "=") + trim(kvp) + if len(kvp) < 2 { + continue + } + + fKey := section + "." + if subsection != "" { + fKey += subsection + "." + } + fKey += kvp[0] + if key == "" { + wKey = kvp[0] + } + + oValue := kvp[1] + comment := "" + + if strings.ContainsAny(oValue, "#;") { + comment = " " + oValue[strings.IndexAny(oValue, "#;"):] + oValue = oValue[:strings.IndexAny(oValue, "#;")] + oValue = strings.TrimSpace(oValue) + } + + if key != "" && (key != fKey) { + continue + } + if key != "" { + oValue = value + } + + newLine, skip := cb(fKey, wKey, oValue, comment) + if skip { + // remove the last line + lines = lines[:len(lines)-1] + + continue + } + lines[len(lines)-1] = newLine + } + + return lines +} + +// NewFromMap allows creating a new preset config from a map. +func NewFromMap(data map[string]string) *Config { + c := &Config{ + readonly: true, + vars: make(map[string]string, len(data)), + } + + for k, v := range data { + c.vars[k] = v + } + + return c +} + +// LoadConfig tries to load a gitconfig from the given path. +func LoadConfig(fn string) (*Config, error) { + fh, err := os.Open(fn) + if err != nil { + return nil, err + } + defer fh.Close() //nolint:errcheck + + c := ParseConfig(fh) + c.path = fn + + return c, nil +} + +// ParseConfig will try to parse a gitconfig from the given io.Reader. It never fails. +// Invalid configs will be silently rejceted. +func ParseConfig(r io.Reader) *Config { + c := &Config{ + vars: make(map[string]string, 42), + } + + lines := parseConfig(r, "", "", func(fk, k, v, comment string) (string, bool) { + c.vars[fk] = v + + return fmt.Sprintf(keyValueTpl, k, v, comment), false + }) + + c.raw.WriteString(strings.Join(lines, "\n")) + c.raw.WriteString("\n") + + debug.Log("processed config: %s\nvars: %+v", c.raw.String(), c.vars) + + return c +} + +// LoadConfigFromEnv will try to parse an overlay config from the environment variables. +// If no environment variables are set the resulting config will be valid but empty. +// Either way it will not be writeable. +func LoadConfigFromEnv(envPrefix string) *Config { + c := &Config{ + noWrites: true, + } + + count, err := strconv.Atoi(os.Getenv(envPrefix + "_CONFIG_COUNT")) + if err != nil || count < 1 { + return &Config{ + noWrites: true, + } + } + + for i := 0; i < count; i++ { + keyVar := fmt.Sprintf("%s%d", envPrefix+"_CONFIG_KEY_", i) + key := os.Getenv(keyVar) + + valVar := fmt.Sprintf("%s%d", envPrefix+"_CONFIG_VALUE_", i) + value, found := os.LookupEnv(valVar) + + if key == "" || !found { + return &Config{ + noWrites: true, + } + } + + c.vars[key] = value + debug.Log("added %s from env", key) + } + + return c +} diff --git a/pkg/gitconfig/config_test.go b/pkg/gitconfig/config_test.go new file mode 100644 index 0000000000..87217efe30 --- /dev/null +++ b/pkg/gitconfig/config_test.go @@ -0,0 +1,132 @@ +package gitconfig + +import ( + "strings" + "testing" + + "github.com/gopasspw/gopass/internal/set" + "github.com/stretchr/testify/assert" + "golang.org/x/exp/maps" +) + +func TestInsertOnce(t *testing.T) { + t.Parallel() + + c := &Config{ + noWrites: true, + } + + assert.NoError(t, c.insertValue("foo.bar", "baz")) + assert.Equal(t, `[foo] + bar = baz +`, c.raw.String()) +} + +func TestSubsection(t *testing.T) { + t.Parallel() + + in := `[core] + showsafecontent = true + parsing = false + readonly = true +[aliases "subsection with spaces"] + foo = bar +` + c := ParseConfig(strings.NewReader(in)) + c.noWrites = true + + assert.Equal(t, c.vars["aliases.subsection with spaces.foo"], "bar") +} + +func TestParseSection(t *testing.T) { + t.Parallel() + + for in, out := range map[string]struct { + section string + subs string + skip bool + }{ + `[aliases]`: { + section: "aliases", + }, + `[aliases "subsection"]`: { + section: "aliases", + subs: "subsection", + }, + `[aliases "subsection with spaces"]`: { + section: "aliases", + subs: "subsection with spaces", + }, + `[aliases "subsection with spaces and \" \t \0 escapes"]`: { + section: "aliases", + subs: `subsection with spaces and " t 0 escapes`, + }, + } { + section, subsection, skip := parseSectionHeader(in) + assert.Equal(t, out.section, section, in) + assert.Equal(t, out.subs, subsection, in) + assert.Equal(t, out.skip, skip, in) + } +} + +func TestInsertMultiple(t *testing.T) { + t.Parallel() + + c := &Config{ + noWrites: true, + } + + updates := map[string]string{ + "foo.bar": "baz", + "core.show": "true", + "core.noshow": "true", + } + + for _, k := range set.Sorted(maps.Keys(updates)) { + v := updates[k] + assert.NoError(t, c.insertValue(k, v)) + } + + assert.Equal(t, `[core] + show = true + noshow = true +[foo] + bar = baz +`, c.raw.String()) +} + +func TestRewriteRaw(t *testing.T) { + t.Parallel() + + in := `[core] + showsafecontent = true + parsing = false + readonly = true +[mounts] + path = /tmp/foo +` + c := ParseConfig(strings.NewReader(in)) + c.noWrites = true + + updates := map[string]string{ + "foo.bar": "baz", + "mounts.readonly": "true", + "core.showsafecontent": "false", + "core.parsing": "true", + } + for _, k := range set.Sorted(maps.Keys(updates)) { + v := updates[k] + assert.NoError(t, c.Set(k, v)) + } + + assert.Equal(t, `[core] + showsafecontent = false + parsing = true + readonly = true +[mounts] + readonly = true + path = /tmp/foo +[foo] + bar = baz +`, c.raw.String()) +} diff --git a/pkg/gitconfig/configs.go b/pkg/gitconfig/configs.go new file mode 100644 index 0000000000..db0556ef9b --- /dev/null +++ b/pkg/gitconfig/configs.go @@ -0,0 +1,363 @@ +package gitconfig + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/gopasspw/gopass/internal/set" + "github.com/gopasspw/gopass/pkg/appdir" + "github.com/gopasspw/gopass/pkg/debug" +) + +// Configs is a container for a config "view" that is composed of several different +// config objects. The intention is for the ones with a wider scope to provide defaults +// so those with a more narrow scope then only have to override what they are interested in. +type Configs struct { + Preset *Config + system *Config + global *Config + local *Config + worktree *Config + env *Config + workdir string + + SystemConfig string + GlobalConfig string + LocalConfig string + WorktreeConfig string + EnvPrefix string + NoWrites bool +} + +func New() *Configs { + return &Configs{ + system: &Config{ + readonly: true, + }, + global: &Config{ + path: globalConfigFile(), + }, + local: &Config{}, + worktree: &Config{}, + env: &Config{ + noWrites: true, + }, + + SystemConfig: systemConfig, + GlobalConfig: globalConfig, + LocalConfig: localConfig, + WorktreeConfig: worktreeConfig, + EnvPrefix: envPrefix, + } +} + +// Reload will reload the config(s) from disk. +func (cs *Configs) Reload() { + cs.LoadAll(cs.workdir) +} + +// LoadAll tries to load all known config files. Missing or invalid files are +// silently ignored. It never fails. The workdir is optional. If non-empty +// this method will try to load a local config from this location. +func (cs *Configs) LoadAll(workdir string) *Configs { + cs.workdir = workdir + + debug.Log("Loading gitconfigs for %+v ...", cs) + + // load the system config, if any + if os.Getenv(cs.EnvPrefix+"_NOSYSTEM") == "" { + c, err := LoadConfig(cs.SystemConfig) + if err != nil { + debug.Log("failed to load system config: %s", err) + } else { + debug.Log("loaded system config from %s", cs.SystemConfig) + cs.system = c + // the system config should generally not be written from gopass. + // in almost any scenario gopass shouldn't have write access + // and even if it does we shouldn't accidentially change it. + // It's for operators and package mainatiners. + cs.system.readonly = true + } + } + + // load the "global" (per user) config, if any + cs.loadGlobalConfigs() + cs.global.noWrites = cs.NoWrites + + // load the local config, if any + if workdir != "" { + localConfigPath := filepath.Join(workdir, cs.LocalConfig) + c, err := LoadConfig(localConfigPath) + if err != nil { + debug.Log("failed to load local config from %s: %s", localConfigPath, err) + // set the path just in case we want to modify / write to it later + cs.local.path = localConfigPath + } else { + debug.Log("loaded local config from %s", localConfigPath) + cs.local = c + } + } + cs.local.noWrites = cs.NoWrites + + // load the worktree config, if any + if workdir != "" { + worktreeConfigPath := filepath.Join(workdir, cs.WorktreeConfig) + c, err := LoadConfig(worktreeConfigPath) + if err != nil { + debug.Log("failed to load worktree config from %s: %s", worktreeConfigPath, err) + // set the path just in case we want to modify / write to it later + cs.worktree.path = worktreeConfigPath + } else { + debug.Log("loaded local config from %s", worktreeConfigPath) + cs.worktree = c + } + } + cs.worktree.noWrites = cs.NoWrites + + // load any env vars + cs.env = LoadConfigFromEnv(cs.EnvPrefix) + + return cs +} + +func globalConfigFile() string { + // $XDG_CONFIG_HOME/git/config + return filepath.Join(appdir.UserConfig(), "config") +} + +// loadGlobalConfigs will try to load the per-user (Git calls them "global") configs. +// Since we might need to try different locations but only want to use the first one +// it's easier to handle this in it's own method. +func (c *Configs) loadGlobalConfigs() string { + for _, p := range []string{ + globalConfigFile(), + // ~/.gitconfig + c.GlobalConfig, + } { + // GlobalConfig might be set to an empty string to disable it + // and instead of the XDG_CONFIG_HOME path only. + if p == "" { + continue + } + cfg, err := LoadConfig(p) + if err != nil { + debug.Log("failed to load global config from %s", p) + + continue + } + + debug.Log("loaded global config from %s", p) + c.global = cfg + + return p + } + + debug.Log("no global config found") + + // set the path in case we want to write to it (create it) later + c.global = &Config{ + path: globalConfigFile(), + } + + return "" +} + +// HasGlobalConfig indicates if a per-user config can be found. +func HasGlobalConfig() bool { + c := &Configs{} + + return c.loadGlobalConfigs() != "" +} + +// Get returns the value for the given key from the first location that is found. +// Lookup order: env, worktree, local, global, system and presets. +func (c *Configs) Get(key string) string { + for _, cfg := range []*Config{ + c.env, + c.worktree, + c.local, + c.global, + c.system, + c.Preset, + } { + if cfg == nil || cfg.vars == nil { + continue + } + if v, found := cfg.vars[key]; found { + return v + } + } + + debug.Log("no value for %s found", key) + + return "" +} + +// GetGlobal specifically ask the per-user (global) config for a key. +func (c *Configs) GetGlobal(key string) string { + if c.global == nil { + return "" + } + + if v, found := c.global.vars[key]; found { + return v + } + + debug.Log("no value for %s found", key) + + return "" +} + +// GetLocal specifically asks the per-directory (local) config for a key. +func (c *Configs) GetLocal(key string) string { + if c.local == nil { + return "" + } + + if v, found := c.local.vars[key]; found { + return v + } + + debug.Log("no value for %s found", key) + + return "" +} + +// IsSet returns true if this key is set in any of our configs. +func (c *Configs) IsSet(key string) bool { + for _, cfg := range []*Config{ + c.env, + c.worktree, + c.local, + c.global, + c.system, + c.Preset, + } { + if cfg.IsSet(key) { + return true + } + } + + return false +} + +// SetLocal sets (or adds) a key only in the per-directory (local) config. +func (c *Configs) SetLocal(key, value string) error { + if c.local == nil { + if c.workdir == "" { + return fmt.Errorf("no workdir set") + } + c.local = &Config{ + path: filepath.Join(c.workdir, c.LocalConfig), + } + } + + return c.local.Set(key, value) +} + +// SetGlobal sets (or adds) a key only in the per-user (global) config. +func (c *Configs) SetGlobal(key, value string) error { + if c.global == nil { + c.global = &Config{ + path: globalConfigFile(), + } + } + + return c.global.Set(key, value) +} + +// SetEnv sets (or adds) a key in the per-process (env) config. Useful +// for persisting flags during the invocation. +func (c *Configs) SetEnv(key, value string) error { + if c.env == nil { + c.env = &Config{ + noWrites: true, + } + } + + return c.env.Set(key, value) +} + +// UnsetLocal deletes a key from the local config. +func (c *Configs) UnsetLocal(key string) error { + if c.local == nil { + return nil + } + + return c.local.Unset(key) +} + +// UnsetGlobal delets a key from the global config. +func (c *Configs) UnsetGlobal(key string) error { + if c.global == nil { + return nil + } + + return c.global.Unset(key) +} + +// Keys returns a list of all keys from all available scopes. Every key has section and possibly +// a subsection. They are seprated by dots. The subsection itself may contain dots. The final +// key name and the section MUST NOT contain dots. +// +// Examples +// - remote.gist.gopass.pw.path -> section: remote, subsection: gist.gopass.pw, key: path +// - core.timeout -> section: core, key: timeout +func (c *Configs) Keys() []string { + keys := make([]string, 0, 128) + + for _, cfg := range []*Config{ + c.Preset, + c.system, + c.global, + c.local, + c.worktree, + c.env, + } { + if cfg == nil { + continue + } + for k := range cfg.vars { + keys = append(keys, k) + } + } + + return set.Sorted(keys) +} + +// List returns all keys matching the given prefix. The prefix can be empty, +// then this is identical to Keys(). +func (c *Configs) List(prefix string) []string { + return set.SortedFiltered(c.Keys(), func(k string) bool { + return strings.HasPrefix(k, prefix) + }) +} + +// ListSections returns a sorted list of all sections. +func (c *Configs) ListSections() []string { + return set.Sorted(set.Apply(c.Keys(), func(k string) string { + section, _, _ := splitKey(k) + + return section + })) +} + +// ListSubsections returns a sorted list of all subsections +// in the given section. +func (c *Configs) ListSubsections(wantSection string) []string { + // apply extracts the subsection and matches it to the empty string + // if it doesn't belong to the section we're looking for. Then the + // filter func filters out any empty string. + return set.SortedFiltered(set.Apply(c.Keys(), func(k string) string { + section, subsection, _ := splitKey(k) + if section != wantSection { + return "" + } + + return subsection + }), func(s string) bool { + return s != "" + }) +} diff --git a/pkg/gitconfig/doc.go b/pkg/gitconfig/doc.go new file mode 100644 index 0000000000..db67ab6320 --- /dev/null +++ b/pkg/gitconfig/doc.go @@ -0,0 +1,61 @@ +// Package gitconfig implements a pure Go parser of Git SCM config files. The support +// is currently not matching git exactly, e.g. includes, urlmatches and multivars are currently +// not supported. And while we try to preserve the original file a much as possible +// when writing we currently don't exactly retain (insignificant) whitespaces. +// +// The reference for this implementation is https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-config.html +// +// # Usage +// +// Use gitconfig.LoadAll with an optional workspace argument to process configuration +// input from these locations in order (i.e. the later ones take precedence): +// +// - `system` - /etc/gitconfig +// - `global` - `$XDG_CONFIG_HOME/git/config` or `~/.gitconfig` +// - `local` - `/config` +// - `worktree` - `/config.worktree` +// - `command` - GIT_CONFIG_{COUNT,KEY,VALUE} environment variables +// +// Note: We do not support parsing command line flags directly, but one +// can use the SetEnv method to set flags from the command line in the config. +// +// # Customization +// +// `gopass` and other users of this package can easily customize file and environment +// names by utilizing the exported variables from the Configs struct: +// +// - SystemConfig +// - GlobalConfig (can be set to the empty string to disable) +// - LocalConfig +// - WorktreeConfig +// - EnvPrefix +// +// Note: For tests users will want to set `NoWrites = true` to avoid overwriting +// their real configs. +// +// Example +// +// import "github.com/gopasspw/gopass/pkg/gitconfig" +// +// func main() { +// cfg := gitconfig.New() +// cfg.SystemConfig = "/etc/gopass/config" +// cfg.GlobalConfig = "" +// cfg.EnvPrefix = "GOPASS_CONFIG" +// cfg.LoadAll(".") +// _ = cfg.Get("core.parsing") +// } +// +// # Versioning and Compatibility +// +// We aim to support the latest stable release of Git only. +// Currently we do not provide any backwards compatibility +// and semantic versioning. Once this package has become +// mostly feature complete and if there is interest from +// other projects in using it we may choose to move it to +// it's own repository and start proper versioning. +// +// # Known limitations +// +// * Worktree support is only partial +package gitconfig diff --git a/pkg/gitconfig/gitconfig.go b/pkg/gitconfig/gitconfig.go new file mode 100644 index 0000000000..262c48bfd8 --- /dev/null +++ b/pkg/gitconfig/gitconfig.go @@ -0,0 +1,15 @@ +package gitconfig + +var ( + // SystemConfig is the location of the (optional) system-wide config defaults file. + systemConfig = "/etc/gitconfig" // /etc/gopass/config + // GlobalConfig is the location of the (optional) global (i.e. user-wide) config file. + globalConfig = ".gitconfig" + // LocalConfig is the name of the local (per-workdir) configuration. + localConfig = "config" + // WorktreeConfig is the name of the local worktree configuration. Can be used to override + // a committed local config. + worktreeConfig = "config.worktree" + // EnvPrefix is the prefix for the environment variables controlling and overriding config variables. + envPrefix = "GIT_CONFIG" +) diff --git a/pkg/gitconfig/gitconfig_test.go b/pkg/gitconfig/gitconfig_test.go new file mode 100644 index 0000000000..84d858cd44 --- /dev/null +++ b/pkg/gitconfig/gitconfig_test.go @@ -0,0 +1,424 @@ +package gitconfig + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-config.html#EXAMPLES +var configSampleDocs = `# +# This is the config file, and +# a '#' or ';' character indicates +# a comment +# + +; core variables +[core] + ; Don't trust file modes + filemode = false + +; Our diff algorithm +[diff] + external = /usr/local/bin/diff-wrapper + renames = true + +; Proxy settings +[core] + gitproxy = default-proxy ; default proxy + +; HTTP +[http] + sslVerify + +[http "https://weak.example.com"] + sslVerify = false + cookieFile = /tmp/cookie.txt +` + +var configSampleComplex = ` +[alias] + + # add + a = add # add + aa = add --all + all = add -A + chunkyadd = add --patch # stage commits chunk by chunk + + # branch + b = branch -v # branch (verbose) + branches = branch -a + recent = branch --sort=-committerdate + + # commit + c = commit -m + ca = commit -am + ci = commit + credit = "!f() { git commit --amend --author \"$1 <$2>\" -C HEAD; }; f" # Credit an author on the latest commit + credit = commit --amend --author "$1 <$2>" -C HEAD + amend = commit --amend + commend = commit --amend --no-edit + + # checkout + co = checkout # checkout + nb = checkout -b # create and switch to a new branch (mnemonic: "git new branch branchname...") + go = checkout -B # Switch to a branch, creating it if necessary + + # clone + cr = clone --recursive # Clone a repository including all submodules + + # cherry-pick + cp = cherry-pick -x # grab a change from a branch + + # diff + d = !"git diff-index --quiet HEAD -- || clear; git diff --patch-with-stat" # Show the diff between the latest commit and the current state + di = diff + dc = diff --cached + div = divergence # Divergence (commits we added and commits remote added) + ds = diff --stat=160,120 + gn = goodness # Goodness (summary of diff lines added/removed/total) + gnc = goodness --cached + last = diff HEAD^ + + # log + l = log --pretty=oneline -n 20 --graph # View the SHA, description, and history graph of the latest 20 commits + changes = log --pretty=format:\"%h %cr %cn %Cgreen%s%Creset\" --name-status + short = log --pretty=format:\"%h %cr %cn %Cgreen%s%Creset\" + changelog = log --pretty=format:\" * %s\" + shortnocolor = log --pretty=format:\"%h %cr %cn %s\" + show-graph = log --graph --abbrev-commit --pretty=oneline + + # pull + please = push --force-with-lease + pl = pull + fa = fetch --all + ff = merge --ff-only + noff = merge --no-ff + pullff = pull --ff-only + p = !"git pull; git submodule foreach git pull origin master" # Pull in remote changes for the current repository and all its submodules + pp = !"git pull ; git push origin master" + ppd = !"git pull origin develop ; git push origin develop" + mdm = !"git checkout master ; git pull origin master ; git push origin master ; git merge develop ; git push origin master ; git checkout develop" + + # push + ps = push + pom = pull origin master + pum = push origin master + + # rebase + rc = rebase --continue # continue rebase + rs = rebase --skip # skip rebase + reb = "!r() { git rebase -i HEAD~$1; }; r" # Interactive rebase with the given number of latest commits + + # remote + r = remote -v + remotes = remote -v + + # reset + unstage = reset HEAD # remove files from index (tracking) + uncommit = reset --soft HEAD^ # go back before last commit, with files in uncommitted state + undo = reset --soft HEAD^ + filelog = log -u # show changes to a file + mt = mergetool # fire up the merge tool + + # stash + ss = stash # stash changes + sl = stash list # list stashes + sa = stash apply # apply stash (restore changes) + sd = stash drop # drop stashes (destroy changes) + stsh = stash --keep-index + staash = stash --include-untracked + staaash = stahs --all + + # status + s = status -s # View the current working tree status using the short format + st = status # status + stat = status # status + shorty = status --short --branch + + # tag + t = tag -n # show tags with lines of each tag message + tags = tag -l # Show verbose output about tags, branches or remotes + + # init + it = !"git init && git commit -m "root" --allow-empty" + + # merge + merc = merge --no-ff + grog = log --graph --abbrev-commit --decorate --all --format=format:\"%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(dim white) - %an%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n %C(white)%s%C(reset)\" + +[apply] + # Detect whitespace errors when applying a patch + whitespace = fix + +[branch] + autosetupmerge = true + +[core] + editor = vim + excludesfile = ~/.gitignore + attributesfile = ~/.gitattributes + # Treat spaces before tabs, lines that are indented with 8 or more spaces, and all kinds of trailing whitespace as an error + whitespace = space-before-tab,indent-with-non-tab,trailing-space + autocrlf = input + protectHFS = true + protectNTFS = true + +[receive] + fsckObjects = true + quotepath = false + +[color] + # Use colors in Git commands that are capable of colored output when outputting to the terminal + ui = auto + +[diff] + +[format] + pretty = format:%C(blue)%ad%Creset %C(yellow)%h%C(green)%d%Creset %C(blue)%s %C(magenta) [%an]%Creset + +[mergetool] + prompt = false + +#[mergetool "mvimdiff"] +# cmd="mvim -c 'Gdiff' $MERGED" # use fugitive.vim for 3-way merge +# keepbackup=false + +[merge] + # Include summaries of merged commits in newly created merge commit messages + log = true + summary = true + verbosity = 1 +# tool = mvimdiff + +# Use origin as the default remote on the master branch in all cases +[branch "master \""] + remote = origin + merge = refs/heads/master + +[github] + user = johndoe + +# URL shorthands +[url "git@github.com:"] + insteadOf = "gh:" + pushInsteadOf = "github:" + pushInsteadOf = "git://github.com/" + insteadOf = https://github.com + +[url "git://github.com/"] + insteadOf = "github:" + +[url "git@gist.github.com:"] + insteadOf = "gst:" + pushInsteadOf = "gist:" + pushInsteadOf = "git://gist.github.com/" + +[url "git://gist.github.com/"] + insteadOf = "gist:" + +[url "git@gitlab.com:"] + insteadOf = https://gitlab.com/ + +[url "git@gitlab.com:"] + insteadOf = http://gitlab.com/ + +[user] + email = john.doe@gmail.com + name = John Doe + signingkey = DEADBEEF + +[push] + default = simple + +[gc] + auto = 64 + autopacklimit = 64 +[pull] + rebase = false +[init] + defaultBranch = master +[fetch] + prune = true + +[credential] + helper = osxkeychain + +` + +var configSampleGopass = ` +# This is a gopass config file + +[core] + autoclip = true + autoimport = true + cliptimeout = 45 + editor = vim + exportkeys = true + pager = false + notifications = true + showsafecontent = false + +[mounts] + path = /home/johndoe/.password-store + +[mounts "foo/sub"] + path = /home/johndoe/.password-store-foo-sub + +[mounts "work"] + path = /home/johndoe/.password-store-work +` + +func TestGopass(t *testing.T) { + t.Parallel() + + c := &Configs{ + global: ParseConfig(strings.NewReader(configSampleGopass)), + } + c.global.noWrites = true + + assert.Equal(t, "true", c.Get("core.autoclip")) + assert.Equal(t, "true", c.Get("core.autoimport")) + assert.Equal(t, "45", c.Get("core.cliptimeout")) + assert.Equal(t, "vim", c.Get("core.editor")) + assert.Equal(t, "true", c.Get("core.exportkeys")) + assert.Equal(t, "false", c.Get("core.pager")) + assert.Equal(t, "true", c.Get("core.notifications")) + assert.Equal(t, "false", c.Get("core.showsafecontent")) + + assert.Equal(t, "/home/johndoe/.password-store", c.Get("mounts.path")) + assert.Equal(t, "/home/johndoe/.password-store-foo-sub", c.Get("mounts.foo/sub.path")) + assert.Equal(t, "/home/johndoe/.password-store-work", c.Get("mounts.work.path")) + + t.Logf("Raw:\n%s\n", c.global.raw.String()) + t.Logf("Vars:\n%+v\n", c.global.vars) +} + +func TestParse(t *testing.T) { + t.Parallel() + + c := ParseConfig(strings.NewReader(configSampleDocs)) + + for k, v := range c.vars { + t.Logf("%s => %s\n", k, v) + } +} + +func TestGitBinary(t *testing.T) { //nolint:paralleltest + t.Skip("not ready, yet") // TODO(gitconfig) make tests pass + + cfgs := New() + cfgs.LoadAll(".") + + cmd := exec.Command("git", "config", "--list") + buf, err := cmd.Output() + require.NoError(t, err) + lines := strings.Split(string(buf), "\n") + for _, line := range lines { + p := strings.SplitN(line, "=", 2) + if len(p) < 2 { + continue + } + key := p[0] + want := p[1] + + assert.Equal(t, want, cfgs.Get(key), key) + } +} + +func TestSet(t *testing.T) { + t.Parallel() + + c := ParseConfig(strings.NewReader(configSampleDocs)) + c.noWrites = true + require.NoError(t, c.Set("core.gitproxy", "foobar")) + want := strings.ReplaceAll(configSampleDocs, "default-proxy", "foobar") + assert.Equal(t, want, c.raw.String()) +} + +func TestUnset(t *testing.T) { + t.Parallel() + + c := ParseConfig(strings.NewReader(configSampleDocs)) + c.noWrites = true + require.NoError(t, c.Unset("core.filemode")) + want := `# +# This is the config file, and +# a '#' or ';' character indicates +# a comment +# + +; core variables +[core] + ; Don't trust file modes + +; Our diff algorithm +[diff] + external = /usr/local/bin/diff-wrapper + renames = true + +; Proxy settings +[core] + gitproxy = default-proxy ; default proxy + +; HTTP +[http] + sslVerify + +[http "https://weak.example.com"] + sslVerify = false + cookieFile = /tmp/cookie.txt +` + assert.Equal(t, want, c.raw.String()) +} + +func TestSetEmptyConfig(t *testing.T) { + t.Parallel() + + td := t.TempDir() + c := &Config{ + path: filepath.Join(td, "config"), + noWrites: false, + } + assert.Error(t, c.Set("foobar", "baz")) + assert.NoError(t, c.Set("foo.bar", "baz")) + assert.Equal(t, "baz", c.vars["foo.bar"]) + buf, err := os.ReadFile(c.path) + require.NoError(t, err) + assert.Equal(t, "[foo]\n\tbar = baz\n", string(buf)) +} + +func TestList(t *testing.T) { + t.Parallel() + + c := &Configs{ + global: ParseConfig(strings.NewReader(configSampleGopass)), + } + c.global.noWrites = true + assert.Equal(t, []string{ + "mounts.foo/sub.path", + "mounts.path", + "mounts.work.path", + }, c.List("mounts.")) +} + +func TestListSections(t *testing.T) { + t.Parallel() + + c := &Configs{global: ParseConfig(strings.NewReader(configSampleGopass))} + c.global.noWrites = true + assert.Equal(t, []string{"core", "mounts"}, c.ListSections()) +} + +func TestListSubsections(t *testing.T) { + t.Parallel() + + c := &Configs{global: ParseConfig(strings.NewReader(configSampleGopass))} + c.global.noWrites = true + assert.Equal(t, []string{"foo/sub", "work"}, c.ListSubsections("mounts")) +} diff --git a/pkg/gitconfig/utils.go b/pkg/gitconfig/utils.go new file mode 100644 index 0000000000..e3f36c43c3 --- /dev/null +++ b/pkg/gitconfig/utils.go @@ -0,0 +1,35 @@ +package gitconfig + +import "strings" + +// splitKey splits a fully qualified gitconfig key into two or three parts. +// A valid key consists of either a section and a key separated by a dot +// or section, subsection and key, all separated by a dot. Note that +// the subsection might contain dots itself. +// +// Valid examples: +// - core.push +// - insteadof.git@github.com.push. +func splitKey(key string) (section, subsection, skey string) { //nolint:nonamedreturns + n := strings.Index(key, ".") + if n > 0 { + section = key[:n] + } + + if m := strings.LastIndex(key, "."); n != m && m > 0 && len(key) > m+1 { + subsection = key[n+1 : m] + skey = key[m+1:] + + return + } + + skey = key[n+1:] + + return +} + +func trim(s []string) { + for i, e := range s { + s[i] = strings.TrimSpace(e) + } +} diff --git a/pkg/gitconfig/utils_test.go b/pkg/gitconfig/utils_test.go new file mode 100644 index 0000000000..471db07019 --- /dev/null +++ b/pkg/gitconfig/utils_test.go @@ -0,0 +1,49 @@ +package gitconfig + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTrim(t *testing.T) { + t.Parallel() + + for _, tc := range [][]string{ + {" a ", "b ", "\tc\n"}, + } { + trim(tc) + for _, e := range tc { + assert.Equal(t, strings.TrimSpace(e), e) + } + } +} + +func TestSplitKey(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + in string + section string + subsection string + key string + }{ + { + in: "url.git@gist.github.com:.pushinsteadof", + section: "url", + subsection: "git@gist.github.com:", + key: "pushinsteadof", + }, + { + in: "gc.auto", + section: "gc", + key: "auto", + }, + } { + sec, sub, key := splitKey(tc.in) + assert.Equal(t, tc.section, sec, sec) + assert.Equal(t, tc.subsection, sub, sub) + assert.Equal(t, tc.key, key, key) + } +} diff --git a/pkg/gopass/api/api.go b/pkg/gopass/api/api.go index ebf3f55cb5..c820594f3f 100644 --- a/pkg/gopass/api/api.go +++ b/pkg/gopass/api/api.go @@ -35,7 +35,7 @@ var ErrNotInitialized = fmt.Errorf("password store not initialized. run 'gopass // // WARNING: This will need to change to accommodate for runtime configuration. func New(ctx context.Context) (*Gopass, error) { - cfg := config.LoadWithFallbackRelaxed() + cfg := config.New() store := root.New(cfg) initialized, err := store.IsInitialized(ctx) diff --git a/pkg/pwgen/cryptic.go b/pkg/pwgen/cryptic.go index dc66c9a4a9..2d34a92b72 100644 --- a/pkg/pwgen/cryptic.go +++ b/pkg/pwgen/cryptic.go @@ -2,6 +2,7 @@ package pwgen import ( "bytes" + "context" "fmt" "sort" "strings" @@ -44,9 +45,9 @@ func NewCryptic(length int, symbols bool) *Cryptic { // NewCrypticForDomain tries to look up password rules for the given domain // or uses the default generator. -func NewCrypticForDomain(length int, domain string) *Cryptic { +func NewCrypticForDomain(ctx context.Context, length int, domain string) *Cryptic { c := NewCryptic(length, true) - r, found := pwrules.LookupRule(domain) + r, found := pwrules.LookupRule(ctx, domain) debug.Log("found rules for %s: %t", domain, found) diff --git a/pkg/pwgen/cryptic_test.go b/pkg/pwgen/cryptic_test.go index df714ee7d3..60e3b2a87d 100644 --- a/pkg/pwgen/cryptic_test.go +++ b/pkg/pwgen/cryptic_test.go @@ -1,6 +1,7 @@ package pwgen import ( + "context" "fmt" "sort" "testing" @@ -28,7 +29,7 @@ func TestCrypticForDomain(t *testing.T) { for _, length := range []int{1, 4, 8, 100} { tcName := fmt.Sprintf("%s - %d", domain, length) - c := NewCrypticForDomain(length, domain) + c := NewCrypticForDomain(context.Background(), length, domain) require.NotNil(t, c, tcName) diff --git a/pkg/pwgen/pwrules/aliases.go b/pkg/pwgen/pwrules/aliases.go index 582b6bbab1..4feea35b90 100644 --- a/pkg/pwgen/pwrules/aliases.go +++ b/pkg/pwgen/pwrules/aliases.go @@ -1,21 +1,21 @@ package pwrules import ( - "encoding/json" - "fmt" - "os" - "path/filepath" + "context" "sort" + "strings" - "github.com/gopasspw/gopass/pkg/appdir" - "github.com/gopasspw/gopass/pkg/debug" - "github.com/gopasspw/gopass/pkg/fsutil" + "github.com/gopasspw/gopass/internal/config" + "github.com/gopasspw/gopass/internal/set" ) -var customAliases = map[string][]string{} +var customAliases map[string][]string // LookupAliases looks up known aliases for the given domain. -func LookupAliases(domain string) []string { +func LookupAliases(ctx context.Context, domain string) []string { + if customAliases == nil { + _ = loadCustomAliases(ctx) + } aliases := make([]string, 0, len(genAliases[domain])+len(customAliases[domain])) aliases = append(aliases, genAliases[domain]...) aliases = append(aliases, customAliases[domain]...) @@ -25,7 +25,10 @@ func LookupAliases(domain string) []string { } // AllAliases returns all aliases. -func AllAliases() map[string][]string { +func AllAliases(ctx context.Context) map[string][]string { + if customAliases == nil { + _ = loadCustomAliases(ctx) + } all := make(map[string][]string, len(genAliases)+len(customAliases)) for k, v := range genAliases { all[k] = append(all[k], v...) @@ -38,123 +41,23 @@ func AllAliases() map[string][]string { return all } -func init() { - if err := loadCustomAliases(); err != nil { - debug.Log("failed to load custom aliases: %s", err) - } -} - -func filename() string { - return filepath.Join(appdir.UserConfig(), "domain-aliases.json") -} - -func loadCustomAliases() error { - fn := filename() - - if !fsutil.IsFile(fn) { - debug.Log("no custom aliases found at %s", fn) - - return nil - } - - fh, err := os.Open(fn) - if err != nil { - return fmt.Errorf("failed to open %s for reading: %w", fn, err) - } - - defer func() { - _ = fh.Close() - }() - - if err := json.NewDecoder(fh).Decode(&customAliases); err != nil { - return fmt.Errorf("failed to decode custom aliases: %w", err) - } - - return nil -} - -func saveCustomAliases() error { - fn := filename() - - dir := filepath.Dir(fn) - if err := os.MkdirAll(dir, 0o700); err != nil { - return fmt.Errorf("failed to create directory %s: %w", dir, err) - } - - fh, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE, 0o600) - if err != nil { - return fmt.Errorf("failed to open %s for writing: %w", fn, err) - } - - defer func() { - _ = fh.Close() - }() - - if err := json.NewEncoder(fh).Encode(customAliases); err != nil { - return fmt.Errorf("failed to encode custom aliases: %w", err) - } - - return nil -} - -// AddCustomAlias adds a custom alias. -func AddCustomAlias(domain, alias string) error { - if len(customAliases) < 1 { - _ = loadCustomAliases() - } - - v := make([]string, 0, 1) - - if ev, found := customAliases[domain]; found { - v = ev - } - - for _, k := range v { - if k == alias { - return nil - } - } - - v = append(v, alias) - sort.Strings(v) - customAliases[domain] = v - - return saveCustomAliases() -} - -// RemoveCustomAlias removes a custom alias. -func RemoveCustomAlias(domain, alias string) error { - if len(customAliases) < 1 { - _ = loadCustomAliases() - } +func loadCustomAliases(ctx context.Context) error { + customAliases = make(map[string][]string, 128) + for _, k := range set.SortedFiltered(config.FromContext(ctx).Keys(""), func(k string) bool { + return strings.HasPrefix(k, "domain-alias.") + }) { + from := config.String(ctx, k) + to := strings.TrimPrefix(k, "domain-alias.") + if e, found := customAliases[from]; found { + e = append(e, to) + sort.Strings(e) + customAliases[from] = e - ev, found := customAliases[domain] - if !found { - return nil - } - - nv := make([]string, 0, len(ev)-1) - - for _, a := range ev { - if alias == a { continue } - nv = append(nv, a) + customAliases[from] = []string{to} } - customAliases[domain] = nv - - return saveCustomAliases() -} - -// DeleteCustomAlias removes a whole domain. -func DeleteCustomAlias(domain string) error { - if len(customAliases) < 1 { - _ = loadCustomAliases() - } - - delete(customAliases, domain) - - return saveCustomAliases() + return nil } diff --git a/pkg/pwgen/pwrules/change.go b/pkg/pwgen/pwrules/change.go index fe8c99efec..06a165b6da 100644 --- a/pkg/pwgen/pwrules/change.go +++ b/pkg/pwgen/pwrules/change.go @@ -1,5 +1,7 @@ package pwrules +import "context" + var changeURLs = map[string]string{} func init() { @@ -15,12 +17,12 @@ func init() { // LookupChangeURL looks up a change URL, either directly or through // one of it's know aliases. -func LookupChangeURL(domain string) string { +func LookupChangeURL(ctx context.Context, domain string) string { if u, found := changeURLs[domain]; found { return u } - for _, alias := range LookupAliases(domain) { + for _, alias := range LookupAliases(ctx, domain) { if u, found := changeURLs[alias]; found { return u } diff --git a/pkg/pwgen/pwrules/pwrules.go b/pkg/pwgen/pwrules/pwrules.go index 0de8764836..88f51dd35d 100644 --- a/pkg/pwgen/pwrules/pwrules.go +++ b/pkg/pwgen/pwrules/pwrules.go @@ -1,6 +1,7 @@ package pwrules import ( + "context" "regexp" "sort" "strconv" @@ -20,13 +21,13 @@ func AllRules() map[string]Rule { // LookupRule looks up a rule either directly or through one of it's know // aliases. -func LookupRule(domain string) (Rule, bool) { +func LookupRule(ctx context.Context, domain string) (Rule, bool) { r, found := genRules[domain] if found { return r, true } - for _, alias := range LookupAliases(domain) { + for _, alias := range LookupAliases(ctx, domain) { if r, found := genRules[alias]; found { return r, true } diff --git a/tests/config_test.go b/tests/config_test.go index d71d769a45..13824c9b88 100644 --- a/tests/config_test.go +++ b/tests/config_test.go @@ -15,24 +15,20 @@ func TestBaseConfig(t *testing.T) { //nolint:paralleltest out, err := ts.run("config") assert.NoError(t, err) - wanted := `autoclip: false -autoimport: true -cliptimeout: 45 -exportkeys: false -keychain: false -nopager: false -notifications: true -parsing: true + wanted := `core.autosync = true +core.cliptimeout = 45 +core.exportkeys = false +core.notifications = true +core.parsing = true ` - wanted += "path: " + ts.storeDir("root") + "\n" - wanted += "safecontent: false" + wanted += "mounts.path = " + ts.storeDir("root") assert.Equal(t, wanted, out) invertables := []string{ - "autoimport", - "safecontent", - "parsing", + "core.autoimport", + "core.showsafecontent", + "core.parsing", } for _, invert := range invertables { //nolint:paralleltest @@ -48,11 +44,11 @@ parsing: true } t.Run("cliptimeout", func(t *testing.T) { - out, err = ts.run("config cliptimeout 120") + out, err = ts.run("config core.cliptimeout 120") assert.NoError(t, err) assert.Equal(t, "120", out) - out, err = ts.run("config cliptimeout") + out, err = ts.run("config core.cliptimeout") assert.NoError(t, err) assert.Equal(t, "120", out) }) @@ -69,19 +65,14 @@ func TestMountConfig(t *testing.T) { //nolint:paralleltest _, err = ts.run("config") assert.NoError(t, err) - wanted := `autoclip: false -autoimport: true -cliptimeout: 45 -exportkeys: false -keychain: false -nopager: false -notifications: true -parsing: true -path: ` - wanted += ts.storeDir("root") + "\n" - wanted += `safecontent: false -mount "mnt/m1" => "` - wanted += ts.storeDir("m1") + "\"\n" + wanted := `core.autosync = true +core.cliptimeout = 45 +core.exportkeys = false +core.notifications = true +core.parsing = true +` + wanted += "mounts.mnt/m1.path = " + ts.storeDir("m1") + "\n" + wanted += "mounts.path = " + ts.storeDir("root") + "\n" out, err := ts.run("config") assert.NoError(t, err) diff --git a/tests/find_test.go b/tests/find_test.go index 4c5a592380..4f61a6deac 100644 --- a/tests/find_test.go +++ b/tests/find_test.go @@ -18,7 +18,7 @@ func TestFind(t *testing.T) { //nolint:paralleltest assert.Error(t, err) assert.Equal(t, "\nError: Usage: "+filepath.Base(ts.Binary)+" find \n", out) - _, err = ts.run("config safecontent false") + _, err = ts.run("config core.showsafecontent false") require.NoError(t, err) out, err = ts.run("find bar") @@ -40,7 +40,7 @@ func TestFind(t *testing.T) { //nolint:paralleltest assert.NoError(t, err) assert.Contains(t, "Found exact match in 'foo/bar'\nbaz", out) - _, err = ts.run("config safecontent true") + _, err = ts.run("config core.showsafecontent true") require.NoError(t, err) out, err = ts.run("find bar") diff --git a/tests/gptest/gunit.go b/tests/gptest/gunit.go index dd1fccba10..4de44d8a84 100644 --- a/tests/gptest/gunit.go +++ b/tests/gptest/gunit.go @@ -12,6 +12,7 @@ import ( aclip "github.com/atotto/clipboard" "github.com/gopasspw/gopass/tests/can" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var gpgDefaultRecipients = []string{"BE73F104"} @@ -27,7 +28,7 @@ type GUnit struct { // GPConfig returns the gopass config location. func (u GUnit) GPConfig() string { - return filepath.Join(u.Dir, "config.yml") + return filepath.Join(u.Dir, ".config", "gopass", "config") } // GPGHome returns the GnuPG homedir. @@ -50,16 +51,17 @@ func NewGUnitTester(t *testing.T) *GUnit { Dir: td, } u.env = map[string]string{ - "CHECKPOINT_DISABLE": "true", - "GNUPGHOME": u.GPGHome(), - "GOPASS_CONFIG": u.GPConfig(), - "GOPASS_HOMEDIR": u.Dir, - "NO_COLOR": "true", - "GOPASS_NO_NOTIFY": "true", - "PAGER": "", + "CHECKPOINT_DISABLE": "true", + "GNUPGHOME": u.GPGHome(), + "GOPASS_CONFIG_NOSYSTEM": "true", + "GOPASS_CONFIG_NO_MIGRATE": "true", + "GOPASS_HOMEDIR": u.Dir, + "NO_COLOR": "true", + "GOPASS_NO_NOTIFY": "true", + "PAGER": "", } assert.NoError(t, setupEnv(u.env)) - assert.NoError(t, os.Mkdir(u.GPGHome(), 0o700)) + require.NoError(t, os.Mkdir(u.GPGHome(), 0o700)) assert.NoError(t, u.initConfig()) assert.NoError(t, u.InitStore("")) @@ -67,9 +69,12 @@ func NewGUnitTester(t *testing.T) *GUnit { } func (u GUnit) initConfig() error { + if err := os.MkdirAll(filepath.Dir(u.GPConfig()), 0o755); err != nil { + return err + } err := os.WriteFile( u.GPConfig(), - []byte(gopassConfig+"\npath: "+u.StoreDir("")+"\nexportkeys: false\n"), + []byte(gopassConfig+"\texportkeys = false\n[mounts]\npath = "+u.StoreDir("")+"\n"), 0o600, ) if err != nil { diff --git a/tests/gptest/unit.go b/tests/gptest/unit.go index 55a472a80d..40a514fb7f 100644 --- a/tests/gptest/unit.go +++ b/tests/gptest/unit.go @@ -9,14 +9,18 @@ import ( aclip "github.com/atotto/clipboard" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( - gopassConfig = `autoclip: true -autoimport: true -cliptimeout: 45 -notifications: true -parsing: true` + gopassConfig = `[core] + autoclip = true + autoimport = true + cliptimeout = 45 + notifications = true + nopager = true + parsing = true +` ) var ( @@ -35,7 +39,7 @@ type Unit struct { // GPConfig returns the gopass config location. func (u Unit) GPConfig() string { - return filepath.Join(u.Dir, "config.yml") + return filepath.Join(u.Dir, ".config", "gopass", "config") } // GPGHome returns the GnuPG homedir. @@ -60,25 +64,30 @@ func NewUnitTester(t *testing.T) *Unit { u.env = map[string]string{ "CHECKPOINT_DISABLE": "true", "GNUPGHOME": u.GPGHome(), - "GOPASS_CONFIG": u.GPConfig(), + "GOPASS_CONFIG_NOSYSTEM": "true", + "GOPASS_CONFIG_NO_MIGRATE": "true", "GOPASS_DISABLE_ENCRYPTION": "true", "GOPASS_HOMEDIR": u.Dir, "NO_COLOR": "true", "GOPASS_NO_NOTIFY": "true", "PAGER": "", } - assert.NoError(t, setupEnv(u.env)) - assert.NoError(t, os.Mkdir(u.GPGHome(), 0o700)) - assert.NoError(t, u.initConfig()) - assert.NoError(t, u.InitStore("")) + assert.NoError(t, setupEnv(u.env), "setup env") + require.NoError(t, os.Mkdir(u.GPGHome(), 0o700)) + assert.NoError(t, u.initConfig(), "pre-populate config") + assert.NoError(t, u.InitStore(""), "init store") return u } func (u Unit) initConfig() error { + if err := os.MkdirAll(filepath.Dir(u.GPConfig()), 0o755); err != nil { + return err + } + err := os.WriteFile( u.GPConfig(), - []byte(gopassConfig+"\npath: "+u.StoreDir("")+"\nexportkeys: true\n"), + []byte(gopassConfig+"\texportkeys = true\n[mounts]\n\tpath: "+u.StoreDir("")+"\n"), 0o600, ) if err != nil { diff --git a/tests/show_test.go b/tests/show_test.go index 897b0aad9c..c47b668f74 100644 --- a/tests/show_test.go +++ b/tests/show_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var goldenQr = "\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[40m \x1b[0m\x1b[40m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\n\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m\x1b[47m \x1b[0m" @@ -42,7 +43,7 @@ func TestShow(t *testing.T) { //nolint:paralleltest }) t.Run("show w/o safecontent", func(t *testing.T) { //nolint:paralleltest - _, err = ts.run("config safecontent false") + _, err = ts.run("config core.showsafecontent false") assert.NoError(t, err) out, err := ts.run("show fixed/secret") @@ -59,29 +60,33 @@ func TestShow(t *testing.T) { //nolint:paralleltest }) t.Run("show w/o autoclip", func(t *testing.T) { //nolint:paralleltest - _, err = ts.run("config autoclip false") + _, err = ts.run("config core.autoclip false") assert.NoError(t, err) _, err = ts.run("show fixed/secret") assert.NoError(t, err) }) t.Run("show with safecontent", func(t *testing.T) { //nolint:paralleltest - _, err = ts.run("config safecontent true") - assert.NoError(t, err) + _, err = ts.run("config core.showsafecontent true") + require.NoError(t, err, "set core.showsafecontent = true") - out, err := ts.run("show fixed/secret") + out, err := ts.run("config core.showsafecontent") + require.NoError(t, err) + assert.Contains(t, out, "true", "verify core.showsafecontent = true") + + out, err = ts.run("show fixed/secret") assert.Error(t, err) - assert.Contains(t, out, "safecontent") + assert.Contains(t, out, "safecontent", "output should contain a safecontent warning") out, err = ts.run("show fixed/twoliner") - assert.NoError(t, err) + require.NoError(t, err) assert.NotContains(t, out, "password: ***") assert.Contains(t, out, "second line") - assert.NotContains(t, out, "first line") + assert.NotContains(t, out, "first line", "safecontent = true should remove the first (password) line") }) t.Run("force showing full secret", func(t *testing.T) { //nolint:paralleltest - _, err = ts.run("config safecontent true") + _, err = ts.run("config core.showsafecontent true") assert.NoError(t, err) out, err := ts.run("show -u fixed/secret") @@ -116,7 +121,7 @@ func TestShow(t *testing.T) { //nolint:paralleltest t.Run("Regression test for #1574 and #1575", func(t *testing.T) { //nolint:paralleltest t.Setenv("GOPASS_CHARACTER_SET", "a") - _, err = ts.run("config safecontent true") + _, err = ts.run("config core.showsafecontent true") assert.NoError(t, err) _, err := ts.run("generate fo2 5") diff --git a/tests/tester.go b/tests/tester.go index da58c465b7..4d78a95626 100644 --- a/tests/tester.go +++ b/tests/tester.go @@ -19,8 +19,8 @@ import ( ) const ( - gopassConfig = ` -exportkeys: false + gopassConfig = `[core] +exportkeys = false ` keyID = "BE73F104" ) @@ -67,6 +67,7 @@ func newTester(t *testing.T) *tester { sourceDir: sourceDir, Binary: gopassBin, } + // create tempDir td, err := os.MkdirTemp("", "gopass-") require.NoError(t, err) @@ -79,14 +80,15 @@ func newTester(t *testing.T) *tester { require.NoError(t, os.Setenv("GNUPGHOME", ts.gpgDir())) require.NoError(t, os.Setenv("GOPASS_DEBUG", "")) require.NoError(t, os.Setenv("NO_COLOR", "true")) - require.NoError(t, os.Setenv("GOPASS_CONFIG", ts.gopassConfig())) + require.NoError(t, os.Setenv("GOPASS_CONFIG_NOSYSTEM", "true")) + require.NoError(t, os.Setenv("GOPASS_CONFIG_NO_MIGRATE", "true")) require.NoError(t, os.Setenv("GOPASS_NO_NOTIFY", "true")) require.NoError(t, os.Setenv("GOPASS_HOMEDIR", td)) // write config require.NoError(t, os.MkdirAll(filepath.Dir(ts.gopassConfig()), 0o700)) // we need to set the root path to something else than the root directory otherwise the mounts will show as regular entries - if err := os.WriteFile(ts.gopassConfig(), []byte(gopassConfig+"\npath: "+ts.storeDir("root")+"\n"), 0o600); err != nil { + if err := os.WriteFile(ts.gopassConfig(), []byte(gopassConfig+"\n[mounts]\npath = "+ts.storeDir("root")+"\n"), 0o600); err != nil { t.Fatalf("Failed to write gopass config to %s: %s", ts.gopassConfig(), err) } @@ -101,7 +103,7 @@ func (ts tester) gpgDir() string { } func (ts tester) gopassConfig() string { - return filepath.Join(ts.tempDir, ".config", "gopass", "config.yml") + return filepath.Join(ts.tempDir, ".config", "gopass", "config") } func (ts tester) storeDir(mount string) string { @@ -124,6 +126,8 @@ func (ts tester) teardown() { } func (ts tester) runCmd(args []string, in []byte) (string, error) { + ts.t.Helper() + if len(args) < 1 { return "", fmt.Errorf("invalid args %v: %w", args, ErrNoCommand) } @@ -143,6 +147,8 @@ func (ts tester) runCmd(args []string, in []byte) (string, error) { } func (ts tester) run(arg string) (string, error) { + ts.t.Helper() + if runtime.GOOS == "windows" { arg = strings.ReplaceAll(arg, "\\", "\\\\") } @@ -166,12 +172,16 @@ func (ts tester) run(arg string) (string, error) { } func (ts tester) runWithInput(arg, input string) ([]byte, error) { + ts.t.Helper() + reader := strings.NewReader(input) return ts.runWithInputReader(arg, reader) } func (ts tester) runWithInputReader(arg string, input io.Reader) ([]byte, error) { + ts.t.Helper() + args, err := shellquote.Split(arg) if err != nil { return nil, fmt.Errorf("failed to split args %v: %w", arg, err) diff --git a/zsh.completion b/zsh.completion index eea256f43f..9de4fa345f 100644 --- a/zsh.completion +++ b/zsh.completion @@ -19,13 +19,6 @@ _gopass () { ;; alias) - local -a subcommands - subcommands=( - "add:Add a new alias" - "remove:Remove an alias from a domain" - "delete:Delete an entire domain" - ) - _describe -t commands "gopass alias" subcommands