#![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()
}
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"]);
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}"
);
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();
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"
);
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);
}