wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The order a fleet is committed in, on real repositories.

use super::*;

fn git(dir: &Path, args: &[&str]) {
    let output = crate::git_backend::git_command().args(args).current_dir(dir).output().expect("git runs");
    assert!(output.status.success(), "git {args:?} in {}: {}", dir.display(), String::from_utf8_lossy(&output.stderr));
}

/// A superproject holding two sibling repositories, `a` and `b`, each a
/// project under its own alias.
fn fleet() -> tempfile::TempDir {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let root = dir.path();
    for name in ["a", "b"] {
        let repo = root.join(name);
        std::fs::create_dir_all(repo.join(".wsb")).unwrap();
        git(&repo, &["init", "--quiet", "-b", "main"]);
        std::fs::write(repo.join("file.txt"), name).unwrap();
        // The repository's settings are part of what it commits: written
        // after the commit they left every child with an untracked file, so a
        // walk found both children changed and moved the superproject for them.
        std::fs::write(
            repo.join(".wsb").join("repo.json"),
            format!(
                r#"{{"name":"{name}","alias":"{name}","templates_root":"..","version":{{"major":1,"file":"version.txt","detect_manifests":true}}}}"#
            ),
        )
        .unwrap();
        git(&repo, &["add", "file.txt", ".wsb/repo.json"]);
        git(&repo, &["-c", "user.name=t", "-c", "user.email=t@t", "commit", "--quiet", "-m", name]);
    }
    git(root, &["init", "--quiet", "-b", "main"]);
    std::fs::write(
        root.join(".gitmodules"),
        "[submodule \"a\"]\n\tpath = a\n\turl = ./a\n[submodule \"b\"]\n\tpath = b\n\turl = ./b\n",
    )
    .unwrap();
    dir
}

fn order(root: &Path) -> std::result::Result<Vec<String>, String> {
    let tree = SubmoduleTree::discover(root).map_err(|e| e.to_string())?;
    commit_order(&tree, root)
        .map(|nodes| nodes.iter().map(|node| node.label().to_string()).collect())
        .map_err(|e| e.to_string())
}

/// TEST12578: a repository is committed after every repository whose
/// version its templates read.
///
/// A commit moves its repository's version and every commit renders, so a
/// repository committed first rendered from a version that moved a moment
/// later. Ordered by nesting alone, every checkpoint committed floom-uikit's
/// lock against capdag-js's previous version and the next checkpoint
/// committed the catch-up. Siblings keep their order unless one reads the
/// other; reading each other leaves no order, and is refused naming both.
#[test]
fn test12578_a_repository_commits_after_those_it_reads() {
    let fleet = fleet();
    let root = fleet.path();
    let a_reads_b = root.join("a").join("pin.txt.wstemplate");
    let b_reads_a = root.join("b").join("pin.txt.wstemplate");

    std::fs::write(&a_reads_b, "{{ projects.b.version }}\n").unwrap();
    assert_eq!(order(root).unwrap(), vec!["b", "a"], "a was committed before the b it reads");
    std::fs::remove_file(&a_reads_b).unwrap();

    std::fs::write(&b_reads_a, "{{ projects.a.version }}\n").unwrap();
    assert_eq!(order(root).unwrap(), vec!["a", "b"], "b was committed before the a it reads");

    std::fs::write(&a_reads_b, "{{ projects.b.version }}\n").unwrap();
    let refused = order(root).unwrap_err();
    assert!(refused.contains("a") && refused.contains("b") && refused.contains("no order"), "{refused}");
}

fn head(dir: &Path) -> String {
    let output = crate::git_backend::git_command().args(["rev-parse", "HEAD"]).current_dir(dir).output().expect("git runs");
    String::from_utf8_lossy(&output.stdout).trim().to_string()
}

/// TEST12583: a clean superproject whose children did not move is not
/// committed — not even by a pre-commit hook that would make a change.
///
/// Git runs the pre-commit hook before it decides there is nothing to commit,
/// and wsb's hook renders the version a commit being made gets and stages it.
/// Handed to `git commit` unconditionally, a clean superproject came out of
/// every checkpoint with a commit of nothing but its own version. The hook
/// here stands in for that render; with something of the superproject's own
/// to commit, it runs and the commit is made.
#[test]
fn test12583_a_clean_superproject_is_not_committed() {
    let fleet = fleet();
    let root = fleet.path();
    git(root, &["config", "user.name", "t"]);
    git(root, &["config", "user.email", "t@t"]);
    git(root, &["add", ".gitmodules", "a", "b"]);
    git(root, &["commit", "--quiet", "-m", "fleet"]);
    let hook = root.join(".git").join("hooks").join("pre-commit");
    std::fs::create_dir_all(hook.parent().unwrap()).unwrap();
    std::fs::write(&hook, "#!/bin/sh\ndate +%s%N > version.txt\ngit add version.txt\n").unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&hook, std::fs::Permissions::from_mode(0o755)).unwrap();
    }
    let before = head(root);

    execute(root, "nothing", false, true, &[], &[], false, false, false).expect("the walk runs");
    assert_eq!(head(root), before, "a clean superproject was committed");
    assert!(!root.join("version.txt").exists(), "the hook ran for a commit that had nothing to record");

    std::fs::write(root.join("notes.txt"), "an edit at the root\n").unwrap();
    execute(root, "an edit", false, true, &[], &[], false, false, false).expect("the walk runs");
    assert_ne!(head(root), before, "the superproject's own edit was not committed");
}