use super::*;
use crate::tracker::fixtures::{
binding, checkout, commit, env_held, env_top, legacy_remote, local_unpushed,
remote_with_branch, remote_with_config, store_clone, tip, BRANCH,
};
use tempfile::TempDir;
#[test]
fn sync_fast_forwards_store_onto_the_advanced_remote() {
let tmp = TempDir::new().unwrap();
let remote = remote_with_branch(tmp.path());
let store = store_clone(tmp.path(), &remote);
let other = checkout(tmp.path(), &remote, "other");
let moved = commit(&other, "next.txt", "next");
git(&other, &["push", "-q", "origin", BRANCH]).unwrap();
sync(&binding(Some(&remote), &store), &env_top()).unwrap();
assert_eq!(tip(&store, "HEAD"), moved);
}
#[test]
fn sync_of_an_upstream_less_branch_is_a_no_op_the_landing_for_free() {
let tmp = TempDir::new().unwrap();
let remote = remote_with_branch(tmp.path());
let store = store_clone(tmp.path(), &remote);
let before = tip(&store, "HEAD");
for upstream_less in [crate::LANDING_BRANCH, "work/bl-0000"] {
let mut b = binding(Some(&remote), &store);
b.tasks_branch = upstream_less.into();
sync(&b, &env_top()).unwrap();
assert_eq!(tip(&store, "HEAD"), before);
}
}
#[test]
fn sync_of_a_non_checked_out_branch_ffs_that_branch_not_the_checkout() {
let tmp = TempDir::new().unwrap();
let remote = remote_with_branch(tmp.path());
let store = store_clone(tmp.path(), &remote);
let seat = checkout(tmp.path(), &remote, "seat");
git(&seat, &["checkout", "-q", "-b", "other"]).unwrap();
let moved = commit(&seat, "other.txt", "other");
git(&seat, &["push", "-q", "origin", "other"]).unwrap();
let head = tip(&store, "HEAD");
let mut b = binding(Some(&remote), &store);
b.tasks_branch = "other".into();
sync(&b, &env_top()).unwrap();
assert_eq!(tip(&store, "other"), moved); assert_eq!(tip(&store, "HEAD"), head); }
#[test]
fn sync_in_stealth_is_a_no_op() {
let tmp = TempDir::new().unwrap();
let remote = remote_with_branch(tmp.path());
let store = store_clone(tmp.path(), &remote);
let before = tip(&store, "HEAD");
sync(&binding(None, &store), &env_top()).unwrap();
assert_eq!(tip(&store, "HEAD"), before);
}
#[test]
fn sync_rebases_a_diverged_stores_seals_onto_the_remote_and_publishes() {
let tmp = TempDir::new().unwrap();
let remote = remote_with_branch(tmp.path());
let store = store_clone(tmp.path(), &remote);
let held1 = commit(&store, "tasks/bl-1111.md", "local one");
commit(&store, "tasks/bl-2222.md", "local two");
let other = checkout(tmp.path(), &remote, "other");
commit(&other, "tasks/bl-3333.md", "remote");
git(&other, &["push", "-q", "origin", BRANCH]).unwrap();
sync(&binding(Some(&remote), &store), &env_top()).unwrap();
let head = tip(&store, "HEAD");
assert_eq!(tip(&remote, BRANCH), head, "the rebased seals published");
assert!(git(&store, &["merge-base", "--is-ancestor", &held1, "HEAD"]).is_err(), "the old seal sha is gone");
for f in ["bl-1111", "bl-2222", "bl-3333"] {
assert!(git(&store, &["cat-file", "-e", &format!("HEAD:tasks/{f}.md")]).is_ok(), "{f}");
}
let log = git(&store, &["log", "--format=%s", "-3"]).unwrap();
assert_eq!(log.lines().collect::<Vec<_>>(), ["local two", "local one", "remote"]);
}
#[test]
fn sync_refuses_a_same_ball_conflict_by_name_and_leaves_every_seal_local() {
let tmp = TempDir::new().unwrap();
let remote = remote_with_branch(tmp.path());
let store = store_clone(tmp.path(), &remote);
commit(&store, "tasks/bl-4444.md", "unrelated local seal");
let held = commit(&store, "tasks/bl-5555.md", "mine");
let other = checkout(tmp.path(), &remote, "other");
commit(&other, "tasks/bl-5555.md", "theirs");
git(&other, &["push", "-q", "origin", BRANCH]).unwrap();
let published = tip(&remote, BRANCH);
let err = sync(&binding(Some(&remote), &store), &env_top()).unwrap_err().to_string();
assert!(err.contains(&format!("`{BRANCH}` moved and bl-5555 changed on both sides")), "{err}");
assert!(err.contains("nothing was published and nothing local was changed"), "{err}");
assert!(err.contains(&format!("the store checkout, {}: `git log FETCH_HEAD..{BRANCH}` lists them", store.display())), "{err}");
assert!(err.contains("rebase FETCH_HEAD`, resolve the named file, then `bl sync` publishes"), "{err}");
assert!(err.contains("reset --hard FETCH_HEAD"), "{err}");
assert!(!err.contains("CONFLICT") && !err.contains("hint:"), "raw git leaked: {err}");
assert_eq!(tip(&store, "HEAD"), held, "every local seal stays local");
assert_eq!(tip(&remote, BRANCH), published);
assert!(git(&store, &["rev-parse", "--verify", "-q", "REBASE_HEAD"]).is_err(), "aborted, not wedged");
git(&store, &["reset", "-q", "--hard", "FETCH_HEAD"]).unwrap();
sync(&binding(Some(&remote), &store), &env_top()).unwrap();
assert_eq!(tip(&store, "HEAD"), published);
}
#[test]
fn sync_to_an_unreachable_remote_fails_open() {
let tmp = TempDir::new().unwrap();
let store = local_unpushed(tmp.path());
let before = tip(&store, "HEAD");
let gone = tmp.path().join("no-such-remote.git");
sync(&binding(Some(&gone), &store), &env_top()).unwrap();
assert_eq!(tip(&store, "HEAD"), before);
}
#[test]
fn sync_from_a_nested_op_rebases_but_leaves_the_push_to_the_enclosing_op() {
let tmp = TempDir::new().unwrap();
let remote = remote_with_branch(tmp.path());
let store = store_clone(tmp.path(), &remote);
commit(&store, "tasks/bl-6666.md", "local");
let published = tip(&remote, BRANCH);
sync(&binding(Some(&remote), &store), &env_held(&[&store, &store])).unwrap();
assert_eq!(tip(&remote, BRANCH), published, "not this op's to publish");
assert!(git(&store, &["cat-file", "-e", "HEAD:tasks/bl-6666.md"]).is_ok());
}
#[test]
fn sync_skips_a_not_yet_cut_over_legacy_upstream_instead_of_failing() {
let tmp = TempDir::new().unwrap();
let remote = legacy_remote(tmp.path());
let store = local_unpushed(tmp.path()); let before = tip(&store, "HEAD");
sync(&binding(Some(&remote), &store), &env_top()).unwrap();
assert_eq!(tip(&store, "HEAD"), before);
}
#[test]
fn fetch_config_brings_the_centers_config_to_the_landing_fetch_head() {
let tmp = TempDir::new().unwrap();
let center = remote_with_config(tmp.path(), "balls/shared");
let landing = local_unpushed(tmp.path()); let mut b = binding(Some(¢er), &landing);
b.landing = landing.to_string_lossy().into_owned();
fetch_config(&b).unwrap();
let cfg = git(&landing, &["show", "FETCH_HEAD:config/balls.toml"]).unwrap();
assert!(cfg.contains("balls/shared"), "fetched config: {cfg}");
}
#[test]
fn fetch_config_in_stealth_is_a_no_op() {
let tmp = TempDir::new().unwrap();
let landing = local_unpushed(tmp.path());
let mut b = binding(None, &landing);
b.landing = landing.to_string_lossy().into_owned();
fetch_config(&b).unwrap(); assert!(git(&landing, &["rev-parse", "FETCH_HEAD"]).is_err());
}
#[test]
fn fetch_config_when_the_remote_lacks_the_landing_is_a_no_op() {
let tmp = TempDir::new().unwrap();
let remote = remote_with_branch(tmp.path()); let landing = local_unpushed(tmp.path());
let mut b = binding(Some(&remote), &landing);
b.landing = landing.to_string_lossy().into_owned();
fetch_config(&b).unwrap(); assert!(git(&landing, &["rev-parse", "FETCH_HEAD"]).is_err());
}
#[test]
fn sync_refuses_an_option_like_branch_before_touching_git() {
let tmp = TempDir::new().unwrap();
let remote = remote_with_branch(tmp.path());
let store = store_clone(tmp.path(), &remote);
let mut b = binding(Some(&remote), &store);
b.tasks_branch = "--upload-pack=evil".into();
let err = sync(&b, &env_top()).unwrap_err().to_string();
assert!(err.contains("looks like an option"), "{err}");
}