Skip to content

fix: status lists untracked filter-marked files; warn on plaintext blobs#27

Merged
lucatescari merged 4 commits into
devfrom
fix/status-show-untracked-files
May 12, 2026
Merged

fix: status lists untracked filter-marked files; warn on plaintext blobs#27
lucatescari merged 4 commits into
devfrom
fix/status-show-untracked-files

Conversation

@lucatescari

@lucatescari lucatescari commented May 12, 2026

Copy link
Copy Markdown
Owner

Summary

Two bugs in gitveil status:

  1. Unstaged filter-marked files were invisible. Plain git ls-files lists only tracked files, so a freshly-created *.secret (matching a filter=git-crypt rule but not yet staged) was completely hidden.
  2. Silent in repos with no filter rules. When .gitattributes had no filter=git-crypt entries, the early-return if git_crypt_files.is_empty() { return Ok(()); } printed nothing and exited 0 — leaving the user with no feedback, even in repos where gitveil init hadn't been run.

This PR fixes both while keeping gitveil's focused default output (rather than adopting git-crypt's verbose-by-default listing). The -e/-u flag semantics from gitveil ≤ 1.2.0 are preserved, and a new -a/--all flag opts into the verbose git-crypt-style listing.

What changed

  • Lists tracked + untracked filter-marked files (was: only tracked).
  • Untracked filter-marked files are tagged (untracked) in dim text so users can distinguish prospective encryption (file will be encrypted on staging) from the actual committed state — without having to cross-reference git status.
  • Appends *** WARNING: staged/committed version is NOT ENCRYPTED! *** when a tracked filter-marked file's index blob is plaintext (typically staged before .gitattributes took effect), with a summary at the end pointing to gitveil status -f.
  • -f now skips files deleted from the working tree (would otherwise stage the deletion), prints a diagnostic, and reports re-staged N of M file(s). Never auto-adds untracked.
  • Recognizes named-key filters (git-crypt-<keyname>) too.
  • Calls find_git_dir() up front so running outside a git repo gives the existing clean Not a git repository error instead of git ls-files failed.
  • Switches all git ls-files parsing to NUL-delimited (-z) — filenames with whitespace/newlines no longer split incorrectly.

Flag layout

Invocation Behavior
gitveil status filter-marked files only (tracked + untracked), WARNING for plaintext blobs, (untracked) for untracked files
gitveil status -a / --all also list files without the filter (git-crypt-style verbose)
gitveil status -e only files whose blob is encrypted (original meaning preserved)
gitveil status -u only files needing re-encryption — the WARNING set (original meaning preserved)
gitveil status -f re-stage WARNING files so the clean filter re-encrypts them

WARNING files are collected unconditionally so -e -f still re-stages them — the display suppression applies to display only. The summary is shown whenever WARNINGs exist, regardless of display flags, so misconfigured files can't be silently hidden.

Why focused-by-default (not git-crypt-style)

git-crypt's verbose default lists every tracked + untracked file in the repo. On the kind of large-repo workload gitveil's perf optimizations target (the README's Unity benchmark is ~4,000 files), that's thousands of not encrypted: lines of noise. The focused default keeps the actionable signal — which files matter for encryption — and -a is one keystroke away when the verbose view is wanted.

Cross-compatibility

No wire-format changes. Filter names (git-crypt / git-crypt-<key>), key file format, and encrypted blob header are untouched. tests/cross_compat.rs still passes. gitveil status -a output matches git-crypt status byte-for-byte except for the (untracked) annotation, which is a gitveil-only clarity affordance.

Security

  • NUL-delimited (-z) parsing is safer than the previous line-based parsing: filenames containing whitespace or newlines no longer split incorrectly.
  • -f auto-staging is scoped strictly to tracked filter+plaintext-blob files (warning_files); cannot auto-add untracked files; cannot stage the deletion of a missing tracked file.
  • No new GPG/key material handling; status is purely informational.
  • All filename → git add calls use the -- separator (no flag injection).

Test plan

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test112 pass (34 unit + 56 integration + 16 GPG + 6 cross-compat); was 95
  • TDD red confirmed: every behavioral assertion has been seen to fail against pre-fix code before the corresponding implementation; including the (untracked) marker assertion in test_status_shows_untracked_filter_matched_file and the deleted-file skip in test_status_fix_skips_file_deleted_from_working_tree.
  • Flake stress: previously-flaky test passes 5 consecutive runs of the full status suite.
  • Manual scenarios verified: default focused output, -a verbose, -e (encrypted only), -u (WARNING only), -f re-encrypts, -f on deleted file skips, untracked filter file shows with (untracked) and no WARNING, uninitialized repo with filter-marked files surfaces WARNINGs, outside-git-repo gives clean error.

New test coverage (all run on Linux/macOS/Windows):

  • test_status_shows_untracked_filter_matched_file (also asserts the (untracked) marker)
  • test_status_tracked_filter_file_has_no_untracked_marker
  • test_status_default_hides_non_filter_files
  • test_status_a_flag_lists_non_filter_files
  • test_status_a_long_flag_alias
  • test_status_warning_for_filter_with_plain_blob
  • test_status_no_warning_when_all_correctly_encrypted
  • test_status_e_flag_only_encrypted_blobs
  • test_status_u_flag_only_warning_files
  • test_status_fix_restages_only_warning_files
  • test_status_fix_skips_file_deleted_from_working_tree
  • test_status_excludes_gitignored_files
  • test_status_not_a_git_repo_clear_error
  • test_status_works_without_gitveil_init
  • test_status_handles_filename_with_spaces
  • test_status_named_key_filter
  • has_git_crypt_filter_recognizes_default_and_named (unit)

Documentation updates included

  • README.md: rewritten gitveil status section with the new flag table, default behavior, WARNING summary, and (untracked) suffix.
  • CONTRIBUTING.md: bumped test count to 112; expanded the status entry in the coverage list.
  • TESTING.md: updated the manual "Test Status and Fix" walkthrough to reflect the WARNING output; added a short "Untracked file marker" subsection.

Known limitations not addressed in this PR

  • Bare repos: find_git_dir() succeeds in a bare repo, then git ls-files --others fails with a less-clean error. Status doesn't make sense in a bare repo anyway, but the error message could be friendlier. Not regressed by this PR.
  • Submodule paths: surfaced as a single entry, same as git-crypt.

🤖 Generated with Claude Code

lucatescari and others added 3 commits May 12, 2026 15:11
`gitveil status` previously ran plain `git ls-files` and early-returned
when no filter-matched files existed. Two consequences:

  1. Untracked files were never shown — even ones that would be
     encrypted on staging (filter=git-crypt). git-crypt lists them.
  2. In any repo without `filter=git-crypt` entries in .gitattributes
     (including freshly-inited or uninitialized repos), status printed
     nothing and exited 0 — leaving the user with no feedback.

Rewrite to mirror git-crypt's semantics:

  - List tracked + untracked files via two `git ls-files -z` calls
    (NUL-delimited; handles filenames with whitespace).
  - For every file, print `    encrypted:` (has git-crypt filter) or
    `not encrypted:` (no filter) — matches git-crypt's prefix layout.
  - Append `*** WARNING: staged/committed version is NOT ENCRYPTED! ***`
    when a filter-marked file's index blob is plaintext (file was
    staged before .gitattributes took effect). Print a summary at the
    end pointing at `gitveil status -f`.
  - `-f` only re-stages WARNING files; never auto-adds untracked.
  - Recognize named-key filters (`git-crypt-<keyname>`) too.
  - Call `find_git_dir()` up front so running outside a git repo
    gives the clean `Not a git repository` error instead of a
    confusing `git ls-files failed`.

