use loopflow::ops::{
plan_rebase, rebase_with_recovery, NullProgress, OpsError, RebaseClass, RebaseOptions,
RebaseStrategy,
};
use loopflow_test_support::TestRepo;
use std::process::Command;
fn git(repo: &std::path::Path, args: &[&str]) -> String {
let output = Command::new("git")
.args(args)
.current_dir(repo)
.output()
.unwrap();
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
#[test]
fn rebase_onto_main_succeeds() {
let repo = TestRepo::new();
repo.create_branch("feature");
repo.create_file("feature.txt", "feature");
repo.stage_all();
repo.commit("feature work");
repo.checkout("main");
repo.create_file("main.txt", "main");
repo.stage_all();
repo.commit("main work");
repo.push();
repo.checkout("feature");
rebase_with_recovery(
repo.path(),
&RebaseOptions {
onto: "origin/main".to_string(),
push: false,
},
&NullProgress,
)
.expect("rebase");
}
#[test]
fn rebase_conflict_returns_error() {
let repo = TestRepo::new();
repo.create_branch("feature");
repo.create_file("conflict.txt", "feature line\n");
repo.stage_all();
repo.commit("feature work");
repo.checkout("main");
repo.create_file("conflict.txt", "main line\n");
repo.stage_all();
repo.commit("main work");
repo.push();
repo.checkout("feature");
let result = rebase_with_recovery(
repo.path(),
&RebaseOptions {
onto: "origin/main".to_string(),
push: false,
},
&NullProgress,
);
assert!(
matches!(result, Err(OpsError::RebaseConflict { ref onto, .. }) if onto == "origin/main"),
"expected rebase conflict, got {result:?}"
);
}
#[test]
fn rebase_after_squash_merge_replays_only_unique_work() {
let repo = TestRepo::new();
repo.create_branch("parent");
repo.create_file("a1.txt", "a1");
repo.stage_all();
repo.commit("a1");
repo.create_file("a2.txt", "a2");
repo.stage_all();
repo.commit("a2");
repo.create_branch("feature");
repo.create_file("feature.txt", "feature");
repo.stage_all();
repo.commit("feature work");
repo.checkout("main");
git(repo.path(), &["merge", "--squash", "parent"]);
git(repo.path(), &["commit", "-m", "squash parent"]);
repo.push();
repo.checkout("feature");
rebase_with_recovery(
repo.path(),
&RebaseOptions {
onto: "origin/main".to_string(),
push: false,
},
&NullProgress,
)
.expect("rebase after squash merge");
assert!(repo.path().join("feature.txt").exists());
assert_eq!(
git(repo.path(), &["diff", "--name-only", "origin/main...HEAD"]),
"feature.txt"
);
}
#[test]
fn dotted_branch_names_do_not_imply_a_parent() {
let repo = TestRepo::new();
repo.create_branch("jack/a.b");
repo.create_file("feature.txt", "feature");
repo.stage_all();
repo.commit("feature work");
let plan = plan_rebase(repo.path(), None).expect("plan rebase");
assert_eq!(plan.base_ref, "origin/main");
assert_eq!(plan.class, RebaseClass::CleanAuthored);
assert_eq!(plan.strategy, RebaseStrategy::DirectRebase);
}
#[test]
fn explicit_onto_is_the_only_alternate_base() {
let repo = TestRepo::new();
repo.create_branch("alternate");
repo.create_file("alternate.txt", "alternate");
repo.stage_all();
repo.commit("alternate work");
repo.create_branch("feature");
let plan = plan_rebase(repo.path(), Some("alternate")).expect("plan rebase");
assert_eq!(plan.base_ref, "alternate");
}
#[test]
fn dirty_scratch_only_branch_resets_to_base() {
let repo = TestRepo::new();
repo.create_branch("feature");
repo.create_file("scratch/design.md", "notes");
let plan = plan_rebase(repo.path(), None).expect("plan rebase");
assert_eq!(plan.class, RebaseClass::ScratchOnly);
assert_eq!(plan.strategy, RebaseStrategy::ResetToBase);
assert_eq!(plan.unique_commits, 0);
assert!(!plan.protected);
}
#[test]
fn modified_scratch_file_keeps_its_leading_path_character() {
let repo = TestRepo::new();
repo.create_file("scratch/notes.md", "original\n");
repo.stage_all();
repo.commit("add scratch notes");
repo.push();
repo.create_branch("feature");
repo.create_file("scratch/notes.md", "evolved\n");
let plan = plan_rebase(repo.path(), None).expect("plan rebase");
assert_eq!(plan.class, RebaseClass::ScratchOnly);
assert!(plan
.changed_files
.iter()
.all(|path| path.starts_with("scratch")));
}
#[test]
fn wave_changes_are_protected() {
let repo = TestRepo::new();
repo.create_branch("feature");
repo.create_file("wave/goals/MEMORY.md", "state");
repo.stage_all();
repo.commit("update wave memory");
let plan = plan_rebase(repo.path(), None).expect("plan rebase");
assert_eq!(plan.class, RebaseClass::Protected);
assert_eq!(plan.strategy, RebaseStrategy::DirectRebase);
assert!(plan.protected);
}