Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
944774c
feat: add config module for global keyring path
lucatescari Apr 15, 2026
beb77d9
feat: add config CLI subcommand (set-keyring, unset-keyring, show)
lucatescari Apr 15, 2026
4f299a2
fix: skip symlinks in GPG key directory scan
lucatescari Apr 15, 2026
2c9930f
feat: add-gpg-user falls back to global keyring when no args given
lucatescari Apr 15, 2026
b80184b
test: add comprehensive integration tests for config and keyring feature
lucatescari Apr 15, 2026
bcd7bbb
docs: add config commands and keyring fallback documentation
lucatescari Apr 15, 2026
3e8dbd0
fix: address code review findings in config module
lucatescari Apr 15, 2026
6c208d7
docs: update CONTRIBUTING project layout with config module
lucatescari Apr 15, 2026
0651efc
docs: document keyring directory format and structure
lucatescari Apr 15, 2026
ae86c9e
fix: skip GPG-dependent test on Windows CI (prevents hang)
lucatescari Apr 15, 2026
08c4e04
fix: resolve test races and Windows GPG hang
lucatescari Apr 15, 2026
4257266
test: add 14 GPG integration tests and enforce testing policy
lucatescari Apr 15, 2026
ded9a13
fix: GPG tests skip gracefully on Windows CI
lucatescari Apr 15, 2026
864b0d9
ci: install standalone GPG on Windows via Chocolatey
lucatescari Apr 15, 2026
6b14c61
style: fix formatting in gpg_integration.rs
lucatescari Apr 15, 2026
c0a5d7b
ci: use winget for faster GPG install on Windows
lucatescari Apr 15, 2026
0743de3
ci: install GnuPG on Windows via direct download (faster than winget/…
lucatescari Apr 15, 2026
9e70f08
ci: fix GnuPG Windows installer URL (use 2.4.8 stable)
lucatescari Apr 15, 2026
5d646f9
ci: use scoop for fast GPG install on Windows
lucatescari Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,24 @@ jobs:
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Install git-crypt (Linux)
- name: Install git-crypt and GPG (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y git-crypt
run: sudo apt-get update && sudo apt-get install -y git-crypt gnupg

- name: Install git-crypt (macOS)
- name: Install git-crypt and GPG (macOS)
if: runner.os == 'macOS'
run: brew install git-crypt
run: brew install git-crypt gnupg

- name: Install GPG (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) {
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
}
scoop install gnupg
gpg --version

- name: Check formatting
run: cargo fmt --check
Expand All @@ -54,5 +65,8 @@ jobs:
- name: Integration tests
run: cargo test --test integration

- name: GPG integration tests
run: cargo test --test gpg_integration

- name: Cross-compatibility tests
run: cargo test --test cross_compat
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ When adding features, changing CLI flags, modifying commands, or altering behavi
- Update CONTRIBUTING.md if project structure, test count, build steps, or development guidelines changed
- Keep the test count in CONTRIBUTING.md accurate after adding/removing tests

## Testing Requirements

Every command, subcommand, and code path MUST have integration tests. This is non-negotiable for security-critical software.

- **Every CLI command** must have integration tests covering happy path AND error paths
- **Every flag/option** on every command must be tested
- **GPG-dependent commands** (add-gpg-user, rm-gpg-user, ls-gpg-users, unlock via GPG) must have tests that exercise real GPG operations using test keys in a temp GNUPGHOME
- **Tests must run on all 3 CI platforms** (Linux, macOS, Windows). Only use `#[cfg(unix)]` for genuinely Unix-only concepts (file mode bits, Unix symlinks)
- **When adding a new command or feature**: write tests BEFORE or WITH the implementation, never after. No PR should add a command without corresponding tests.
- **When modifying an existing command**: verify existing tests still cover the behavior, add new tests if the change adds flags or code paths
- **GPG tests** should auto-skip gracefully if GPG is not available (use a `skip_without_gpg!()` macro, same pattern as `skip_without_git_crypt!()` in cross_compat.rs)
- Keep the test count in CONTRIBUTING.md accurate after adding/removing tests

## Code Quality

- Run `cargo clippy` and fix warnings before committing
Expand Down
19 changes: 15 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ cargo build
cargo test
```

All 54 tests should pass (28 unit + 20 integration + 6 cross-compatibility). They cover:
All 91 tests should pass (31 unit + 40 integration + 14 GPG integration + 6 cross-compatibility). They cover:
- AES-256-CTR encryption/decryption round-trips
- HMAC-SHA1 known-answer vectors
- Key file TLV serialization/deserialization
Expand All @@ -36,6 +36,15 @@ All 54 tests should pass (28 unit + 20 integration + 6 cross-compatibility). The
- Status, export-key, quiet mode, error messages (integration)
- Edge cases: empty files, binary files, multi-key lock (integration)
- Pipe deadlock regression: many-file and large-blob status, unlock, lock (integration)
- Global config: XDG resolution, keyring path save/load/remove, permissions (unit)
- Config CLI: set-keyring, unset-keyring, show, overwrite, canonicalization, symlinks (integration)
- Keyring fallback: add-gpg-user with no args, empty dir, deleted dir, precedence (integration)
- Scan security: symlink skipping, non-key extensions, empty directory (integration)
- GPG add-gpg-user: by email, fingerprint, --trusted, --no-commit, -k, --from file (GPG integration)
- GPG rm-gpg-user: remove, --no-commit, user not found (GPG integration)
- GPG ls-gpg-users: list, no users, named key (GPG integration)
- GPG unlock roundtrip: add user, lock, unlock via GPG (GPG integration)
- GPG multi-user: add 2 users, remove 1, verify count (GPG integration)
- Cross-tool: key exchange, encrypt/decrypt, named keys, binary files (cross-compatibility)

The cross-compatibility tests (`tests/cross_compat.rs`) verify interoperability with
Expand All @@ -60,16 +69,18 @@ src/
key/ Key file format (TLV serialization, entries, key container)
filter/ Git clean/smudge/diff filters
commands/ User-facing commands (init, lock, unlock, status, export-key,
add/rm/ls-gpg-users)
add/rm/ls-gpg-users, config)
git/ Git repository helpers (config, checkout, repo inspection)
gpg/ GPG integration (key import, encrypt/decrypt via gpg CLI)
cli.rs clap CLI definitions + shell completion generation
config.rs Global configuration (XDG keyring path)
constants.rs Shared constants (magic bytes, sizes, field IDs)
error.rs Error types
main.rs Entry point
tests/
integration.rs E2E tests using temporary git repos
cross_compat.rs Cross-tool tests against git-crypt
integration.rs E2E tests using temporary git repos
gpg_integration.rs GPG user management tests (add/rm/ls, unlock via GPG)
cross_compat.rs Cross-tool tests against git-crypt
benchmark/
bench.sh Status command scaling by file count
bench_large_files.sh Status with large binary files (Unity-like repos)
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ gitveil add-gpg-user [-k <key-name>] [-n] [--trusted] [--from <source>] [<GPG_US
| `--trusted` | Skip GPG Web of Trust verification |
| `--from <source>` | Import GPG key(s) from a file, directory, or git URL |

When called with no arguments and no `--from`, gitveil checks for a globally configured keyring directory (see `gitveil config set-keyring`). If configured, it scans the directory and shows an interactive picker.

#### Import keys from a shared keyring

If your team stores GPG public keys in a shared repository, you can import them directly:
Expand All @@ -203,6 +205,37 @@ gitveil add-gpg-user --from git@github.com:company/gpg-keys.git

When pointing at a directory (or git URL), gitveil scans for `.asc`, `.gpg`, `.pub`, and `.key` files, shows a list of found keys (name, email, fingerprint), and lets you select one or more to add as collaborators.

### `gitveil config`

Manage global gitveil configuration.

```bash
# Set a global GPG keyring directory
gitveil config set-keyring /path/to/team-keys

# Show current configuration
gitveil config show

# Remove the keyring setting
gitveil config unset-keyring
```

When a keyring directory is configured, `gitveil add-gpg-user` (with no arguments and no `--from`) will automatically scan the keyring directory and present an interactive picker to select GPG keys. This is useful when your team stores GPG public keys in a shared folder or git repository.

The keyring directory has no special format -- it's just a folder containing GPG public key files (exported with `gpg --export` or `gpg --armor --export`). Files are matched by extension (`.asc`, `.gpg`, `.pub`, `.key`) and can be organized in subdirectories. Non-key files and symlinks are ignored.

```
team-keys/
├── engineering/
│ ├── alice.asc
│ └── bob.pub
├── design/
│ └── carol.gpg
└── README.md # ignored (not a key extension)
```

The keyring path is stored in `~/.config/gitveil/config` (respects `$XDG_CONFIG_HOME`). The config file is created with 0600 permissions and the config directory with 0700 permissions.

### `gitveil rm-gpg-user`

Remove a GPG user's access.
Expand Down Expand Up @@ -309,6 +342,7 @@ The clean filter must read the entire file into memory to compute the HMAC-SHA1
src/
main.rs # Entry point + CLI dispatch
cli.rs # clap CLI definitions
config.rs # Global configuration (XDG keyring path)
constants.rs # Magic bytes, sizes, field IDs
error.rs # Error types
crypto/
Expand All @@ -330,6 +364,7 @@ src/
status.rs # Show encryption status
export_key.rs # Export symmetric key
add_gpg_user.rs # Add GPG collaborator
config.rs # Global config management
rm_gpg_user.rs # Remove GPG collaborator
ls_gpg_users.rs # List GPG collaborators
git/
Expand Down
24 changes: 24 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ pub enum Commands {
key_name: Option<String>,
},

/// Manage global gitveil configuration
Config {
#[command(subcommand)]
action: ConfigAction,
},

/// Generate shell completions for bash, zsh, or fish
Completions {
/// Shell to generate completions for
Expand Down Expand Up @@ -161,6 +167,24 @@ pub enum Commands {
},
}

#[derive(Subcommand)]
pub enum ConfigAction {
/// Set the global GPG keyring directory
#[command(name = "set-keyring")]
SetKeyring {
/// Path to a directory containing GPG public key files
#[arg()]
path: PathBuf,
},

/// Remove the global keyring directory setting
#[command(name = "unset-keyring")]
UnsetKeyring,

/// Show current configuration
Show,
}

/// Generate shell completions and write to stdout.
pub fn print_completions(shell: Shell) {
clap_complete::generate(
Expand Down
23 changes: 22 additions & 1 deletion src/commands/add_gpg_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::process::Command;

use colored::Colorize;

use crate::config;
use crate::constants::DEFAULT_KEY_NAME;
use crate::error::GitVeilError;
use crate::git::repo::{find_git_dir, find_repo_root, git_crypt_dir, key_path};
Expand Down Expand Up @@ -47,9 +48,29 @@ pub fn add_gpg_user(
}
}
None => {
// Try global keyring if no --from and no gpg_user_id
if gpg_user_id.is_none() {
match config::load_keyring_path() {
Ok(Some(keyring_path)) => {
return add_from_path(
key_name,
no_commit,
trusted,
&keyring_path,
&git_dir,
);
}
Ok(None) => {} // No keyring configured, fall through to error
Err(e) => {
eprintln!("{} global keyring: {}", "Warning:".yellow().bold(), e);
// Fall through to error
}
}
}

let gpg_user_id = gpg_user_id.ok_or_else(|| {
GitVeilError::Other(
"GPG user ID is required (or use --from to import from a file/directory/URL)"
"GPG user ID is required (or use --from to import from a file/directory/URL, or configure a global keyring with 'gitveil config set-keyring <path>')"
.into(),
)
})?;
Expand Down
43 changes: 43 additions & 0 deletions src/commands/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::path::Path;

use colored::Colorize;

use crate::config;
use crate::error::GitVeilError;

/// Set the global GPG keyring directory.
pub fn config_set_keyring(path: &Path) -> Result<(), GitVeilError> {
config::save_keyring_path(path)?;

// Re-load to show the canonicalized path
let canonical = config::load_keyring_path()?.unwrap_or_default();
eprintln!(
"{} keyring path: {}",
"Set".green().bold(),
canonical.display()
);
Ok(())
}

/// Remove the global GPG keyring directory setting.
pub fn config_unset_keyring() -> Result<(), GitVeilError> {
config::remove_keyring_path()?;
eprintln!("{} keyring path.", "Removed".green().bold());
Ok(())
}

/// Show current configuration.
pub fn config_show() -> Result<(), GitVeilError> {
match config::load_keyring_path() {
Ok(Some(path)) => {
println!("keyring-path: {}", path.display());
}
Ok(None) => {
println!("keyring-path: (not set)");
}
Err(e) => {
println!("keyring-path: (error: {})", e);
}
}
Ok(())
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod add_gpg_user;
pub mod config;
pub mod export_key;
pub mod init;
pub mod lock;
Expand Down
Loading
Loading