wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! What may be carried, and what may never be — including the paths a peer
//! could name to reach outside the tree.

use super::*;

/// TEST9960: `.git` is refused at any depth, in any shape, and a
/// repository's committed `.wsb/` is not.
///
/// Not for size. `.git` is the repository's own state — carrying it would BE
/// reconciling git, which this feature never does — and it reaches outside the
/// directory it sits in, because a submodule's `.git` is a FILE pointing at
/// the superproject. `.wsb/` is the repository's settings, notes and links,
/// committed with it: work like any other file's.
#[test]
fn test9960_git_can_never_be_carried_and_wsb_settings_are() {
    for refused in [
        ".git",
        ".git/config",
        ".git/modules/capdag/HEAD",
        "cartridges/.git",
        "cartridges/mlx/.git/objects/ab/cd",
    ] {
        assert!(
            is_refused(std::path::Path::new(refused)),
            "{refused} was not refused"
        );
    }
    for allowed in [
        "src/main.rs",
        "git/notes.md",
        "wsb/src/lib.rs",
        ".github/workflows/ci.yml",
        ".wsb/repo.json",
        ".wsb/notes/20261024-213000-a1b2.md",
        "wsb/.wsb/links/0123456789abcdef.json",
    ] {
        assert!(
            !is_refused(std::path::Path::new(allowed)),
            "{allowed} was refused"
        );
    }
}

/// TEST9961: a path from a peer cannot escape the repository.
///
/// A peer is authenticated, not trusted. This is the one place a name chosen
/// by another machine becomes a path on this filesystem, so it refuses rather
/// than sanitises: a cleaned-up version of a hostile path is still a path
/// somebody else chose.
#[test]
fn test9961_a_peer_cannot_name_a_path_outside_the_tree() {
    let root = std::path::Path::new("/worktree");
    for hostile in [
        "../outside",
        "a/../../outside",
        "/etc/passwd",
        ".git/hooks/pre-commit",
        "src\\windows.rs",
        "",
    ] {
        assert!(
            local_path(root, hostile).is_err(),
            "{hostile:?} resolved to a path"
        );
    }

    let resolved = local_path(root, "src/peer/mod.rs").expect("an ordinary path");
    assert_eq!(resolved, root.join("src/peer/mod.rs"));
    assert!(resolved.starts_with(root));
}

/// TEST9962: the continuity set is what git says is work in progress.
///
/// Tracked files and untracked files git does not ignore. Build output is
/// ignored, which is exactly why it is excluded — it is large, machine-
/// specific and reproducible, and nobody wants it copied between machines.
#[test]
fn test9962_the_set_is_tracked_plus_untracked_minus_ignored() {
    let temp = tempfile::tempdir().expect("tempdir");
    let dir = temp.path();
    run(dir, &["init", "-q", "-b", "main"]);
    run(dir, &["config", "user.name", "Test"]);
    run(dir, &["config", "user.email", "test@example.com"]);

    std::fs::write(dir.join(".gitignore"), "target/\n*.log\n").expect("write");
    std::fs::create_dir_all(dir.join("src")).expect("mkdir");
    std::fs::create_dir_all(dir.join("target/debug")).expect("mkdir");
    std::fs::write(dir.join("src/main.rs"), "fn main() {}\n").expect("write");
    run(dir, &["add", "."]);
    run(dir, &["commit", "-qm", "one"]);

    // After the commit: one modification, one new file, and two things git
    // ignores.
    std::fs::write(dir.join("src/main.rs"), "fn main() { work() }\n").expect("write");
    std::fs::write(dir.join("src/new.rs"), "// in progress\n").expect("write");
    std::fs::write(dir.join("target/debug/binary"), "ELF").expect("write");
    std::fs::write(dir.join("run.log"), "noise").expect("write");
    std::fs::write(dir.join(".git-not-refused"), "ordinary").expect("write");

    let set = read(dir, &Policy::default()).expect("a set");
    let paths: Vec<&str> = set.keys().map(String::as_str).collect();

    assert!(paths.contains(&"src/main.rs"), "{paths:?}");
    assert!(paths.contains(&".gitignore"), "{paths:?}");
    assert!(paths.contains(&"src/new.rs"), "an untracked file is work in progress: {paths:?}");
    assert!(!paths.contains(&"target/debug/binary"), "{paths:?}");
    assert!(!paths.contains(&"run.log"), "{paths:?}");
    // A name that merely starts with a refused one is not refused.
    assert!(paths.contains(&".git-not-refused"), "{paths:?}");

    // The entry carries content identity, not a timestamp.
    let entry = set.get("src/main.rs").expect("the modified file");
    assert_eq!(
        entry.hash,
        crate::peer::converge::hash_bytes(b"fn main() { work() }\n")
    );
}

