gunnar-crypt 1.6.0

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"
    );
}