gunnar-crypt 1.5.1

Encrypted repositories: the content envelope, the per-repo key ring, its members, the server's plaintext check, each person's one encryption key and its unlock methods, and the ring's wrap to members' encryption keys
Documentation
//! **A nested `.gitattributes` is plaintext on both sides** (wiring tracer B
//! 2026-10-08). The client's line `.gitattributes !filter !diff !merge` has no
//! slash, so git matches it at ANY depth and `sub/.gitattributes` is committed
//! as it is; the server allowed only the root one, so such a push was refused
//! with advice (renormalize) that could not fix it. Every `.gitattributes`
//! names filters git reads before a clone is unlocked: plaintext, at any depth.

use gunnar_crypt::policy::{check_blob, Refusal};

#[test]
fn a_gitattributes_at_any_depth_is_plaintext_and_nothing_else_is() {
    let text = b"*.bin -diff\n";
    for path in [".gitattributes", "docs/.gitattributes", "a/b/c/.gitattributes"] {
        assert_eq!(check_blob(path, false, text), Ok(()), "{path}");
    }
    for path in ["docs/.gitattributes.bak", "docs/x.gitattributes", "docs/notes.txt"] {
        assert!(matches!(check_blob(path, false, text), Err(Refusal::Plaintext { .. })), "{path} is no attributes file");
    }
    assert!(matches!(check_blob("docs/.gitattributes", true, text), Err(Refusal::Symlink { .. })), "a symlink never");
    assert!(
        gunnar_crypt::GITATTRIBUTES_LINES.iter().any(|l| l.starts_with(".gitattributes ")),
        "the client keeps the depth-free line the server now matches"
    );
}