wsb 0.258.6441

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The first baseline: what two machines already agree on before they have
//! agreed on anything.

use crate::peer::{converge, scan, seed};
use std::path::Path;

fn run(dir: &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)
    );
}

/// TEST9980: the committed tree is the baseline, and it matches what a scan of
/// a clean checkout produces.
///
/// This is the property the whole seeding idea rests on: if the two disagreed,
/// a fresh relationship would report every committed file as changed —
/// carrying a repository's worth of files that were already identical, or
/// conflicting on every one of them.
#[test]
fn test9980_the_seed_matches_a_scan_of_the_committed_tree() {
    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::create_dir_all(dir.join("src/deep")).expect("mkdir");
    std::fs::write(dir.join("README.md"), "workspace\n").expect("write");
    std::fs::write(dir.join("src/main.rs"), "fn main() {}\n").expect("write");
    std::fs::write(dir.join("src/deep/nested.txt"), "deep\n").expect("write");
    std::fs::write(dir.join("run.sh"), "#!/bin/sh\necho hi\n").expect("write");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(dir.join("run.sh"), std::fs::Permissions::from_mode(0o755))
            .expect("chmod");
    }
    #[cfg(unix)]
    std::os::unix::fs::symlink("src/main.rs", dir.join("link.rs")).expect("symlink");
    run(dir, &["add", "."]);
    run(dir, &["commit", "-qm", "one"]);

    let seeded = seed::from_head(dir).expect("a seed");
    let scanned = scan::read(dir, &scan::Policy::default()).expect("a scan");

    assert_eq!(
        seeded.keys().collect::<Vec<_>>(),
        scanned.keys().collect::<Vec<_>>(),
        "the seed and a scan of the same clean tree must cover the same paths"
    );
    for (path, entry) in &seeded {
        let found = scanned.get(path).expect("the same path");
        assert!(
            entry.same_as(found),
            "{path}: the seed says {entry:?} and the tree holds {found:?}"
        );
    }

    // The details that decide convergence, spelled out.
    #[cfg(unix)]
    {
        assert!(seeded["run.sh"].executable, "the executable bit is part of the seed");
        assert_eq!(seeded["link.rs"].kind, converge::EntryKind::Symlink);
        assert_eq!(
            seeded["link.rs"].hash,
            converge::hash_bytes(b"src/main.rs"),
            "a link is seeded as its target text, like a scan reads it"
        );
    }
}

/// TEST9981: work in progress is NOT in the seed.
///
/// The seed is what both machines already agree on. An uncommitted edit is
/// exactly what they do not agree on — it is the thing being carried — so a
/// seed that included it would make the change look like it had already
/// travelled.
#[test]
fn test9981_uncommitted_work_is_not_agreed_and_not_seeded() {
    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("committed.txt"), "in the commit\n").expect("write");
    run(dir, &["add", "."]);
    run(dir, &["commit", "-qm", "one"]);

    std::fs::write(dir.join("committed.txt"), "edited since\n").expect("write");
    std::fs::write(dir.join("new.txt"), "never committed\n").expect("write");

    let seeded = seed::from_head(dir).expect("a seed");
    assert_eq!(
        seeded["committed.txt"].hash,
        converge::hash_bytes(b"in the commit\n"),
        "the seed holds the committed content, not what is on disk now"
    );
    assert!(
        !seeded.contains_key("new.txt"),
        "an uncommitted file is not something both machines have agreed on"
    );

    // Which is what makes the ordinary case a one-sided change rather than a
    // conflict: this machine differs from the base, the peer does not.
    let here = scan::read(dir, &scan::Policy::default()).expect("a scan");
    let there = seeded.clone();
    let plan = converge::plan(&here, &there, &seeded);
    assert!(
        plan.actions.iter().all(|action| !action.is_conflict()),
        "a fresh relationship must not conflict on committed files: {:?}",
        plan.actions
    );
    assert_eq!(plan.pending(), 2, "both the edit and the new file are carried");
}

/// TEST9982: a repository with no commits seeds nothing, and says so by being
/// empty rather than by failing.
#[test]
fn test9982_an_unborn_branch_seeds_an_empty_baseline() {
    let temp = tempfile::tempdir().expect("tempdir");
    let dir = temp.path();
    run(dir, &["init", "-q", "-b", "main"]);
    assert!(seed::from_head(dir).expect("a seed").is_empty());
}

/// TEST9983: a submodule's files belong to the submodule's baseline, not the
/// superproject's.
///
/// The superproject records a commit id for a submodule, not its contents. A
/// seed that walked into it would put the same files in two baselines, and
/// every change to them would then be decided twice.
#[test]
fn test9983_a_submodule_is_not_part_of_its_parents_seed() {
    let temp = tempfile::tempdir().expect("tempdir");
    let child = temp.path().join("child");
    std::fs::create_dir_all(&child).expect("mkdir");
    run(&child, &["init", "-q", "-b", "main"]);
    run(&child, &["config", "user.name", "Test"]);
    run(&child, &["config", "user.email", "test@example.com"]);
    std::fs::write(child.join("inner.txt"), "child\n").expect("write");
    run(&child, &["add", "."]);
    run(&child, &["commit", "-qm", "child"]);

    let parent = temp.path().join("parent");
    std::fs::create_dir_all(&parent).expect("mkdir");
    run(&parent, &["init", "-q", "-b", "main"]);
    run(&parent, &["config", "user.name", "Test"]);
    run(&parent, &["config", "user.email", "test@example.com"]);
    std::fs::write(parent.join("top.txt"), "parent\n").expect("write");
    run(&parent, &["add", "."]);
    run(&parent, &["commit", "-qm", "parent"]);
    run(
        &parent,
        &[
            "-c",
            "protocol.file.allow=always",
            "submodule",
            "add",
            "-q",
            child.to_str().expect("path"),
            "child",
        ],
    );
    run(&parent, &["commit", "-qm", "add child"]);

    let seeded = seed::from_head(&parent).expect("a seed");
    assert!(seeded.contains_key("top.txt"));
    assert!(
        !seeded.keys().any(|path| path.starts_with("child/")),
        "the submodule's files reached the superproject's baseline: {:?}",
        seeded.keys().collect::<Vec<_>>()
    );
}