/// TEST9963: the policy is declared, compared, and cannot re-admit the refused
/// names.
///
/// Two machines that disagree about what may move must block rather than merge
/// their rules: a union would move files one side never agreed to move, and an
/// intersection would silently drop a rule somebody wrote.
#[test]
fn test9963_policy_is_declared_compared_and_bounded() {
    let temp = tempfile::tempdir().expect("tempdir");
    let dir = temp.path();
    run(dir, &["init", "-q", "-b", "main"]);
    run(dir, &["config", "user.name", "Test"]);
    run(dir, &["config", "user.email", "test@example.com"]);
    std::fs::write(dir.join(".gitignore"), ".env\nbuild/\n").expect("write");
    std::fs::write(dir.join("kept.rs"), "x\n").expect("write");
    run(dir, &["add", "."]);
    run(dir, &["commit", "-qm", "one"]);
    std::fs::write(dir.join(".env"), "TOKEN=abc\n").expect("write");
    std::fs::create_dir_all(dir.join("build")).expect("mkdir");
    std::fs::write(dir.join("build/out"), "big").expect("write");

    // Ignored by default.
    let plain = read(dir, &Policy::default()).expect("a set");
    assert!(!plain.contains_key(".env"));

    // Opted in by declaration.
    let opted = Policy {
        include_ignored: vec![".env".into()],
        exclude: vec![],
    };
    let with_env = read(dir, &opted).expect("a set");
    assert!(with_env.contains_key(".env"), "an explicit include must be carried");
    assert!(!with_env.contains_key("build/out"), "only what was named");

    // Excluded by declaration.
    let excluded = Policy {
        include_ignored: vec![],
        exclude: vec!["kept.rs".into()],
    };
    assert!(!read(dir, &excluded).expect("a set").contains_key("kept.rs"));

    // The hard exclusions cannot be re-admitted, however they are spelled.
    let hostile = Policy {
        include_ignored: vec![".git".into(), ".wsb".into(), ".git/config".into()],
        exclude: vec![],
    };
    let set = read(dir, &hostile).expect("a set");
    assert!(
        set.keys().all(|path| !is_refused(std::path::Path::new(path))),
        "a declaration re-admitted a refused name: {:?}",
        set.keys().collect::<Vec<_>>()
    );

    // Two spellings of one policy agree; a real difference does not.
    let one = Policy {
        include_ignored: vec![".env".into(), "fixtures/".into()],
        exclude: vec![],
    };
    let same = Policy {
        include_ignored: vec!["fixtures".into(), "/.env".into()],
        exclude: vec![],
    };
    one.agrees_with(&same).expect("the same policy, spelled differently");

    let different = Policy {
        include_ignored: vec![".env".into()],
        exclude: vec!["src".into()],
    };
    let error = one.agrees_with(&different).expect_err("a real difference");
    assert!(error.contains("exclude"), "{error}");
}

fn run(dir: &std::path::Path, args: &[&str]) {
    let output = crate::git_backend::git_command()
        .args(args)
        .current_dir(dir)
        .output()
        .unwrap_or_else(|e| panic!("git {args:?}: {e}"));
    assert!(
        output.status.success(),
        "git {args:?}: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

/// TEST10121: a nested repository's files are not the parent's.
///
/// The fault this catches destroyed real work, twice, in minutes. A submodule
/// is a plain directory with a `.git` FILE in it, so a walk that only refuses
/// the NAME `.git` descends into it and lists its files as the parent's.
/// Continuity then carries every one of them twice — once as
/// `floom-uikit/src/x.ts` under the superproject and once as `src/x.ts` under
/// floom-uikit — with a baseline each, advancing independently.
///
/// When those two disagree, one plans a deletion for a file the other has just
/// created. Both machines apply the other's deletion. The log said it plainly
/// once there was a log: at 15:41:13 the laptop removed a file "because a
/// paired machine deleted it", and at 15:41:16 the other machine removed the
/// same path for the same stated reason. Neither had.
#[test]
fn test10121_a_scan_stops_at_a_nested_repository() {
    let temp = tempfile::tempdir().expect("tempdir");
    let dir = temp.path();
    run(dir, &["init", "-q", "-b", "main"]);
    run(dir, &["config", "user.name", "Test"]);
    run(dir, &["config", "user.email", "test@example.com"]);
    std::fs::write(dir.join("own.txt"), "the parent's\n").expect("write");

    // A submodule as it exists on disk: a directory whose `.git` is a FILE
    // pointing into the parent's modules directory.
    let inner = dir.join("inner");
    std::fs::create_dir_all(inner.join("src")).expect("mkdir");
    std::fs::write(inner.join(".git"), "gitdir: ../.git/modules/inner\n").expect("write");
    std::fs::write(inner.join("src/deep.rs"), "// the submodule's\n").expect("write");
    std::fs::write(inner.join("top.txt"), "also the submodule's\n").expect("write");

    let set = read(dir, &Policy::default()).expect("a set");
    let paths: Vec<&str> = set.keys().map(String::as_str).collect();

    assert!(paths.contains(&"own.txt"), "the parent's own file: {paths:?}");
    for claimed in ["inner/src/deep.rs", "inner/top.txt", "inner/.git"] {
        assert!(
            !paths.contains(&claimed),
            "the parent claimed {claimed}, which belongs to the repository at inner/: {paths:?}"
        );
    }

    // And a plain directory that merely has files under it is still walked —
    // the rule is "another repository", not "any directory".
    std::fs::create_dir_all(dir.join("plain")).expect("mkdir");
    std::fs::write(dir.join("plain/ordinary.txt"), "still mine\n").expect("write");
    let set = read(dir, &Policy::default()).expect("a set");
    assert!(
        set.keys().any(|path| path == "plain/ordinary.txt"),
        "an ordinary subdirectory stopped being walked: {:?}",
        set.keys().collect::<Vec<_>>()
    );
}

/// TEST10122: a repository nested at any depth stops the walk, and a directory
/// with a `.git` DIRECTORY stops it too.
///
/// A checkout inside a checkout is not always a submodule — a clone somebody
/// made inside a worktree is a repository with a real `.git` directory, and
/// its files are just as much not the parent's.
#[test]
fn test10122_a_nested_checkout_stops_the_walk_at_any_depth() {
    let temp = tempfile::tempdir().expect("tempdir");
    let dir = temp.path();
    run(dir, &["init", "-q", "-b", "main"]);
    run(dir, &["config", "user.name", "Test"]);
    run(dir, &["config", "user.email", "test@example.com"]);

    let nested = dir.join("vendor").join("thing");
    std::fs::create_dir_all(nested.join(".git")).expect("mkdir");
    std::fs::create_dir_all(nested.join("src")).expect("mkdir");
    std::fs::write(nested.join("src/lib.rs"), "// theirs\n").expect("write");
    std::fs::write(dir.join("vendor/notes.md"), "mine\n").expect("write");

    let set = read(dir, &Policy::default()).expect("a set");
    let paths: Vec<&str> = set.keys().map(String::as_str).collect();
    assert!(paths.contains(&"vendor/notes.md"), "{paths:?}");
    assert!(
        !paths.contains(&"vendor/thing/src/lib.rs"),
        "a clone inside the tree was claimed as this repository's: {paths:?}"
    );
}