1use crate::operations::{FileInfo, VersionInfo};
14
15#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub enum ConflictType {
18 Divergent {
20 common_ancestor: u64,
22 local_version: u64,
24 remote_version: u64,
26 },
27 ContentMismatch {
29 version: u64,
31 local_hash: String,
33 remote_hash: String,
35 },
36 VersionGap {
38 expected: u64,
40 found: u64,
42 },
43 None,
45}
46
47impl std::fmt::Display for ConflictType {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::Divergent {
51 common_ancestor,
52 local_version,
53 remote_version,
54 } => {
55 write!(f, "Divergent histories from version {common_ancestor}: local={local_version}, remote={remote_version}")
56 }
57 Self::ContentMismatch {
58 version,
59 local_hash,
60 remote_hash,
61 } => {
62 write!(f, "Content mismatch at version {version}: local={local_hash}, remote={remote_hash}")
63 }
64 Self::VersionGap { expected, found } => {
65 write!(f, "Version gap: expected {expected}, found {found}")
66 }
67 Self::None => write!(f, "No conflict"),
68 }
69 }
70}
71
72#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
74pub struct ConflictReport {
75 pub conflict_type: ConflictType,
77 pub local_version_count: u64,
79 pub remote_version_count: u64,
81 pub suggested_strategy: MergeStrategy,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
87pub enum MergeStrategy {
88 KeepLocal,
90 KeepRemote,
92 KeepNewest,
94 Manual,
96 Append,
98}
99
100impl std::fmt::Display for MergeStrategy {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 match self {
103 Self::KeepLocal => write!(f, "Keep local"),
104 Self::KeepRemote => write!(f, "Keep remote"),
105 Self::KeepNewest => write!(f, "Keep newest"),
106 Self::Manual => write!(f, "Manual merge required"),
107 Self::Append => write!(f, "Append remote versions"),
108 }
109 }
110}
111
112#[must_use]
123pub fn detect_conflict(local: &FileInfo, remote: &FileInfo) -> ConflictReport {
124 if local.file_id != remote.file_id {
125 return file_id_mismatch_report(local, remote);
126 }
127 if local.version_count == remote.version_count {
128 return same_length_report(local, remote);
129 }
130 differing_length_report(local, remote)
131}
132
133fn file_id_mismatch_report(local: &FileInfo, remote: &FileInfo) -> ConflictReport {
134 ConflictReport {
135 conflict_type: ConflictType::ContentMismatch {
136 version: 0,
137 local_hash: format!("{:016x}", local.file_id),
138 remote_hash: format!("{:016x}", remote.file_id),
139 },
140 local_version_count: local.version_count,
141 remote_version_count: remote.version_count,
142 suggested_strategy: MergeStrategy::Manual,
143 }
144}
145
146fn same_length_report(local: &FileInfo, remote: &FileInfo) -> ConflictReport {
147 if versions_match(local, remote) {
148 return ConflictReport {
149 conflict_type: ConflictType::None,
150 local_version_count: local.version_count,
151 remote_version_count: remote.version_count,
152 suggested_strategy: MergeStrategy::KeepLocal,
153 };
154 }
155 ConflictReport {
156 conflict_type: ConflictType::ContentMismatch {
157 version: local.current_version,
158 local_hash: format_version_hash(local),
159 remote_hash: format_version_hash(remote),
160 },
161 local_version_count: local.version_count,
162 remote_version_count: remote.version_count,
163 suggested_strategy: MergeStrategy::Manual,
164 }
165}
166
167fn differing_length_report(local: &FileInfo, remote: &FileInfo) -> ConflictReport {
168 let (shorter, longer) = if local.version_count < remote.version_count {
169 (local, remote)
170 } else {
171 (remote, local)
172 };
173 if is_linear_extension(shorter, longer) {
174 let strategy = if local.version_count < remote.version_count {
175 MergeStrategy::KeepRemote
176 } else {
177 MergeStrategy::KeepLocal
178 };
179 return ConflictReport {
180 conflict_type: ConflictType::None,
181 local_version_count: local.version_count,
182 remote_version_count: remote.version_count,
183 suggested_strategy: strategy,
184 };
185 }
186 ConflictReport {
187 conflict_type: ConflictType::Divergent {
188 common_ancestor: find_common_ancestor(local, remote),
189 local_version: local.current_version,
190 remote_version: remote.current_version,
191 },
192 local_version_count: local.version_count,
193 remote_version_count: remote.version_count,
194 suggested_strategy: MergeStrategy::Manual,
195 }
196}
197
198fn versions_match(local: &FileInfo, remote: &FileInfo) -> bool {
200 match (local.versions.last(), remote.versions.last()) {
201 (Some(l), Some(r)) => l.rules_hash == r.rules_hash,
202 (None, None) => true,
203 _ => false,
204 }
205}
206
207fn format_version_hash(info: &FileInfo) -> String {
209 info.versions
210 .last()
211 .map_or_else(|| "empty".to_string(), |v| hex::encode(&v.rules_hash[..8]))
212}
213
214fn is_linear_extension(shorter: &FileInfo, longer: &FileInfo) -> bool {
216 if shorter.versions.len() > longer.versions.len() {
217 return false;
218 }
219
220 for (i, short_ver) in shorter.versions.iter().enumerate() {
221 let Some(long_ver) = longer.versions.get(i) else {
222 return false;
223 };
224 if short_ver.rules_hash != long_ver.rules_hash {
225 return false;
226 }
227 }
228
229 true
230}
231
232fn find_common_ancestor(local: &FileInfo, remote: &FileInfo) -> u64 {
234 let min_len = std::cmp::min(local.versions.len(), remote.versions.len());
235 let mut last_matching: Option<&VersionInfo> = None;
236
237 for i in 0..min_len {
238 let (Some(l), Some(r)) = (local.versions.get(i), remote.versions.get(i)) else {
239 break;
240 };
241 if l.rules_hash != r.rules_hash {
242 return last_matching.map_or(0, |v| v.version_number);
243 }
244 last_matching = Some(l);
245 }
246
247 last_matching.map_or(0, |v| v.version_number)
248}
249
250#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
252pub struct ConflictMarker {
253 pub marker_type: MarkerType,
255 pub start: usize,
257 pub end: usize,
259 pub source: String,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
265pub enum MarkerType {
266 ConflictStart,
268 Separator,
270 ConflictEnd,
272}
273
274#[must_use]
285pub fn create_conflict_markers(
286 local_content: &[u8],
287 remote_content: &[u8],
288 local_label: &str,
289 remote_label: &str,
290) -> Vec<u8> {
291 let mut result = Vec::new();
292
293 result.extend_from_slice(b"<<<<<<< ");
295 result.extend_from_slice(local_label.as_bytes());
296 result.push(b'\n');
297
298 result.extend_from_slice(local_content);
300 if !local_content.ends_with(b"\n") {
301 result.push(b'\n');
302 }
303
304 result.extend_from_slice(b"=======\n");
306
307 result.extend_from_slice(remote_content);
309 if !remote_content.ends_with(b"\n") {
310 result.push(b'\n');
311 }
312
313 result.extend_from_slice(b">>>>>>> ");
315 result.extend_from_slice(remote_label.as_bytes());
316 result.push(b'\n');
317
318 result
319}
320
321#[must_use]
323pub fn has_conflict_markers(content: &[u8]) -> bool {
324 let content_str = String::from_utf8_lossy(content);
325 content_str.contains("<<<<<<<") && content_str.contains(">>>>>>>")
326}
327
328#[must_use]
332pub fn parse_conflict_markers(content: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
333 let content_str = String::from_utf8_lossy(content);
334
335 let start_idx = content_str.find("<<<<<<< ")?;
336 let separator_idx = content_str.find("=======")?;
337 let end_idx = content_str.find(">>>>>>> ")?;
338
339 if start_idx >= separator_idx || separator_idx >= end_idx {
340 return None;
341 }
342
343 let after_start = content_str
345 .get(start_idx..)?
346 .find('\n')?
347 .checked_add(start_idx)?
348 .checked_add(1)?;
349 let local = content_str.get(after_start..separator_idx)?;
350
351 let after_sep = separator_idx.checked_add(8)?; let remote = content_str.get(after_sep..end_idx)?;
354
355 Some((
356 local.trim_end().as_bytes().to_vec(),
357 remote.trim_end().as_bytes().to_vec(),
358 ))
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn test_conflict_markers() {
367 let local = b"local version";
368 let remote = b"remote version";
369
370 let merged = create_conflict_markers(local, remote, "LOCAL", "REMOTE");
371
372 assert!(has_conflict_markers(&merged));
373
374 let (parsed_local, parsed_remote) =
375 parse_conflict_markers(&merged).unwrap_or_else(|| std::process::abort());
376 assert_eq!(parsed_local, local.to_vec());
377 assert_eq!(parsed_remote, remote.to_vec());
378 }
379
380 #[test]
381 fn test_no_conflict_markers() {
382 let content = b"normal content without markers";
383 assert!(!has_conflict_markers(content));
384 }
385
386 #[test]
387 fn test_merge_strategy_display() {
388 assert_eq!(MergeStrategy::KeepLocal.to_string(), "Keep local");
389 assert_eq!(MergeStrategy::Manual.to_string(), "Manual merge required");
390 }
391
392 #[test]
393 fn test_conflict_type_display() {
394 let conflict = ConflictType::Divergent {
395 common_ancestor: 5,
396 local_version: 7,
397 remote_version: 8,
398 };
399 let display = conflict.to_string();
400 assert!(display.contains("Divergent"));
401 assert!(display.contains('5'));
402 }
403}