Performance characteristics are preserved: at most one ls-files per
category, one batched check-attr, and one batched cat-file regardless
of repo size. Pipe-deadlock writer threads kept on both batched paths.

12 new integration tests + 1 unit test cover: untracked filter-matched
and non-filter files, tracked non-filter, WARNING + summary, no-warning
when clean, -e/-u/-f flags, fix-stages-only-warning-files, named-key
filter, filenames-with-spaces, not-a-git-repo error, and works-without-
gitveil-init. Existing assertions in test_status_shows_encrypted_files
and test_status_many_files_no_deadlock updated for the new semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…diagnostics

Audit-driven follow-ups on the same PR:

* `gitveil status -f` previously called `git add -- <file>` for every
  WARNING file. If the file had been deleted from the working tree (but
  was still tracked), git would stage the deletion — the opposite of
  re-encrypting. Skip such files with a clear diagnostic instead.

* Add a regression test asserting gitignored files are excluded from
  status output (relies on `--exclude-standard` in `git ls-files`).

* `test_status_fix_restages_only_warning_files` flaked once during
  local stress runs. Beef up its failure message with full gitveil
  stdout/stderr + `git diff` + `git status --porcelain` so any future
  flake gives us the state to diagnose, instead of just `got: ""`.

* `Done. re-staged N of M file(s).` summary so the user can tell when
  some files were skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…isting

Reverts the verbose-by-default behavior from the previous commits and
restores the original gitveil semantics for -e/-u, on the basis that
they were genuinely more useful and changing them silently broke
existing scripts. The bug-fix gains (untracked files, WARNING, clean
not-a-git-repo error, -f skip-deleted, NUL-delimited parsing) are kept.

New flag layout:

  default       focused: filter-marked files only (tracked + untracked)
  -a / --all    verbose: also list files without the git-crypt filter
                (git-crypt-style listing — only when explicitly asked for)
  -e            only files whose blob is encrypted  (original meaning)
  -u            only files marked for encryption with plaintext blob,
                i.e. the WARNING set — pair with -f                (original meaning)
  -f / --fix    unchanged

WARNING files are still collected unconditionally so that `-e -f` (which
hides them from display) still re-stages them. The summary is shown
whenever WARNINGs exist, regardless of display flags, so problems can't
be silently hidden.

Tests updated and added accordingly: split the old "non-filter file is
listed" tests into `test_status_default_hides_non_filter_files` (default)
and `test_status_a_flag_lists_non_filter_files` (-a), plus
`test_status_a_long_flag_alias` for --all. Rewrote `-u` and `-e` tests
for the restored semantics; `-u` now asserts the WARNING set, not
non-filter files. Setup of `bad.secret` warning files switched to
explicit `git add <paths>` because `git add .` would re-stage them
through the now-active clean filter and erase the WARNING state we're
testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lucatescari lucatescari changed the title fix: status lists untracked files and matches git-crypt semantics fix: status lists untracked filter-marked files; warn on plaintext blobs May 12, 2026
`encrypted: <file>` on its own is aspirational for a file that isn't
even staged yet: the file on disk is still plaintext, it just *will*
be encrypted when staged. Append a dim "(untracked)" suffix in that
specific case so users can distinguish prospective encryption from
the actual committed state without cross-referencing `git status`.

Tracked filter+encrypted files are unchanged (no suffix). WARNING
files are unchanged (only ever fire for tracked files anyway).

Tests: updated `test_status_shows_untracked_filter_matched_file` to
assert the marker; added negative pairing
`test_status_tracked_filter_file_has_no_untracked_marker` so a
regression that prints the suffix on tracked files is caught.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lucatescari lucatescari merged commit 1de9d59 into dev May 12, 2026
3 checks passed
@lucatescari lucatescari deleted the fix/status-show-untracked-files branch May 12, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant