* feat(backend): add pure-Go go-git storage backend Add an opt-in storage/RCS backend (gogit) that implements backend.Storage using github.com/go-git/go-git/v5. It removes the runtime dependency on the git binary for cross-compiled and single-binary distributions and for Windows users without Git for Windows on PATH. - New package internal/backend/storage/gogit implementing the existing backend.Storage interface (file ops delegate to fs.Store, RCS ops use go-git). The interface contract is unchanged. - Registered as gogit with priority 12 (after gitfs) so the CLI backend remains the auto-detected default when git is present. - Selectable via storage.backend=gogit, GOPASS_GIT_BACKEND=gogit or --storage=gogit. - Automatic fallback to gogit when the git binary is absent: the gitfs loader now fails Handles/New/Init/Clone with ErrGitBinaryNotFound and backend.ResolveStorageBackend downgrades a requested GitFS backend. - File-level merge with a two-parent merge commit for non-fast-forward pulls. A true conflict never discards a secret: the local version is kept and the remote version is written to <name>.conflict-<shortsha>. - All transport/auth logic isolated in auth.go (the go-git v6 seam). SSH via x/crypto/ssh + ssh-agent; HTTPS via git credential fill when git is available, otherwise URL userinfo. - Signed commits shell out to git commit -S when configured and git is present; otherwise an unsigned commit is created with a warning. gpg.format=ssh signing is unsupported. - doctor is aware of the pure-Go backend. - Docs: docs/backends/gogit.md, docs/backends.md, docs/config.md, docs/components.dot, docs/commands/git.md, ADR A-17 marked implemented. - Tests: unit + merge-matrix tests, a cross-backend object-graph parity test, and an opt-in integration test. Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> * fix(backend): address review feedback on the go-git backend Harden and correct the pure-Go go-git storage backend following review of the initial implementation. Security and correctness: - Validate every path from a remote tree before materialising it: reject absolute paths, paths escaping the store root, and anything inside .git. Also reject .git/.git/... in fs.Store.safePath as defence in depth, so a crafted remote commit can no longer plant a hook or rewrite the config. - Pull before pushing so a remote that is ahead no longer makes every push fail with a non-fast-forward error. - Replace HardReset with MergeReset when fast-forwarding: untracked files are preserved and uncommitted edits abort the reset instead of being silently discarded. Materialise an unborn HEAD first, which is what `gopass setup --remote` needs. - Stage explicit paths with AddOptions{Path} only. go-git checks All before Path, so the previous {All:true, Path:rel} staged the whole worktree and swept unrelated files into every "Save secret" commit. - Never resolve a concurrent .gpg-id (recipient) change automatically: abort the pull with a clear error and leave the worktree untouched. - Make the merge transactional: compute the whole resolution before writing anything and restore the index and worktree to HEAD on failure. - Recreate symlinks from the remote tree instead of writing the target as a regular file, refuse submodules, and include the file mode in the identical-on-both-sides check. - Route the merge commit through the same signing path as Commit. - Pick a collision-free conflict-file name by growing the hash prefix. - Fix Revisions: go-git's PathFilter compares each commit with the next in iteration order, so walk the log and diff against the first parent. - Honour commit.gpgsign from the global scope and pass --no-verify to the signing shell-out so git hooks are never run. - Fix urlUserInfo: only short-circuit on a URL that carries a password and percent-decode the userinfo. Run `git credential fill` in the store directory. Redact the password from clone errors. Backend selection: - Centralise the flag > env > config > default precedence in ResolveGitBackend and GitBackendFromEnvOrConfig, and use it from init, rcs and clone so GOPASS_GIT_BACKEND is honoured everywhere. - Only honour GOPASS_GIT_BACKEND when the configured/detected backend is already a git-family backend, so a cryptfs or jjfs store is not reopened as plain gogit/gitfs. - Resolve the cryptfs substorage so cryptfs stores open on git-less systems. - Move fossilfs to priority 20 to resolve the collision with gogit (12). Also stop reporting the expected "nothing to commit" result of the .gitattributes commit as a failure. Add unit tests for the new behaviour (add isolation, push-pulls-first, untracked preservation, symlink merge, .git rejection, .gpg-id conflict, conflict-name collision, revisions across merges, path validation, auth, no-git path), a cross-backend merge test, and a `gopass sync` integration test. Reconcile the ADR and docs/backends/gogit.md with the implementation. Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> * test(gogit): make cross-backend git CLI checks work on Windows The cross-backend tests invoked the git CLI in ways the MSYS2 git used by the Windows CI job cannot handle: - `git -C <dir>` is implemented with a chdir() that does not accept a Windows-style backslash path (e.g. `C:\msys64\tmp\...\001`), so it failed with exit status 128 even though the repository is valid. Set the process working directory via cmd.Dir instead, matching the gitfs backend. - `rev-parse HEAD^{tree}` is mangled by the MSYS2 runtime's brace expansion (`{tree}` -> `tree`), yielding "ambiguous argument 'HEAD^tree'". Use `show -s --format=%T HEAD` instead. Also capture stderr so future failures are diagnosable. Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> * fix(backend): address review feedback on the go-git backend Second review round on the pure-Go go-git storage backend: - fs: refuse to write through symlinked path components and replace a symlink with a regular file instead of following it, so a remote tree cannot plant a link into .git and then write through it. SetSymlink now only accepts relative targets that stay inside the store and outside .git. The .git guard also covers Windows aliases (GIT~1, trailing dots/spaces, alternate data streams) and nested components. - gogit: skip local-only deletions in the merge plan (staging them failed with "entry not found"), refuse a non-fast-forward merge over uncommitted changes, and restore only the affected paths on failure. - gogit: preserve staged files across an unborn-HEAD pull (gopass setup --remote), return early when the remote is an ancestor of local instead of creating an empty merge commit, and report a missing remote branch instead of silently doing nothing. - gogit: make the unsigned-commit fallback opt-in via gogit.allow-unsigned-commits, accept yes/on/1 for commit.gpgsign, sign merge commits via .git/MERGE_HEAD (git commit -p is --patch), and honour the commit timestamp for signed commits. - gogit: strip userinfo from remote URLs before handing them to go-git so credentials cannot leak through go-git's error messages; fix splitHTTPURL for username-only URLs and disable the terminal prompt for git credential fill. - gogit: resolve user.name/user.email local-over-global, compare revisions against all parents, exclude untracked files from staged changes, delete loose objects on prune, and treat .age-recipients/.age-ids/ .gopass-mapping as recipient files. - backend: only honour storage.backend for git-family backends and apply GOPASS_GIT_BACKEND after auto-detection, so a cryptfs/jjfs store is not reopened as plain gogit/gitfs. - docs: reconcile the ADR, docs/backends/gogit.md and docs/config.md with the implementation; drop the duplicate A-17 index row. Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> * fix(backend): address remaining review feedback on the go-git backend - fs: reject .jj (and .git) metadata paths in safePath; rename the guard to rejectVCSDir/IsVCSDirName and cover the .jj aliases in the tests. - gogit: preserve the executable bit when materialising remote tree entries so a merge does not record a spurious mode change. - gogit: add tests for executable-mode preservation, retry after a partial merge failure, symlink-then-regular-file against .git/config, and the opt-in signed-commit requirement. - fossilfs: rephrase the priority comment (highest among VCS backends). - docs: reconcile the ADR and gogit backend docs with the implementation (all-parents TREESAME history, prune handler, opt-in unsigned fallback, .jj guard, executable mode). Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> * test(gogit): make executable-mode merge assertion Windows-aware TestGoGitMergePreservesExecutableMode asserted Unix semantics on all platforms. Windows has no executable bit: os.Chmod cannot set it and go-git derives the index mode from the OS file mode (filemode.NewFromOSFileMode), so both the worktree bit and the recorded index mode are non-executable there. ADR A-17 already accepts this ("gopass secrets are never executable"), so gate the Unix-only assertions on GOOS and assert the regular-file outcome on Windows. Also document that the executable bit is preserved except on Windows in docs/backends/gogit.md and in the applyFileMode comment, and reword the test comment to describe the resolved mode rather than a Unix mode. Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> --------- Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
11 KiB
Conventions
Normative reference for commit messages, versioning, branch and tag names, and file naming.
See CONTRIBUTING.md for the contribution workflow and docs/hacking.md for the development environment.
1. Commit messages
Commit subjects follow Conventional Commits 1.0.0.
1.1 Format
<type>[(<scope>)][!]: <description>
[optional body]
[optional footers]
Rules:
- Limit the subject line to 72 characters.
- Write the description in the imperative mood: "add", not "adds" or "added".
- Start the description in lowercase. Do not end it with a period.
- Include a
Signed-off-by:footer in every commit, as required by CONTRIBUTING.md. Conventional Commits governs the subject line; the Developer Certificate of Origin adds a trailer. Both apply. - Write the pull request title as a valid Conventional Commit.
The pull request title requirement follows from the merge strategy. Pull
requests are squash-merged: pull request #3489, titled fix: Avoid NPE when attempting to edit a non-existing secret, landed on master as the
single-parent commit eb1368e4 with the subject fix: Avoid NPE when attempting to edit a non-existing secret (#3489). The pull request title, not
the individual commit subjects, is the string that reaches CHANGELOG.md.
1.2 Types
This list is exhaustive. Reject any other type.
| Type | Meaning | SemVer impact | Changelog section |
|---|---|---|---|
feat |
New user-visible capability | MINOR | Added |
fix |
Bug fix | PATCH | Fixed |
security |
Security fix or hardening | PATCH | Security |
perf |
Performance improvement, no behaviour change | PATCH | Changed |
refactor |
Internal restructuring, no behaviour change | PATCH | Changed |
revert |
Reverts a previous commit | PATCH | Changed |
deps |
Dependency version change | PATCH | Changed |
docs |
Documentation only | none | omitted |
test |
Tests only | none | omitted |
build |
Build system: Makefile, goreleaser, Dockerfile | none | omitted |
ci |
GitHub Actions, linter configuration, workflows | none | omitted |
chore |
Housekeeping that fits nothing above | none | omitted |
Two entries extend the set suggested by the specification:
securitypopulates theSecuritysection of the changelog, which Keep a Changelog defines as a first-class category. Five commits since 2025-01-01 already use it.depsis the preferred type for hand-written dependency commits. Acceptchore(deps)as well: Dependabot emits it, and 147 of the 344 commit subjects since 2025-01-01 carry thechoretype, nearly all of them dependency bumps.
The following appear as commit types in the history and are not types. Use them
as scopes: otp (2 commits since 2025-01-01), age, bug, fscopy, and
openbsd (1 each).
1.3 Scopes
The scope is optional. Prefer to supply one. Do not invent scopes; omit the scope when none fits. Omit the scope for a change spanning several areas rather than listing more than one.
Commands — the 42 top-level subcommands: 40 registered by
(*Action).GetCommands in internal/action/commands.go, plus pwgen from
internal/action/pwgen and completion, both added by getCommands in
main.go:
alias audit cat clone completion config convert copy create delete doctor edit
env find fsck fscopy fsmove generate grep history init insert link list merge
mounts move otp process pwgen rcs recipients reorg secret-service setup show sum
sync templates unclip update version
Backends: age, gpg, plain, cryptfs, fossilfs, fs, gitfs, gogit, jjfs
Subsystems: action, audit, backend, cache, completion, config,
create, cui, editor, hashsum, hook, notify, out, queue,
recipients, reminder, store, store/leaf, store/root, tpl, tree,
updater
Public API: pkg/gopass, api, secrets, appdir, clipboard,
ctxutil, debug, fsutil, otp, passkey, pinentry, protect, pwgen,
qrcon, set, tempfile, termio
Meta: deps, release, changelog, docs, adr, ci, build
Write pkg/gopass out in full as a scope. Public-API changes must be greppable
against the A-12 stability contract.
1.4 Breaking changes
This repository has two independent compatibility surfaces. Mark them differently. Do not conflate them.
| Surface broken | Marker | Version impact |
|---|---|---|
| gopass CLI: commands, flags, output format, exit codes, config keys, store format | ! after the type or scope, and a BREAKING CHANGE: footer |
MAJOR |
Go module pkg/gopass only: an exported symbol removed or changed |
a PKG-BREAK: footer, no ! |
none; MINOR at most |
| Both | ! + BREAKING CHANGE: + PKG-BREAK: |
MAJOR |
Do not mark a module-only break with !. No CLI user can observe such a
change, and ! demands a major release.
Example of a module-only break, per A-12:
refactor(pkg/gopass): drop Store.GetRevision in favour of Store.History
Deprecated since 1.17.0; the two-minor / three-month window has elapsed.
PKG-BREAK: pkg/gopass: remove Store.GetRevision, use Store.History instead
Signed-off-by: Your Name <your@example.com>
The PKG-BREAK: footer is the machine-readable form of the [PKG-BREAK]
changelog tag mandated by A-12.
1.5 Changelog control footers
| Footer | Effect |
|---|---|
Changelog-Section: Added|Changed|Deprecated|Removed|Fixed|Security |
Overrides the type-to-section mapping. Required for Removed and Deprecated, which have no corresponding commit type. |
RELEASE_NOTES=<text> or RELEASE_NOTES=n/a |
Overrides the bullet text, or suppresses the entry. |
PKG-BREAK: <text> |
Emits a [PKG-BREAK]-prefixed bullet. |
2. Versioning
This project follows Semantic Versioning 2.0.0.
2.1 Covered by SemVer
SemVer applies to the gopass command line interface and its observable behaviour: subcommands, flags and their aliases, the stdout and stderr contracts, JSON output schemas, exit codes (docs/exit-codes.md), configuration keys (docs/config.md), the on-disk store format, and the generated shell completions and man page.
2.2 Not covered by SemVer
The Go module github.com/gopasspw/gopass carries no independent semver
guarantees. pkg/gopass is governed by A-12
and by the package documentation in pkg/gopass/doc.go, which declare it
best-effort stable:
- Additive changes — new exported symbols, new functional-option parameters — may appear in any release.
- Breaking changes — removal or signature change of an exported symbol, change
to an interface method set or to error semantics — require a
[PKG-BREAK]entry inCHANGELOG.mdand a deprecation window of two minor releases or three months, whichever is longer.
2.3 Precedence
Release a change that breaks only pkg/ consumers as MINOR or PATCH. Do not
bump MAJOR for it. Release a change that breaks CLI users as MAJOR regardless
of its pkg/ impact.
2.4 Pre-releases
Use vX.Y.Z-rc.N only: SemVer pre-release identifiers, dot-separated, numeric.
Sign the tag. Cut it from the merged release/vX.Y.Z-rc.N branch. See
docs/releases.md.
3. Branch names
<type>/<kebab-slug>[-<issue>]
- Use a commit type from §1.2 as
<type>. - Restrict the slug to
[a-z0-9-], plus the single/separator. Use no spaces and no underscores. Limit the whole name to 60 characters. - Append the issue number when one exists:
refactor/separate-storage-rcs-3411. - Do not create these prefixes by hand; the release automation owns them:
release/vX.Y.Zandrelease/vX.Y.Z-rc.N, created byhelpers/release/main.go, andprep/vX.Y.Z, documented in docs/releases.md. - Do not push feature branches to
gopasspw/gopass. Work on a fork.
4. Tag names
- Name releases
vX.Y.Zand pre-releasesvX.Y.Z-rc.N. - Sign every tag (
git tag -s). - Create no other tags in this repository.
.github/workflows/autorelease.yml triggers on push: tags: 'v*'.
helpers/release/main.go strips the leading v and parses the remainder as
semver.
5. File names
5.1 General rules
- Use lowercase kebab-case, restricted to
[a-z0-9._-]. - Use no spaces.
- Zero-pad any numeric component that participates in lexical ordering.
- Prefix with
YYYY-MM-DDwhen chronological order matters. - Order components from most general to most specific, left to right.
5.2 Architecture decision records
Name records docs/adr/A-NN-<kebab-slug>.md, with NN zero-padded to two
digits.
- Write the H1 as
# A-NN: <Title>, matching the file name. - Open the record with a
**Status:**line —proposed,accepted,deferred,implemented,partially implemented, orsuperseded by A-NN— and a**Source:**line. - Never reuse a number. Supersede a decision by writing a new record.
- Update docs/adr/README.md in the same commit that adds or supersedes a record.
5.3 Other documentation
- Name command specifications
docs/commands/<command>.md, where the file name is exactly the command name. - Name backend documents
docs/backends/<backend>.mdand use casesdocs/usecases/<kebab-slug>.md. - Use lowercase throughout. Keep the SCREAMING_CASE names of root-level documents, which is the GitHub convention.
5.4 Go files
- Use lowercase names with no underscores, except for the reserved suffixes below. Prefer a single word matching the primary type or the command.
- Place a command implementation in
internal/action/<command>.gowith an accompanyinginternal/action/<command>_test.go. - Reserved suffixes:
_test.go; theGOOSandGOARCHsuffixes_unix.go,_windows.go,_linux.go,_darwin.go,_others.go;_gen.gofor generated code;_fuzz.gofor fuzz targets. - Give a build-tag variant that is not
GOOS-based a descriptive suffix and an explicit//go:buildline, for examplescreenshot_supported.goandscreenshot_stub.go. - Split a file above roughly 600 lines along an existing type or feature seam.
6. Changelog
CHANGELOG.md follows Keep a Changelog 1.1.0:
an ## [Unreleased] section at the top, then one ## [X.Y.Z] - YYYY-MM-DD
section per release, each containing only the non-empty subsections of
Added, Changed, Deprecated, Removed, Fixed, and Security.
helpers/release generates the entries from commit subjects at release time,
using the mapping in §1.2 and the footers in §1.5. Do not edit CHANGELOG.md
by hand, with two exceptions. Add a hand-written ## [Unreleased] entry for:
- any change to the exported surface of
pkg/, as required by A-12; and - any
security:change.