use git2::Oid;
use super::*;
fn oid(hex: &str) -> Oid {
let padded = format!("{:0<40}", hex);
Oid::from_str(&padded).unwrap()
}
fn make_commit(hex: &str, message: &str) -> CommitEntry {
CommitEntry {
oid: oid(hex),
short_hash: hex[..7.min(hex.len())].to_string(),
message: message.to_string(),
command: Command::Pick,
update_refs: Vec::new(),
}
}
fn make_commit_with_refs(hex: &str, message: &str, refs: Vec<&str>) -> CommitEntry {
CommitEntry {
oid: oid(hex),
short_hash: hex[..7.min(hex.len())].to_string(),
message: message.to_string(),
command: Command::Pick,
update_refs: refs.into_iter().map(String::from).collect(),
}
}
const BASE: &str = "ba5e000";
const OID_A1: &str = "abc1234";
const OID_A2: &str = "def5678";
const OID_B1: &str = "bbb2222";
const OID_INT: &str = "1111111";
const OID_C1: &str = "ccc3333";
const OID_C2: &str = "ddd4444";
const OID_MERGE1: &str = "9999999";
const OID_MERGE2: &str = "8888888";
const OID_FIX: &str = "eee5555";
#[test]
fn serialize_single_branch_section() {
let graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1"), make_commit(OID_A2, "A2")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_INT, "Int")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
let todo = graph.to_todo();
let lines: Vec<&str> = todo.lines().collect();
assert_eq!(lines[0], "label onto");
assert_eq!(lines[1], "");
assert_eq!(lines[2], "reset onto");
assert_eq!(lines[3], &format!("pick {} # A1", OID_A1));
assert_eq!(lines[4], &format!("pick {} # A2", OID_A2));
assert_eq!(lines[5], "label feature-a");
assert_eq!(lines[6], "update-ref refs/heads/feature-a");
assert_eq!(lines[7], "");
assert_eq!(lines[8], "reset onto");
assert_eq!(lines[9], &format!("pick {} # Int", OID_INT));
assert!(lines[10].starts_with(&format!("merge -C {} feature-a", OID_MERGE1)));
}
#[test]
fn serialize_two_branch_sections() {
let graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
},
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_B1, "B1")],
label: "feature-b".to_string(),
branch_names: vec!["feature-b".to_string()],
},
],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_INT, "Int")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE2)),
label: "feature-b".to_string(),
},
],
base_refs: vec![],
};
let todo = graph.to_todo();
assert!(todo.contains("label feature-a\n"));
assert!(todo.contains("label feature-b\n"));
assert!(todo.contains(&format!("merge -C {} feature-a", OID_MERGE1)));
assert!(todo.contains(&format!("merge -C {} feature-b", OID_MERGE2)));
}
#[test]
fn serialize_new_merge_without_oid() {
let graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: None,
label: "feature-a".to_string(),
}],
base_refs: vec![],
};
let todo = graph.to_todo();
assert!(todo.contains("merge feature-a # Merge branch 'feature-a'"));
}
#[test]
fn serialize_colocated_branches() {
let graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string(), "feature-b".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
}],
base_refs: vec![],
};
let todo = graph.to_todo();
assert!(todo.contains("update-ref refs/heads/feature-a\n"));
assert!(todo.contains("update-ref refs/heads/feature-b\n"));
}
#[test]
fn serialize_update_refs_on_integration_line() {
let graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![IntegrationEntry::Pick(make_commit_with_refs(
OID_C1,
"C1",
vec!["non-woven"],
))],
base_refs: vec![],
};
let todo = graph.to_todo();
assert!(todo.contains(&format!("pick {} # C1\n", OID_C1)));
assert!(todo.contains("update-ref refs/heads/non-woven\n"));
}
#[test]
fn serialize_empty_graph() {
let graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![],
base_refs: vec![],
};
let todo = graph.to_todo();
assert_eq!(todo, "label onto\n\nreset onto\n");
}
#[test]
fn drop_commit_from_branch_section() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1"), make_commit(OID_A2, "A2")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
}],
base_refs: vec![],
};
assert!(graph.drop_commit(oid(OID_A1), EmptiedRefs::Park).is_some());
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.branch_sections[0].commits.len(), 1);
assert_eq!(graph.branch_sections[0].commits[0].message, "A2");
}
#[test]
fn drop_last_commit_removes_section_and_merge() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_INT, "Int")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
assert_eq!(
graph.drop_commit(oid(OID_A1), EmptiedRefs::Park),
Some(vec!["feature-a".to_string()])
);
assert!(graph.branch_sections.is_empty());
assert_eq!(graph.integration_line.len(), 1); assert_eq!(graph.base_refs, vec!["feature-a".to_string()]);
let todo = graph.to_todo();
assert!(
todo.contains("reset onto\nupdate-ref refs/heads/feature-a\n"),
"parked ref must follow `reset onto`: {todo}"
);
}
#[test]
fn drop_last_commit_detached_leaves_section_refs_out() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string(), "twin".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
}],
base_refs: vec![],
};
assert_eq!(
graph.drop_commit(oid(OID_A1), EmptiedRefs::Detach),
Some(vec!["feature-a".to_string(), "twin".to_string()])
);
assert!(graph.base_refs.is_empty());
assert!(!graph.to_todo().contains("update-ref"));
}
#[test]
fn drop_last_commit_of_stacked_section_parks_at_parent_tip() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
},
BranchSection {
reset_target: "feature-a".to_string(),
commits: vec![make_commit(OID_B1, "B1")],
label: "feature-b".to_string(),
branch_names: vec!["feature-b".to_string()],
},
],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-b".to_string(),
}],
base_refs: vec![],
};
assert_eq!(
graph.drop_commit(oid(OID_B1), EmptiedRefs::Park),
Some(vec!["feature-b".to_string()])
);
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(
graph.branch_sections[0].commits[0].update_refs,
vec!["feature-b".to_string()]
);
assert!(graph.base_refs.is_empty());
assert!(graph.integration_line.is_empty());
}
#[test]
fn drop_commit_from_integration_line() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Pick(make_commit(OID_C2, "C2")),
],
base_refs: vec![],
};
assert!(graph.drop_commit(oid(OID_C1), EmptiedRefs::Park).is_some());
assert_eq!(graph.integration_line.len(), 1);
}
#[test]
fn drop_unknown_commit_or_branch_is_refused() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![IntegrationEntry::Pick(make_commit(OID_C1, "C1"))],
base_refs: vec![],
};
assert!(graph.drop_commit(oid("dead"), EmptiedRefs::Park).is_none());
assert!(!graph.drop_branch("no-such-branch"));
assert!(!graph.reassign_branch("no-such-branch", "feature-a"));
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.integration_line.len(), 1);
}
#[test]
fn branch_drop_size_excludes_commits_the_integration_line_keeps() {
let graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![
make_commit(OID_INT, "Int"),
make_commit(OID_A1, "A1"),
make_commit(OID_A2, "A2"),
],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_INT, "Int")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
assert_eq!(graph.branch_drop_size("feature-a"), Some(2));
}
#[test]
fn drop_branch_removes_section_and_merge() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
},
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_B1, "B1")],
label: "feature-b".to_string(),
branch_names: vec!["feature-b".to_string()],
},
],
integration_line: vec![
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE2)),
label: "feature-b".to_string(),
},
],
base_refs: vec![],
};
assert_eq!(graph.branch_drop_size("feature-a"), Some(1));
assert_eq!(graph.branch_drop_size("no-such-branch"), None);
assert!(graph.drop_branch("feature-a"));
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.branch_sections[0].label, "feature-b");
assert_eq!(graph.integration_line.len(), 1);
}
#[test]
fn move_commit_to_branch() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
graph.move_commit(oid(OID_C1), "feature-a").unwrap();
assert_eq!(graph.branch_sections[0].commits.len(), 2);
assert_eq!(graph.branch_sections[0].commits[1].message, "C1");
assert_eq!(graph.integration_line.len(), 1);
}
#[test]
fn move_commit_leaves_inner_refs_behind() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
commits: vec![
make_commit_with_refs(OID_A1, "A1", vec!["inner"]),
make_commit(OID_A2, "A2"),
],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
},
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_B1, "B1")],
label: "feature-b".to_string(),
branch_names: vec!["feature-b".to_string()],
},
],
integration_line: vec![
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE2)),
label: "feature-b".to_string(),
},
],
base_refs: vec![],
};
let parked = graph.move_commit(oid(OID_A1), "feature-b").unwrap();
assert_eq!(parked, vec!["inner".to_string()]);
let moved = &graph.branch_sections[1].commits[1];
assert_eq!(moved.message, "A1");
assert!(moved.update_refs.is_empty(), "inner must not follow A1");
assert_eq!(graph.base_refs, vec!["inner".to_string()]);
let parked = graph.move_commit(oid(OID_A2), "feature-b").unwrap();
assert_eq!(parked, vec!["feature-a".to_string()]);
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.branch_sections[0].label, "feature-b");
assert_eq!(
graph.base_refs,
vec!["inner".to_string(), "feature-a".to_string()]
);
assert_eq!(graph.integration_line.len(), 1);
assert!(matches!(
&graph.integration_line[0],
IntegrationEntry::Merge { label, .. } if label == "feature-b"
));
}
#[test]
fn move_commit_already_the_inner_tip_is_a_no_op() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![
make_commit_with_refs(OID_A1, "A1", vec!["inner"]),
make_commit(OID_A2, "A2"),
],
label: "outer".to_string(),
branch_names: vec!["outer".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "outer".to_string(),
}],
base_refs: vec![],
};
let before = graph.to_todo();
assert!(graph.move_commit(oid(OID_A1), "inner").unwrap().is_empty());
assert_eq!(graph.to_todo(), before, "the graph must be untouched");
assert!(graph.base_refs.is_empty(), "inner must not be parked");
}
#[test]
fn move_commits_to_inner_branch_keeps_their_order() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![
make_commit_with_refs(OID_A1, "A1", vec!["inner"]),
make_commit(OID_A2, "A2"),
],
label: "outer".to_string(),
branch_names: vec!["outer".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Pick(make_commit(OID_C2, "C2")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "outer".to_string(),
},
],
base_refs: vec![],
};
for hex in [OID_C1, OID_C2] {
assert!(graph.move_commit(oid(hex), "inner").unwrap().is_empty());
}
let commits = &graph.branch_sections[0].commits;
let messages: Vec<&str> = commits.iter().map(|c| c.message.as_str()).collect();
assert_eq!(messages, vec!["A1", "C1", "C2", "A2"]);
assert!(commits[0].update_refs.is_empty());
assert!(commits[1].update_refs.is_empty());
assert_eq!(commits[2].update_refs, vec!["inner".to_string()]);
assert!(commits[3].update_refs.is_empty());
assert_eq!(graph.integration_line.len(), 1);
}
#[test]
fn move_commit_to_inner_branch_inserts_after_its_tip() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
commits: vec![
make_commit_with_refs(OID_A1, "A1", vec!["inner", "twin"]),
make_commit(OID_A2, "A2"),
],
label: "outer".to_string(),
branch_names: vec!["outer".to_string()],
},
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_B1, "B1")],
label: "other".to_string(),
branch_names: vec!["other".to_string()],
},
],
integration_line: vec![
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "outer".to_string(),
},
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE2)),
label: "other".to_string(),
},
],
base_refs: vec![],
};
let parked = graph.move_commit(oid(OID_B1), "inner").unwrap();
assert_eq!(parked, vec!["other".to_string()]);
assert_eq!(graph.base_refs, vec!["other".to_string()]);
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.integration_line.len(), 1);
let commits = &graph.branch_sections[0].commits;
let messages: Vec<&str> = commits.iter().map(|c| c.message.as_str()).collect();
assert_eq!(messages, vec!["A1", "B1", "A2"]);
assert_eq!(commits[0].update_refs, vec!["twin".to_string()]);
assert_eq!(commits[1].update_refs, vec!["inner".to_string()]);
assert!(commits[2].update_refs.is_empty());
assert_eq!(graph.branch_sections[0].label, "outer");
}
#[test]
fn move_commit_to_inner_branch_from_the_same_section() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![
make_commit_with_refs(OID_A1, "A1", vec!["inner"]),
make_commit(OID_A2, "A2"),
make_commit(OID_B1, "B1"),
],
label: "outer".to_string(),
branch_names: vec!["outer".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "outer".to_string(),
}],
base_refs: vec![],
};
let parked = graph.move_commit(oid(OID_B1), "inner").unwrap();
assert!(parked.is_empty());
let commits = &graph.branch_sections[0].commits;
let messages: Vec<&str> = commits.iter().map(|c| c.message.as_str()).collect();
assert_eq!(messages, vec!["A1", "B1", "A2"]);
assert!(commits[0].update_refs.is_empty());
assert_eq!(commits[1].update_refs, vec!["inner".to_string()]);
let before = graph.to_todo();
let parked = graph.move_commit(oid(OID_B1), "inner").unwrap();
assert!(parked.is_empty());
assert_eq!(graph.to_todo(), before);
}
#[test]
fn move_commit_to_colocated_branch_splits_section() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string(), "feature-b".to_string()],
},
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_B1, "B1"), make_commit(OID_C1, "C1")],
label: "feature-c".to_string(),
branch_names: vec!["feature-c".to_string()],
},
],
integration_line: vec![
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE2)),
label: "feature-c".to_string(),
},
],
base_refs: vec![],
};
graph.move_commit(oid(OID_C1), "feature-b").unwrap();
assert_eq!(graph.branch_sections.len(), 3);
assert_eq!(graph.branch_sections[0].label, "feature-a");
assert_eq!(graph.branch_sections[0].commits.len(), 1);
assert_eq!(graph.branch_sections[0].commits[0].message, "A1");
assert_eq!(
graph.branch_sections[0].branch_names,
vec!["feature-a".to_string()]
);
assert_eq!(graph.branch_sections[1].label, "feature-b");
assert_eq!(graph.branch_sections[1].reset_target, "feature-a");
assert_eq!(graph.branch_sections[1].commits.len(), 1);
assert_eq!(graph.branch_sections[1].commits[0].message, "C1");
assert_eq!(
graph.branch_sections[1].branch_names,
vec!["feature-b".to_string()]
);
assert_eq!(graph.branch_sections[2].label, "feature-c");
assert_eq!(graph.branch_sections[2].commits.len(), 1);
if let IntegrationEntry::Merge { label, .. } = &graph.integration_line[0] {
assert_eq!(label, "feature-b");
} else {
panic!("Expected Merge entry");
}
let todo = graph.to_todo();
assert!(todo.contains("label feature-a\nupdate-ref refs/heads/feature-a\n"));
assert!(todo.contains("reset feature-a\n"));
assert!(todo.contains("label feature-b\nupdate-ref refs/heads/feature-b\n"));
}
#[test]
fn move_commit_to_colocated_branch_when_target_is_label() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string(), "feature-b".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
graph.move_commit(oid(OID_C1), "feature-a").unwrap();
assert_eq!(graph.branch_sections.len(), 2);
assert_eq!(graph.branch_sections[0].label, "feature-b");
assert_eq!(
graph.branch_sections[0].branch_names,
vec!["feature-b".to_string()]
);
assert_eq!(graph.branch_sections[1].label, "feature-a");
assert_eq!(graph.branch_sections[1].reset_target, "feature-b");
assert_eq!(graph.branch_sections[1].commits.len(), 1);
assert_eq!(graph.branch_sections[1].commits[0].message, "C1");
assert_eq!(graph.integration_line.len(), 1);
if let IntegrationEntry::Merge { label, .. } = &graph.integration_line[0] {
assert_eq!(label, "feature-a");
} else {
panic!("Expected Merge entry");
}
}
#[test]
fn fixup_commit_moves_and_changes_command() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Pick(make_commit(OID_C2, "C2")),
IntegrationEntry::Pick(make_commit(OID_FIX, "Fixup for C1")),
],
base_refs: vec![],
};
graph.fixup_commit(oid(OID_FIX), oid(OID_C1)).unwrap();
assert_eq!(graph.integration_line.len(), 3);
if let IntegrationEntry::Pick(c) = &graph.integration_line[0] {
assert_eq!(c.message, "C1");
}
if let IntegrationEntry::Pick(c) = &graph.integration_line[1] {
assert_eq!(c.message, "Fixup for C1");
assert_eq!(c.command, Command::Fixup);
}
if let IntegrationEntry::Pick(c) = &graph.integration_line[2] {
assert_eq!(c.message, "C2");
}
}
#[test]
fn move_commit_to_missing_section_errors() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![IntegrationEntry::Pick(make_commit(OID_C1, "C1"))],
base_refs: vec![],
};
let result = graph.move_commit(oid(OID_C1), "nonexistent-branch");
assert!(result.is_err());
assert!(
result.unwrap_err().to_string().contains("not found"),
"should mention target not found"
);
assert_eq!(graph.integration_line.len(), 1);
}
#[test]
fn fixup_commit_to_missing_target_errors() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Pick(make_commit(OID_FIX, "Fixup")),
],
base_refs: vec![],
};
let result = graph.fixup_commit(oid(OID_FIX), oid(OID_C2));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("not in the weave graph"),
"should say the target is not one loom can rewrite"
);
assert_eq!(graph.integration_line.len(), 2);
}
#[test]
fn edit_commit_changes_command() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![IntegrationEntry::Pick(make_commit(OID_C1, "C1"))],
base_refs: vec![],
};
assert!(graph.edit_commit(oid(OID_C1)));
assert!(
!graph.edit_commit(oid("9999999999999999999999999999999999999999")),
"a commit outside the graph cannot be marked"
);
if let IntegrationEntry::Pick(c) = &graph.integration_line[0] {
assert_eq!(c.command, Command::Edit);
}
}
#[test]
fn add_branch_section_and_merge() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![IntegrationEntry::Pick(make_commit(OID_C1, "C1"))],
base_refs: vec![],
};
graph.add_branch_section(
"new-branch".to_string(),
vec!["new-branch".to_string()],
vec![make_commit(OID_B1, "N1")],
"onto".to_string(),
);
graph.add_merge("new-branch".to_string(), None, None);
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.branch_sections[0].label, "new-branch");
assert_eq!(graph.integration_line.len(), 2);
}
#[test]
fn reassign_branch_renames_section() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string(), "feature-b".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
}],
base_refs: vec![],
};
assert!(graph.reassign_branch("feature-a", "feature-b"));
assert_eq!(graph.branch_sections[0].label, "feature-b");
assert!(
!graph.branch_sections[0]
.branch_names
.contains(&"feature-a".to_string())
);
assert!(
graph.branch_sections[0]
.branch_names
.contains(&"feature-b".to_string())
);
if let IntegrationEntry::Merge { label, .. } = &graph.integration_line[0] {
assert_eq!(label, "feature-b");
}
}
#[test]
fn from_repo_linear_integration() {
use crate::core::test_helpers::TestRepo;
let test_repo = TestRepo::new_with_remote();
test_repo.commit("C1", "c1.txt");
test_repo.commit("C2", "c2.txt");
let graph = Weave::from_repo(&test_repo.repo).unwrap();
assert!(graph.branch_sections.is_empty());
assert_eq!(graph.integration_line.len(), 2);
if let IntegrationEntry::Pick(c) = &graph.integration_line[0] {
assert_eq!(c.message, "C1");
} else {
panic!("Expected Pick entry");
}
if let IntegrationEntry::Pick(c) = &graph.integration_line[1] {
assert_eq!(c.message, "C2");
} else {
panic!("Expected Pick entry");
}
}
#[test]
fn from_repo_with_woven_branch() {
use crate::core::test_helpers::TestRepo;
let test_repo = TestRepo::new_with_remote();
let base_oid = test_repo.find_remote_branch_target("origin/main");
test_repo.create_branch_at("feature-a", &base_oid.to_string());
test_repo.switch_branch("feature-a");
test_repo.commit("A1", "a1.txt");
test_repo.commit("A2", "a2.txt");
test_repo.switch_branch("integration");
test_repo.commit("Int", "int.txt");
test_repo.merge_no_ff("feature-a");
let graph = Weave::from_repo(&test_repo.repo).unwrap();
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.branch_sections[0].label, "feature-a");
assert_eq!(graph.branch_sections[0].commits.len(), 2);
assert_eq!(graph.branch_sections[0].commits[0].message, "A1");
assert_eq!(graph.branch_sections[0].commits[1].message, "A2");
assert_eq!(graph.integration_line.len(), 2);
if let IntegrationEntry::Pick(c) = &graph.integration_line[0] {
assert_eq!(c.message, "Int");
} else {
panic!("Expected Pick entry for Int");
}
if let IntegrationEntry::Merge {
label,
original_oid,
} = &graph.integration_line[1]
{
assert_eq!(label, "feature-a");
assert!(original_oid.is_some());
} else {
panic!("Expected Merge entry for feature-a");
}
}
#[test]
fn from_repo_with_non_woven_branch() {
use crate::core::test_helpers::TestRepo;
let test_repo = TestRepo::new_with_remote();
test_repo.commit("C1", "c1.txt");
let c1_oid = test_repo.head_oid();
test_repo.create_branch_at("feature-a", &c1_oid.to_string());
test_repo.commit("C2", "c2.txt");
let graph = Weave::from_repo(&test_repo.repo).unwrap();
assert!(graph.branch_sections.is_empty());
assert_eq!(graph.integration_line.len(), 2);
if let IntegrationEntry::Pick(c) = &graph.integration_line[0] {
assert_eq!(c.message, "C1");
assert!(c.update_refs.contains(&"feature-a".to_string()));
} else {
panic!("Expected Pick entry for C1");
}
}
#[test]
fn from_repo_round_trip_preserves_identity() {
use crate::core::test_helpers::TestRepo;
let test_repo = TestRepo::new_with_remote();
let workdir = test_repo.workdir();
let base_oid = test_repo.find_remote_branch_target("origin/main");
test_repo.create_branch_at("feature-a", &base_oid.to_string());
test_repo.switch_branch("feature-a");
test_repo.commit("A1", "a1.txt");
test_repo.switch_branch("integration");
test_repo.commit("Int", "int.txt");
test_repo.merge_no_ff("feature-a");
let messages_before: Vec<String> = {
let info = repo::gather_repo_info(&test_repo.repo, false, 1).unwrap();
info.commits.iter().map(|c| c.message.clone()).collect()
};
let graph = Weave::from_repo(&test_repo.repo).unwrap();
let todo = graph.to_todo();
run_rebase(workdir.as_path(), Some(&graph.base_oid.to_string()), &todo).unwrap();
let messages_after: Vec<String> = {
let info = repo::gather_repo_info(&test_repo.repo, false, 1).unwrap();
info.commits.iter().map(|c| c.message.clone()).collect()
};
assert_eq!(messages_before, messages_after);
}
#[test]
fn drop_commit_transfers_update_refs_to_adjacent() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
commits: vec![
make_commit("111", "C1"),
make_commit_with_refs("222", "C2", vec!["non-woven-branch"]),
make_commit("333", "C3"),
],
}],
integration_line: vec![IntegrationEntry::Merge {
label: "feature-a".to_string(),
original_oid: None,
}],
base_refs: vec![],
};
assert!(graph.drop_commit(oid("222"), EmptiedRefs::Park).is_some());
assert_eq!(graph.branch_sections[0].commits.len(), 2);
assert!(
graph.branch_sections[0].commits[0]
.update_refs
.contains(&"non-woven-branch".to_string()),
"update_refs should transfer to preceding commit"
);
}
#[test]
fn drop_commit_drops_update_refs_when_first() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
commits: vec![
make_commit_with_refs("111", "C1", vec!["non-woven-branch"]),
make_commit("222", "C2"),
],
}],
integration_line: vec![IntegrationEntry::Merge {
label: "feature-a".to_string(),
original_oid: None,
}],
base_refs: vec![],
};
assert_eq!(
graph.drop_commit(oid("111"), EmptiedRefs::Park),
Some(vec!["non-woven-branch".to_string()])
);
assert_eq!(graph.branch_sections[0].commits.len(), 1);
assert!(
graph.branch_sections[0].commits[0].update_refs.is_empty(),
"update_refs must not transfer to the next commit"
);
assert_eq!(graph.base_refs, vec!["non-woven-branch".to_string()]);
}
#[test]
fn drop_commit_detaches_update_refs_when_first() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
commits: vec![
make_commit_with_refs("111", "C1", vec!["non-woven-branch"]),
make_commit("222", "C2"),
],
}],
integration_line: vec![IntegrationEntry::Merge {
label: "feature-a".to_string(),
original_oid: None,
}],
base_refs: vec![],
};
assert_eq!(
graph.drop_commit(oid("111"), EmptiedRefs::Detach),
Some(vec!["non-woven-branch".to_string()])
);
assert!(graph.branch_sections[0].commits[0].update_refs.is_empty());
assert!(graph.base_refs.is_empty());
}
#[test]
fn drop_commit_parks_inner_ref_at_parent_tip() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
label: "parent".to_string(),
branch_names: vec!["parent".to_string()],
commits: vec![make_commit("111", "P1")],
},
BranchSection {
reset_target: "parent".to_string(),
label: "child".to_string(),
branch_names: vec!["child".to_string()],
commits: vec![
make_commit_with_refs("222", "C1", vec!["inner"]),
make_commit("333", "C2"),
],
},
],
integration_line: vec![IntegrationEntry::Merge {
label: "child".to_string(),
original_oid: None,
}],
base_refs: vec![],
};
assert_eq!(
graph.drop_commit(oid("222"), EmptiedRefs::Park),
Some(vec!["inner".to_string()])
);
assert_eq!(
graph.branch_sections[0].commits[0].update_refs,
vec!["inner".to_string()]
);
assert!(graph.branch_sections[1].commits[0].update_refs.is_empty());
assert!(graph.base_refs.is_empty());
}
#[test]
fn drop_commit_on_integration_line_transfers_update_refs() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![],
integration_line: vec![
IntegrationEntry::Pick(make_commit("111", "C1")),
IntegrationEntry::Pick(make_commit_with_refs("222", "C2", vec!["loose-branch"])),
IntegrationEntry::Pick(make_commit("333", "C3")),
],
base_refs: vec![],
};
assert_eq!(
graph.drop_commit(oid("222"), EmptiedRefs::Park),
Some(vec![])
);
let picks: Vec<&CommitEntry> = graph
.integration_line
.iter()
.filter_map(|e| {
if let IntegrationEntry::Pick(c) = e {
Some(c)
} else {
None
}
})
.collect();
assert_eq!(picks.len(), 2);
assert_eq!(picks[0].update_refs, vec!["loose-branch".to_string()]);
assert!(picks[1].update_refs.is_empty());
}
#[test]
fn drop_commit_on_integration_line_drops_update_refs_when_first() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![],
integration_line: vec![
IntegrationEntry::Pick(make_commit_with_refs("111", "C1", vec!["loose-branch"])),
IntegrationEntry::Pick(make_commit("222", "C2")),
],
base_refs: vec![],
};
assert_eq!(
graph.drop_commit(oid("111"), EmptiedRefs::Park),
Some(vec!["loose-branch".to_string()])
);
assert_eq!(graph.integration_line.len(), 1);
assert!(
matches!(&graph.integration_line[0], IntegrationEntry::Pick(c) if c.update_refs.is_empty()),
"update_refs must not transfer to the next commit"
);
assert_eq!(graph.base_refs, vec!["loose-branch".to_string()]);
}
#[test]
fn drop_branch_preserves_colocated_update_refs_at_boundary() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
label: "feat3".to_string(),
branch_names: vec!["feat3".to_string()],
commits: vec![
make_commit_with_refs("111", "C1", vec!["feat1", "feat2"]),
make_commit("222", "C2"),
],
}],
integration_line: vec![IntegrationEntry::Merge {
label: "feat3".to_string(),
original_oid: None,
}],
base_refs: vec![],
};
assert_eq!(graph.branch_drop_size("feat3"), Some(1));
assert!(graph.drop_branch("feat3"));
assert_eq!(graph.branch_sections[0].label, "feat1");
assert!(
graph.branch_sections[0].commits[0]
.update_refs
.contains(&"feat2".to_string()),
"co-located inner refs must be preserved"
);
}
#[test]
fn weave_branch_moves_picks_into_section() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![],
integration_line: vec![
IntegrationEntry::Pick(make_commit("111", "C1")),
IntegrationEntry::Pick(make_commit_with_refs("222", "C2", vec!["feature-x"])),
IntegrationEntry::Pick(make_commit("333", "C3")),
],
base_refs: vec![],
};
graph.weave_branch("feature-x");
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.branch_sections[0].label, "feature-x");
assert_eq!(graph.branch_sections[0].commits.len(), 2);
assert_eq!(graph.integration_line.len(), 2);
assert!(
matches!(&graph.integration_line[0], IntegrationEntry::Merge { label, .. } if label == "feature-x"),
"expected Merge(feature-x) at position 0"
);
assert!(
matches!(&graph.integration_line[1], IntegrationEntry::Pick(c) if c.message == "C3"),
"expected Pick(C3) at position 1"
);
}
fn line_labels(graph: &Weave) -> Vec<String> {
graph
.integration_line
.iter()
.map(|e| match e {
IntegrationEntry::Pick(c) => c.message.clone(),
IntegrationEntry::Merge { label, .. } => format!("merge {label}"),
})
.collect()
}
#[test]
fn weave_branch_merges_above_the_last_merge() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![],
integration_line: vec![
IntegrationEntry::Pick(make_commit_with_refs("111", "C1", vec!["feature-x"])),
IntegrationEntry::Merge {
original_oid: Some(oid("999")),
label: "ra".to_string(),
},
IntegrationEntry::Pick(make_commit("333", "C3")),
],
base_refs: vec![],
};
graph.weave_branch("feature-x");
assert_eq!(line_labels(&graph), ["merge ra", "merge feature-x", "C3"]);
assert_eq!(graph.branch_sections[0].reset_target, "onto");
}
#[test]
fn weave_branch_merge_stays_below_loose_picks_built_on_it() {
let mut graph = Weave {
base_oid: oid("aaa"),
branch_sections: vec![],
integration_line: vec![
IntegrationEntry::Pick(make_commit_with_refs("111", "C1", vec!["feature-x"])),
IntegrationEntry::Pick(make_commit("222", "C2")),
IntegrationEntry::Merge {
original_oid: Some(oid("999")),
label: "ra".to_string(),
},
IntegrationEntry::Pick(make_commit("333", "C3")),
],
base_refs: vec![],
};
graph.weave_branch("feature-x");
assert_eq!(
line_labels(&graph),
["merge feature-x", "C2", "merge ra", "C3"]
);
}
#[test]
fn swap_commits_on_integration_line() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Pick(make_commit(OID_C2, "C2")),
],
base_refs: vec![],
};
graph.swap_commits(oid(OID_C1), oid(OID_C2)).unwrap();
if let IntegrationEntry::Pick(c) = &graph.integration_line[0] {
assert_eq!(c.message, "C2");
} else {
panic!("Expected Pick");
}
if let IntegrationEntry::Pick(c) = &graph.integration_line[1] {
assert_eq!(c.message, "C1");
} else {
panic!("Expected Pick");
}
}
#[test]
fn swap_commits_in_branch_section() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1"), make_commit(OID_A2, "A2")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
}],
base_refs: vec![],
};
graph.swap_commits(oid(OID_A1), oid(OID_A2)).unwrap();
assert_eq!(graph.branch_sections[0].commits[0].message, "A2");
assert_eq!(graph.branch_sections[0].commits[1].message, "A1");
}
#[test]
fn swap_commits_across_sections_errors() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
},
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_B1, "B1")],
label: "feature-b".to_string(),
branch_names: vec!["feature-b".to_string()],
},
],
integration_line: vec![
IntegrationEntry::Merge {
original_oid: None,
label: "feature-a".to_string(),
},
IntegrationEntry::Merge {
original_oid: None,
label: "feature-b".to_string(),
},
],
base_refs: vec![],
};
let result = graph.swap_commits(oid(OID_A1), oid(OID_B1));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("different"));
}
#[test]
fn swap_commits_across_section_and_integration_errors() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Merge {
original_oid: None,
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
let result = graph.swap_commits(oid(OID_A1), oid(OID_C1));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("different"));
}
#[test]
fn swap_commits_with_itself_errors() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![IntegrationEntry::Pick(make_commit(OID_C1, "C1"))],
base_refs: vec![],
};
let result = graph.swap_commits(oid(OID_C1), oid(OID_C1));
assert!(result.is_err());
}
#[test]
fn swap_commits_not_found_errors() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![],
integration_line: vec![IntegrationEntry::Pick(make_commit(OID_C1, "C1"))],
base_refs: vec![],
};
let result = graph.swap_commits(oid(OID_C1), oid(OID_C2));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
fn swap_commits_on_integration_line_with_interleaved_merge() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Merge {
original_oid: None,
label: "feature-a".to_string(),
},
IntegrationEntry::Pick(make_commit(OID_C2, "C2")),
],
base_refs: vec![],
};
graph.swap_commits(oid(OID_C1), oid(OID_C2)).unwrap();
if let IntegrationEntry::Pick(c) = &graph.integration_line[0] {
assert_eq!(c.message, "C2");
} else {
panic!("Expected Pick at 0");
}
assert!(matches!(
&graph.integration_line[1],
IntegrationEntry::Merge { .. }
));
if let IntegrationEntry::Pick(c) = &graph.integration_line[2] {
assert_eq!(c.message, "C1");
} else {
panic!("Expected Pick at 2");
}
}
#[test]
fn run_rebase_or_abort_rejects_a_paused_rebase() {
use crate::core::test_helpers::TestRepo;
let test_repo = TestRepo::new();
let base = test_repo.commit("base", "base.txt");
let c1 = test_repo.commit("first", "a.txt");
let c2 = test_repo.commit("second", "b.txt");
let workdir = test_repo.workdir();
let git_dir = test_repo.repo.path().to_path_buf();
let todo = format!("label onto\n\nreset onto\nedit {c1}\npick {c2}\n");
let err = super::run_rebase_or_abort(&workdir, Some(&base.to_string()), &todo)
.expect_err("a rebase left paused must not be reported as done");
assert!(
!crate::git::rebase_is_in_progress(&git_dir),
"the failed rebase should have been aborted, got: {err}"
);
assert!(
super::run_rebase_expecting_edit(&workdir, Some(&base.to_string()), &todo, c1, &[]).is_ok(),
"a caller that drives the `edit` itself expects the pause"
);
assert!(crate::git::rebase_is_in_progress(&git_dir));
crate::git::rebase_abort(&workdir).unwrap();
}
#[test]
fn move_all_colocated_commits_keeps_reset_targets_valid() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1"), make_commit(OID_A2, "A2")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string(), "feature-b".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
}],
base_refs: vec![],
};
assert!(
graph
.move_commit(oid(OID_A1), "feature-b")
.unwrap()
.is_empty()
);
assert_eq!(
graph.move_commit(oid(OID_A2), "feature-b").unwrap(),
vec!["feature-a".to_string()]
);
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.branch_sections[0].label, "feature-b");
assert_eq!(graph.branch_sections[0].reset_target, "onto");
assert_eq!(graph.branch_sections[0].commits.len(), 2);
assert_eq!(graph.base_refs, vec!["feature-a".to_string()]);
let todo = graph.to_todo();
let labels: Vec<&str> = todo
.lines()
.filter_map(|l| l.strip_prefix("label "))
.collect();
for target in todo.lines().filter_map(|l| l.strip_prefix("reset ")) {
assert!(
labels.contains(&target),
"reset {target} has no label:\n{todo}"
);
}
}
#[test]
fn move_sole_colocated_commit_parks_the_other_branch() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string(), "feature-b".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
}],
base_refs: vec![],
};
assert_eq!(
graph.move_commit(oid(OID_A1), "feature-b").unwrap(),
vec!["feature-a".to_string()]
);
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.branch_sections[0].label, "feature-b");
assert_eq!(graph.branch_sections[0].reset_target, "onto");
assert_eq!(graph.base_refs, vec!["feature-a".to_string()]);
assert_eq!(graph.integration_line.len(), 1);
assert!(matches!(
&graph.integration_line[0],
IntegrationEntry::Merge { label, .. } if label == "feature-b"
));
}
fn two_sections() -> Weave {
Weave {
base_oid: oid(BASE),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
commits: vec![
make_commit_with_refs(OID_A1, "A1", vec!["inner", "twin"]),
make_commit(OID_A2, "A2"),
],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
},
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_B1, "B1")],
label: "feature-b".to_string(),
branch_names: vec!["feature-b".to_string()],
},
],
integration_line: vec![
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE2)),
label: "feature-b".to_string(),
},
],
base_refs: vec![],
}
}
fn messages(commits: &[CommitEntry]) -> Vec<&str> {
commits.iter().map(|c| c.message.as_str()).collect()
}
#[test]
fn move_commit_above_within_section_reorders() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![
make_commit(OID_A1, "A1"),
make_commit(OID_A2, "A2"),
make_commit(OID_B1, "B1"),
],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
}],
base_refs: vec![],
};
let parked = graph
.move_commits_relative(&[oid(OID_B1)], oid(OID_A1), Position::Above)
.unwrap();
assert!(parked.is_empty());
assert_eq!(
messages(&graph.branch_sections[0].commits),
vec!["A1", "B1", "A2"]
);
let parked = graph
.move_commits_relative(&[oid(OID_A2)], oid(OID_A1), Position::Below)
.unwrap();
assert!(parked.is_empty());
assert_eq!(
messages(&graph.branch_sections[0].commits),
vec!["A2", "A1", "B1"]
);
}
#[test]
fn move_commit_above_takes_the_anchor_refs_below_leaves_them() {
let mut graph = two_sections();
let parked = graph
.move_commits_relative(&[oid(OID_B1)], oid(OID_A1), Position::Above)
.unwrap();
assert_eq!(parked, vec!["feature-b".to_string()]);
assert_eq!(graph.base_refs, vec!["feature-b".to_string()]);
assert_eq!(graph.branch_sections.len(), 1);
assert_eq!(graph.integration_line.len(), 1);
let commits = &graph.branch_sections[0].commits;
assert_eq!(messages(commits), vec!["A1", "B1", "A2"]);
assert!(commits[0].update_refs.is_empty());
assert_eq!(
commits[1].update_refs,
vec!["inner".to_string(), "twin".to_string()]
);
let mut graph = two_sections();
graph
.move_commits_relative(&[oid(OID_B1)], oid(OID_A1), Position::Below)
.unwrap();
let commits = &graph.branch_sections[0].commits;
assert_eq!(messages(commits), vec!["B1", "A1", "A2"]);
assert!(commits[0].update_refs.is_empty());
assert_eq!(
commits[1].update_refs,
vec!["inner".to_string(), "twin".to_string()]
);
}
#[test]
fn move_commit_above_section_tip_equals_move_to_branch() {
let build = || Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
let mut by_branch = build();
by_branch.move_commit(oid(OID_C1), "feature-a").unwrap();
let mut by_anchor = build();
by_anchor
.move_commits_relative(&[oid(OID_C1)], oid(OID_A1), Position::Above)
.unwrap();
assert_eq!(by_anchor.to_todo(), by_branch.to_todo());
}
#[test]
fn move_commit_above_leaves_refs_parked_on_the_anchor() {
let build = || Weave {
base_oid: oid(BASE),
branch_sections: vec![
BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1"), make_commit(OID_A2, "A2")],
label: "outer".to_string(),
branch_names: vec!["outer".to_string()],
},
BranchSection {
reset_target: "outer".to_string(),
commits: vec![make_commit(OID_B1, "S1")],
label: "stacked".to_string(),
branch_names: vec!["stacked".to_string()],
},
],
integration_line: vec![IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "stacked".to_string(),
}],
base_refs: vec![],
};
let mut by_anchor = build();
let parked = by_anchor
.move_commits_relative(&[oid(OID_B1)], oid(OID_A2), Position::Above)
.unwrap();
assert_eq!(parked, vec!["stacked".to_string()]);
let commits = &by_anchor.branch_sections[0].commits;
assert_eq!(messages(commits), vec!["A1", "A2", "S1"]);
assert_eq!(commits[1].update_refs, vec!["stacked".to_string()]);
assert!(commits[2].update_refs.is_empty());
let mut by_branch = build();
by_branch.move_commit(oid(OID_B1), "outer").unwrap();
assert_eq!(by_anchor.to_todo(), by_branch.to_todo());
}
#[test]
fn move_commits_relative_keeps_the_block_order() {
let build = || Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1"), make_commit(OID_A2, "A2")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Pick(make_commit(OID_C2, "C2")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
let mut graph = build();
graph
.move_commits_relative(&[oid(OID_C1), oid(OID_C2)], oid(OID_A1), Position::Above)
.unwrap();
assert_eq!(
messages(&graph.branch_sections[0].commits),
vec!["A1", "C1", "C2", "A2"]
);
assert_eq!(graph.integration_line.len(), 1);
let mut graph = build();
graph
.move_commits_relative(&[oid(OID_C1), oid(OID_C2)], oid(OID_A1), Position::Below)
.unwrap();
assert_eq!(
messages(&graph.branch_sections[0].commits),
vec!["C1", "C2", "A1", "A2"]
);
}
#[test]
fn move_commit_relative_onto_the_integration_line() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit_with_refs(OID_C1, "C1", vec!["loose"])),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
let parked = graph
.move_commits_relative(&[oid(OID_A1)], oid(OID_C1), Position::Above)
.unwrap();
assert_eq!(parked, vec!["feature-a".to_string()]);
assert!(graph.branch_sections.is_empty());
assert_eq!(graph.base_refs, vec!["feature-a".to_string()]);
let picks: Vec<(&str, &[String])> = graph
.integration_line
.iter()
.map(|e| match e {
IntegrationEntry::Pick(c) => (c.message.as_str(), c.update_refs.as_slice()),
IntegrationEntry::Merge { .. } => panic!("the merge must be gone"),
})
.collect();
assert_eq!(picks[0].0, "C1");
assert!(picks[0].1.is_empty());
assert_eq!(picks[1].0, "A1");
assert_eq!(picks[1].1, &["loose".to_string()]);
}
#[test]
fn move_commit_relative_rejects_self_missing_and_in_place() {
let mut graph = two_sections();
let err = graph
.move_commits_relative(&[oid(OID_A1)], oid(OID_A1), Position::Above)
.unwrap_err();
assert_eq!(err.to_string(), "Source and target are the same commit");
let err = graph
.move_commits_relative(&[oid(OID_FIX)], oid(OID_A1), Position::Above)
.unwrap_err();
assert!(err.to_string().contains("not in the weave graph"), "{err}");
let before = graph.to_todo();
let err = graph
.move_commits_relative(&[oid(OID_A2)], oid(OID_A1), Position::Above)
.unwrap_err();
assert_eq!(
err.to_string(),
format!("Commit `{}` is already directly above `{}`", OID_A2, OID_A1)
);
let err = graph
.move_commits_relative(&[oid(OID_A1)], oid(OID_A2), Position::Below)
.unwrap_err();
assert_eq!(
err.to_string(),
format!("Commit `{}` is already directly below `{}`", OID_A1, OID_A2)
);
assert_eq!(graph.to_todo(), before, "a refused move changes nothing");
}
#[test]
fn move_commit_relative_rejects_a_source_listed_twice() {
let mut graph = two_sections();
let before = graph.to_todo();
let err = graph
.move_commits_relative(&[oid(OID_B1), oid(OID_B1)], oid(OID_A1), Position::Above)
.unwrap_err();
assert!(err.to_string().contains("is listed twice"), "{err}");
assert_eq!(graph.to_todo(), before);
}
#[test]
fn move_commits_relative_rejects_a_block_already_in_place() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![
make_commit(OID_C1, "C1"),
make_commit(OID_C2, "C2"),
make_commit(OID_A2, "A2"),
],
label: "feature-c".to_string(),
branch_names: vec!["feature-c".to_string()],
}],
integration_line: vec![],
base_refs: vec![],
};
let before = graph.to_todo();
let err = graph
.move_commits_relative(&[oid(OID_C2), oid(OID_A2)], oid(OID_C1), Position::Above)
.unwrap_err();
assert_eq!(
err.to_string(),
format!("Commits are already in place above `{OID_C1}`")
);
let err = graph
.move_commits_relative(&[oid(OID_C1), oid(OID_C2)], oid(OID_A2), Position::Below)
.unwrap_err();
assert_eq!(
err.to_string(),
format!("Commits are already in place below `{OID_A2}`")
);
assert_eq!(graph.to_todo(), before);
}
#[test]
fn move_commit_above_across_a_merge_entry_is_not_in_place() {
let mut graph = Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
IntegrationEntry::Pick(make_commit(OID_C2, "C2")),
],
base_refs: vec![],
};
graph
.move_commits_relative(&[oid(OID_C2)], oid(OID_C1), Position::Above)
.unwrap();
assert!(matches!(&graph.integration_line[0], IntegrationEntry::Pick(c) if c.message == "C1"));
assert!(matches!(&graph.integration_line[1], IntegrationEntry::Pick(c) if c.message == "C2"));
assert!(matches!(
&graph.integration_line[2],
IntegrationEntry::Merge { .. }
));
}
#[test]
fn move_commit_above_a_colocated_tip_advances_every_branch() {
let build = || Weave {
base_oid: oid(BASE),
branch_sections: vec![BranchSection {
reset_target: "onto".to_string(),
commits: vec![make_commit(OID_A1, "A1")],
label: "feature-a".to_string(),
branch_names: vec!["feature-a".to_string(), "feature-b".to_string()],
}],
integration_line: vec![
IntegrationEntry::Pick(make_commit(OID_C1, "C1")),
IntegrationEntry::Merge {
original_oid: Some(oid(OID_MERGE1)),
label: "feature-a".to_string(),
},
],
base_refs: vec![],
};
let mut by_anchor = build();
by_anchor
.move_commits_relative(&[oid(OID_C1)], oid(OID_A1), Position::Above)
.unwrap();
assert_eq!(
by_anchor.branch_sections.len(),
1,
"the section is not split"
);
assert_eq!(
messages(&by_anchor.branch_sections[0].commits),
vec!["A1", "C1"]
);
assert_eq!(
by_anchor.branch_sections[0].branch_names,
vec!["feature-a".to_string(), "feature-b".to_string()],
"both co-located branches end at the moved commit"
);
let mut by_branch = build();
by_branch.move_commit(oid(OID_C1), "feature-a").unwrap();
assert_eq!(
by_branch.branch_sections.len(),
2,
"a branch move splits instead, leaving feature-b at A1"
);
assert_ne!(by_anchor.to_todo(), by_branch.to_todo());
}
#[test]
fn an_edit_rebase_refuses_a_commit_the_todo_never_marks() {
use crate::core::test_helpers::TestRepo;
let test_repo = TestRepo::new();
let base = test_repo.commit("base", "base.txt");
let c1 = test_repo.commit("first", "a.txt");
let workdir = test_repo.workdir();
let git_dir = test_repo.repo.path().to_path_buf();
let todo = format!("label onto\n\nreset onto\npick {c1}\n");
let err = super::run_rebase_expecting_edit(&workdir, Some(&base.to_string()), &todo, c1, &[])
.expect_err("a todo with no `edit` for the target must be refused");
assert!(!crate::git::rebase_is_in_progress(&git_dir), "{err}");
assert_eq!(test_repo.head_oid(), c1, "{err}");
}
#[test]
fn ensure_todo_edits_wants_an_edit_line_for_the_target() {
fn check(todo: &str, oid: git2::Oid) -> anyhow::Result<()> {
super::ensure_todo_edits(&super::edited_commits(todo), oid)
}
let oid = git2::Oid::from_str("4783c1b06566c35e8d1cbc020936813639cd0ea4").unwrap();
let other = git2::Oid::from_str("cd46c622e4881bed2ae786285fab0b14a7271f13").unwrap();
assert!(check("reset onto\nedit 4783c1b # msg\n", oid).is_ok());
assert!(check(&format!("edit {oid} # msg\n"), oid).is_ok());
assert!(check("", oid).is_err());
assert!(check("reset onto\npick 4783c1b # msg\n", oid).is_err());
assert!(check("edit cd46c62 # other\n", oid).is_err());
assert!(check("edit 4783 # msg\n", oid).is_ok());
assert!(check("edit \n", oid).is_err());
assert!(check(&format!("edit {other} # other\n"), oid).is_err());
}