dk_engine/workspace/
merge.rs1use std::collections::HashMap;
10
11use dk_core::Result;
12
13use crate::git::GitRepository;
14use crate::parser::ParserRegistry;
15use crate::workspace::conflict::{analyze_file_conflict, MergeAnalysis, SemanticConflict};
16use crate::workspace::session_workspace::SessionWorkspace;
17
18#[derive(Debug)]
22pub enum WorkspaceMergeResult {
23 FastMerge { commit_hash: String },
25
26 RebaseMerge {
28 commit_hash: String,
29 auto_rebased_files: Vec<String>,
31 },
32
33 Conflicts {
35 conflicts: Vec<SemanticConflict>,
36 },
37}
38
39pub fn merge_workspace(
51 workspace: &SessionWorkspace,
52 git_repo: &GitRepository,
53 parser: &ParserRegistry,
54 commit_message: &str,
55 author_name: &str,
56 author_email: &str,
57) -> Result<WorkspaceMergeResult> {
58 let overlay = workspace.overlay_for_tree();
59
60 if overlay.is_empty() {
61 return Err(dk_core::Error::Internal(
62 "workspace has no changes to merge".into(),
63 ));
64 }
65
66 let head_hash = match git_repo.head_hash()? {
72 Some(hash) => hash,
73 None => {
74 if workspace.base_commit == "initial" {
75 let commit_hash = git_repo.commit_initial_overlay(
76 &overlay,
77 commit_message,
78 author_name,
79 author_email,
80 )?;
81 return Ok(WorkspaceMergeResult::FastMerge { commit_hash });
82 }
83 return Err(dk_core::Error::Git("repository has no HEAD".into()));
84 }
85 };
86
87 if head_hash == workspace.base_commit {
89 let commit_hash = git_repo.commit_tree_overlay(
90 &workspace.base_commit,
91 &overlay,
92 &workspace.base_commit,
93 commit_message,
94 author_name,
95 author_email,
96 )?;
97
98 return Ok(WorkspaceMergeResult::FastMerge { commit_hash });
99 }
100
101 let paths: Vec<&String> = overlay.iter().map(|(p, _)| p).collect();
108
109 let mut base_entries: HashMap<&str, Option<Vec<u8>>> = HashMap::with_capacity(paths.len());
110 for path in &paths {
111 base_entries.insert(path.as_str(), git_repo.read_tree_entry(&workspace.base_commit, path).ok());
112 }
113
114 let mut head_entries: HashMap<&str, Option<Vec<u8>>> = HashMap::with_capacity(paths.len());
115 for path in &paths {
116 head_entries.insert(path.as_str(), git_repo.read_tree_entry(&head_hash, path).ok());
117 }
118
119 let mut all_conflicts = Vec::new();
120 let mut auto_rebased = Vec::new();
121 let mut rebased_overlay: Vec<(String, Option<Vec<u8>>)> = Vec::new();
122
123 for (path, maybe_content) in &overlay {
124 let base_content = base_entries.get(path.as_str()).and_then(|v| v.as_ref());
125 let head_content = head_entries.get(path.as_str()).and_then(|v| v.as_ref());
126
127 match maybe_content {
128 None => {
129 match (base_content, head_content) {
131 (Some(base), Some(head)) => {
132 if base == head {
133 rebased_overlay.push((path.clone(), None));
134 } else {
135 all_conflicts.push(SemanticConflict {
136 file_path: path.clone(),
137 symbol_name: "<entire file>".to_string(),
138 our_change: crate::workspace::conflict::SymbolChangeKind::Removed,
139 their_change: crate::workspace::conflict::SymbolChangeKind::Modified,
140 });
141 }
142 }
143 _ => {
144 rebased_overlay.push((path.clone(), None));
145 }
146 }
147 }
148 Some(overlay_content) => {
149 match (base_content, head_content) {
150 (Some(base), Some(head)) => {
151 if base == head {
152 rebased_overlay.push((path.clone(), Some(overlay_content.clone())));
153 } else {
154 let analysis = analyze_file_conflict(
155 path,
156 base,
157 head,
158 overlay_content,
159 parser,
160 );
161
162 match analysis {
163 MergeAnalysis::AutoMerge { merged_content } => {
164 rebased_overlay
165 .push((path.clone(), Some(merged_content)));
166 auto_rebased.push(path.clone());
167 }
168 MergeAnalysis::Conflict { conflicts } => {
169 all_conflicts.extend(conflicts);
170 }
171 }
172 }
173 }
174 (None, Some(head_blob)) => {
175 if *head_blob == *overlay_content {
176 rebased_overlay.push((path.clone(), Some(overlay_content.clone())));
177 } else {
178 all_conflicts.push(SemanticConflict {
179 file_path: path.clone(),
180 symbol_name: "<entire file>".to_string(),
181 our_change: crate::workspace::conflict::SymbolChangeKind::Added,
182 their_change: crate::workspace::conflict::SymbolChangeKind::Added,
183 });
184 }
185 }
186 (None, None) => {
187 rebased_overlay.push((path.clone(), Some(overlay_content.clone())));
188 }
189 (Some(_), None) => {
190 all_conflicts.push(SemanticConflict {
191 file_path: path.clone(),
192 symbol_name: "<entire file>".to_string(),
193 our_change: crate::workspace::conflict::SymbolChangeKind::Modified,
194 their_change: crate::workspace::conflict::SymbolChangeKind::Removed,
195 });
196 }
197 }
198 }
199 }
200 }
201
202 if !all_conflicts.is_empty() {
203 return Ok(WorkspaceMergeResult::Conflicts {
204 conflicts: all_conflicts,
205 });
206 }
207
208 let commit_hash = git_repo.commit_tree_overlay(
210 &head_hash,
211 &rebased_overlay,
212 &head_hash,
213 commit_message,
214 author_name,
215 author_email,
216 )?;
217
218 Ok(WorkspaceMergeResult::RebaseMerge {
219 commit_hash,
220 auto_rebased_files: auto_rebased,
221 })
222}