mitts 0.3.21

Rust-first build and package tooling with a shared artifact cache
Documentation
/*
 * Mitts Build — Rust-first build and package tooling with shared artifact cache
 * Copyright (c) 2026 Alexander Panasenko
 *
 * Contact: alex@prod.codes
 * Author: https://prod.codes/about/
 * Project: https://github.com/alex09x/mitts
 * SPDX-License-Identifier: MIT OR Apache-2.0
 */

//! Git dependencies are cloned by Mitts itself: no Cargo checkout exists anywhere, the
//! lockfile pins a commit of a local bare repository, and `mitts build` has to bring it in
//! with the system `git`, find both packages of the repository (root and `crates/`), build,
//! hit the cache on the second run, and refuse to clone when offline. Without a lockfile,
//! Mitts resolves the tag (or the default branch) to a commit itself and writes the lock.

#![cfg(unix)]

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;

fn git(dir: &Path, args: &[&str]) -> String {
    let out = Command::new("git")
        .current_dir(dir)
        .args(["-c", "user.name=t", "-c", "user.email=t@example.invalid"])
        .args(args)
        .env("GIT_TERMINAL_PROMPT", "0")
        .output()
        .expect("git");
    assert!(
        out.status.success(),
        "git {args:?} failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8_lossy(&out.stdout).trim().to_string()
}

/// A repository with a root package `greet` and a nested `crates/inner`; returns the bare
/// clone's URL and the tagged commit.
fn make_dep_repo(root: &Path) -> (String, String) {
    let work = root.join("greet_work");
    fs::create_dir_all(work.join("src")).unwrap();
    fs::create_dir_all(work.join("crates/inner/src")).unwrap();
    fs::write(
        work.join("Cargo.toml"),
        "[package]\nname = \"greet\"\nversion = \"0.2.0\"\nedition = \"2021\"\n\n[workspace]\nmembers = [\"crates/inner\"]\n",
    )
    .unwrap();
    fs::write(
        work.join("src/lib.rs"),
        "pub fn hi() -> &'static str { \"hi from git\" }\n",
    )
    .unwrap();
    fs::write(
        work.join("crates/inner/Cargo.toml"),
        "[package]\nname = \"inner\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
    )
    .unwrap();
    fs::write(
        work.join("crates/inner/src/lib.rs"),
        "pub fn n() -> u32 { 42 }\n",
    )
    .unwrap();
    git(&work, &["init", "-q", "-b", "main"]);
    git(&work, &["add", "."]);
    git(&work, &["commit", "-q", "-m", "greet 0.2.0"]);
    // Annotated: the lockfile must pin the tagged commit, not the tag object.
    git(&work, &["tag", "-a", "v0.2.0", "-m", "release 0.2.0"]);
    let sha = git(&work, &["rev-parse", "HEAD"]);
    let bare = root.join("greet.git");
    git(root, &["clone", "-q", "--bare", "greet_work", "greet.git"]);
    (format!("file://{}", bare.display()), sha)
}

fn write_consumer(app: &Path, url: &str, sha: &str) {
    fs::create_dir_all(app.join("src")).unwrap();
    fs::write(
        app.join("Cargo.toml"),
        format!(
            "[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ngreet = {{ git = \"{url}\", tag = \"v0.2.0\" }}\ninner = {{ git = \"{url}\", tag = \"v0.2.0\" }}\n"
        ),
    )
    .unwrap();
    fs::write(
        app.join("src/main.rs"),
        "fn main() { println!(\"{} {}\", greet::hi(), inner::n()); }\n",
    )
    .unwrap();
    let source = format!("git+{url}?tag=v0.2.0#{sha}");
    fs::write(
        app.join("Cargo.lock"),
        format!(
            "# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\nversion = 3\n\n[[package]]\nname = \"app\"\nversion = \"0.1.0\"\ndependencies = [\n \"greet\",\n \"inner\",\n]\n\n[[package]]\nname = \"greet\"\nversion = \"0.2.0\"\nsource = \"{source}\"\n\n[[package]]\nname = \"inner\"\nversion = \"0.1.0\"\nsource = \"{source}\"\n"
        ),
    )
    .unwrap();
}

fn mitts(args: &[&str], app: &Path, cache: &Path, cargo_home: &Path) -> (bool, String) {
    let out = Command::new(env!("CARGO_BIN_EXE_mitts"))
        .args(args)
        .arg("--manifest-path")
        .arg(app.join("Cargo.toml"))
        .arg("--cache-dir")
        .arg(cache)
        .env("CI", "true")
        .env("CARGO_HOME", cargo_home)
        .env_remove("MITTS_CACHE_DIR")
        .output()
        .expect("run mitts");
    let text = format!(
        "{}\n{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    (out.status.success(), text)
}

fn checkout_markers(cache: &Path) -> Vec<PathBuf> {
    let mut found = Vec::new();
    let Ok(repos) = fs::read_dir(cache.join("git/checkouts")) else {
        return found;
    };
    for repo in repos.flatten() {
        for rev in fs::read_dir(repo.path()).into_iter().flatten().flatten() {
            if rev.path().join(".mitts-ok").is_file() {
                found.push(rev.path());
            }
        }
    }
    found
}

#[test]
fn a_git_dependency_is_cloned_by_mitts_without_any_cargo_checkout() {
    let temp = TempDir::new().unwrap();
    let (url, sha) = make_dep_repo(temp.path());
    let app = temp.path().join("app");
    write_consumer(&app, &url, &sha);
    let cargo_home = temp.path().join("empty_cargo_home");
    fs::create_dir_all(&cargo_home).unwrap();
    let cache = temp.path().join("cache");

    let (ok, text) = mitts(&["build"], &app, &cache, &cargo_home);
    assert!(
        ok,
        "mitts build must clone the git dependency itself:\n{text}"
    );
    let markers = checkout_markers(&cache);
    assert_eq!(
        markers.len(),
        1,
        "one checkout per locked commit: {markers:?}"
    );
    assert!(
        markers[0].ends_with(&sha[..7]),
        "checkout dir is named after the commit: {:?}",
        markers[0]
    );
    assert!(
        !cargo_home.join("git").exists(),
        "Mitts never writes into CARGO_HOME"
    );
    let bin = app.join("target/debug/app");
    let run = Command::new(&bin).output().expect("run app");
    assert_eq!(
        String::from_utf8_lossy(&run.stdout).trim(),
        "hi from git 42",
        "both packages of the repository are linked"
    );

    let (ok, text) = mitts(&["build"], &app, &cache, &cargo_home);
    assert!(ok, "{text}");
    assert!(
        text.contains("100% cache hit"),
        "second build is a full cache hit:\n{text}"
    );

    // Another consumer of the same commit shares the checkout: no second clone.
    let app2 = temp.path().join("app2");
    write_consumer(&app2, &url, &sha);
    let (ok, text) = mitts(&["build"], &app2, &cache, &cargo_home);
    assert!(ok, "{text}");
    assert_eq!(checkout_markers(&cache).len(), 1);
}

#[test]
fn offline_mode_refuses_to_clone_and_names_the_repository() {
    let temp = TempDir::new().unwrap();
    let (url, sha) = make_dep_repo(temp.path());
    let app = temp.path().join("app");
    write_consumer(&app, &url, &sha);
    let cargo_home = temp.path().join("empty_cargo_home");
    fs::create_dir_all(&cargo_home).unwrap();
    let cache = temp.path().join("cache");

    let (ok, text) = mitts(&["fetch", "--offline"], &app, &cache, &cargo_home);
    assert!(!ok, "offline fetch must not clone:\n{text}");
    assert!(
        text.contains("offline") && text.contains("greet.git"),
        "{text}"
    );
    assert!(checkout_markers(&cache).is_empty());

    let (ok, text) = mitts(&["fetch"], &app, &cache, &cargo_home);
    assert!(ok, "online fetch clones:\n{text}");
    assert_eq!(checkout_markers(&cache).len(), 1);

    let (ok, text) = mitts(&["fetch", "--offline"], &app, &cache, &cargo_home);
    assert!(ok, "offline fetch after the clone succeeds:\n{text}");
}

#[test]
fn without_a_lockfile_git_dependencies_are_pinned_and_written_to_cargo_lock() {
    let temp = TempDir::new().unwrap();
    let (url, sha) = make_dep_repo(temp.path());
    let app = temp.path().join("app");
    write_consumer(&app, &url, &sha);
    fs::remove_file(app.join("Cargo.lock")).unwrap();
    // `greet` by tag, `inner` from the default branch: both must pin the same commit.
    fs::write(
        app.join("Cargo.toml"),
        format!(
            "[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ngreet = {{ git = \"{url}\", tag = \"v0.2.0\" }}\ninner = {{ git = \"{url}\" }}\n"
        ),
    )
    .unwrap();
    let cargo_home = temp.path().join("empty_cargo_home");
    fs::create_dir_all(&cargo_home).unwrap();
    let cache = temp.path().join("cache");

    let (ok, text) = mitts(&["build"], &app, &cache, &cargo_home);
    assert!(
        ok,
        "build without Cargo.lock must resolve the git deps:\n{text}"
    );
    let lock = fs::read_to_string(app.join("Cargo.lock")).unwrap();
    assert!(
        lock.contains(&format!("source = \"git+{url}?tag=v0.2.0#{sha}\"")),
        "greet pinned by tag:\n{lock}"
    );
    assert!(
        lock.contains(&format!("source = \"git+{url}#{sha}\"")),
        "inner pinned from the default branch:\n{lock}"
    );
    assert_eq!(
        checkout_markers(&cache).len(),
        1,
        "one checkout for one commit"
    );
    let run = Command::new(app.join("target/debug/app")).output().unwrap();
    assert_eq!(
        String::from_utf8_lossy(&run.stdout).trim(),
        "hi from git 42"
    );

    // The written lockfile is accepted as is: no re-resolution, full cache hit.
    let (ok, text) = mitts(&["build"], &app, &cache, &cargo_home);
    assert!(ok && text.contains("100% cache hit"), "{text}");
    assert_eq!(fs::read_to_string(app.join("Cargo.lock")).unwrap(), lock);

    // An existing lockfile stays authoritative for `mitts build`, as for registry
    // dependencies; `mitts fetch` re-resolves when the manifest's tag, branch or rev moved.
}