1use crate::dag::graph::{DagError, PatchDag};
9use crate::patch::merge::{self, MergeResult};
10use crate::patch::types::PatchId;
11use std::collections::HashMap;
12use suture_common::BranchName;
13
14impl PatchDag {
15 pub fn merge_branches(
20 &self,
21 branch_a: &BranchName,
22 branch_b: &BranchName,
23 ) -> Result<MergeResult, DagError> {
24 let target_a = self
25 .get_branch(branch_a)
26 .ok_or_else(|| DagError::BranchNotFound(branch_a.as_str().to_string()))?;
27 let target_b = self
28 .get_branch(branch_b)
29 .ok_or_else(|| DagError::BranchNotFound(branch_b.as_str().to_string()))?;
30
31 let lca_id = self
33 .lca(&target_a, &target_b)
34 .ok_or_else(|| DagError::Custom("no common ancestor found".to_string()))?;
35
36 let chain_a = self.patch_chain(&target_a);
38 let chain_b = self.patch_chain(&target_b);
39
40 let lca_pos_a = chain_a.iter().position(|id| *id == lca_id);
42 let lca_pos_b = chain_b.iter().position(|id| *id == lca_id);
43
44 let branch_a_patches: Vec<PatchId> = match lca_pos_a {
46 Some(pos) => chain_a[..pos].to_vec(), None => chain_a.clone(),
48 };
49
50 let branch_b_patches: Vec<PatchId> = match lca_pos_b {
51 Some(pos) => chain_b[..pos].to_vec(),
52 None => chain_b.clone(),
53 };
54
55 let all_patches: HashMap<PatchId, _> = self
57 .nodes
58 .iter()
59 .map(|(id, node)| (*id, node.patch.clone()))
60 .collect();
61
62 merge::merge(
64 &[lca_id],
65 &branch_a_patches,
66 &branch_b_patches,
67 &all_patches,
68 )
69 .map_err(|e| DagError::Custom(e.to_string()))
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use crate::dag::graph::PatchDag;
76 use crate::patch::types::{OperationType, Patch, TouchSet};
77 use suture_common::BranchName;
78
79 fn make_patch(addr: &str) -> Patch {
80 Patch::new(
81 OperationType::Modify,
82 TouchSet::single(addr),
83 Some(format!("file_{}", addr)),
84 vec![],
85 vec![],
86 "test".to_string(),
87 format!("edit {}", addr),
88 )
89 }
90
91 #[test]
92 fn test_merge_clean_divergent_branches() {
93 let mut dag = PatchDag::new();
94
95 let root = make_patch("root");
97 let root_id = dag.add_patch(root, vec![]).unwrap();
98
99 assert!(dag.has_patch(&root_id), "root should be in DAG");
101
102 let a1 = make_patch("A1");
104 let a1_id = dag.add_patch(a1, vec![root_id]).unwrap();
105 assert!(dag.has_patch(&a1_id), "a1 should be in DAG");
106
107 let b1 = make_patch("B1");
109 let b1_id = dag.add_patch(b1, vec![root_id]).unwrap();
110 assert!(dag.has_patch(&b1_id), "b1 should be in DAG");
111
112 let anc_a = dag.ancestors(&a1_id);
114 let anc_b = dag.ancestors(&b1_id);
115 assert!(anc_a.contains(&root_id), "root should be ancestor of a1");
116 assert!(anc_b.contains(&root_id), "root should be ancestor of b1");
117
118 let intersection: std::collections::HashSet<_> = anc_a.intersection(&anc_b).collect();
120 assert_eq!(intersection.len(), 1, "should have 1 common ancestor");
121 assert!(
122 intersection.contains(&root_id),
123 "common ancestor should be root_id"
124 );
125
126 let direct_lca = dag.lca(&a1_id, &b1_id);
128 assert_eq!(direct_lca, Some(root_id), "LCA of a1 and b1 should be root");
129
130 let branch_a = BranchName::new("branch_a").unwrap();
131 let branch_b = BranchName::new("branch_b").unwrap();
132
133 dag.create_branch(branch_a.clone(), a1_id).unwrap();
134 dag.create_branch(branch_b.clone(), b1_id).unwrap();
135
136 let result = dag.merge_branches(&branch_a, &branch_b).unwrap();
137 assert!(result.is_clean);
138 assert!(result.conflicts.is_empty());
139 }
140
141 #[test]
142 fn test_merge_conflicting_branches() {
143 let mut dag = PatchDag::new();
144
145 let root = make_patch("root");
146 let root_id = dag.add_patch(root, vec![]).unwrap();
147
148 let a1 = Patch::new(
151 OperationType::Modify,
152 TouchSet::from_addrs(["shared_addr"]),
153 Some("file_shared_addr".to_string()),
154 vec![1], vec![],
156 "alice".to_string(),
157 "edit shared_addr (version A)".to_string(),
158 );
159 let a1_id = dag.add_patch(a1, vec![root_id]).unwrap();
160
161 let b1 = Patch::new(
162 OperationType::Modify,
163 TouchSet::from_addrs(["shared_addr"]),
164 Some("file_shared_addr".to_string()),
165 vec![2], vec![],
167 "bob".to_string(),
168 "edit shared_addr (version B)".to_string(),
169 );
170 let b1_id = dag.add_patch(b1, vec![root_id]).unwrap();
171
172 let branch_a = BranchName::new("branch_a").unwrap();
173 let branch_b = BranchName::new("branch_b").unwrap();
174
175 dag.create_branch(branch_a.clone(), a1_id).unwrap();
176 dag.create_branch(branch_b.clone(), b1_id).unwrap();
177
178 let result = dag.merge_branches(&branch_a, &branch_b).unwrap();
179 assert!(
180 !result.is_clean,
181 "branches editing the same address should conflict"
182 );
183 assert_eq!(result.conflicts.len(), 1, "should have exactly 1 conflict");
184 }
185}