Files
Dominik Schulz 76a213d4c6 feat(backend): add pure-Go go-git storage backend (#3657)
* 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>
2026-10-14 09:40:57 +02:00

1.4 KiB

git command

The git command runs an arbitrary git command inside a password store, without the need to cd into the store directory first.

Note: This command is provided by the gitfs storage backend and is only available when the selected store uses that backend. It requires the git binary at runtime. Stores using the pure-Go gogit backend still have a .git directory, so this command works for them too as long as git is installed.

Synopsis

$ gopass git status
$ gopass git --store foo log --oneline
$ gopass git remote add origin git@example.com/store.git

Modes of operation

  • Run any git subcommand in the directory of the selected store

Flags

Flag Aliases Description
--store Store to operate on.

Details

The command is hidden from the regular help output because it is intended as an escape hatch. Prefer the dedicated subcommands (e.g. gopass sync) for common operations.

The arguments after gopass git are passed through to git unchanged. STDIN, STDOUT and STDERR are connected to the terminal.

See also rcs, pull and push.

Exit codes

The exit code is that of the invoked git process, with 0 indicating success. See docs/exit-codes.md for the git entry and the full table.