pocket_cli/vcs/
merge.rs

1//! Merge functionality for Pocket VCS
2//!
3//! Handles merging changes between timelines.
4
5use std::path::PathBuf;
6use std::collections::HashMap;
7use anyhow::{Result, anyhow};
8use std::fs;
9use toml;
10use chrono::Utc;
11use colored::Colorize;
12
13use crate::vcs::{
14    ShoveId, ObjectId, ObjectStore, Tree, TreeEntry,
15    Repository, Timeline, Shove, Author
16};
17use crate::vcs::objects::{EntryType};
18
19/// Strategy to use when merging
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum MergeStrategy {
22    /// Automatic merge (default)
23    Auto,
24    /// Fast-forward only (fail if not possible)
25    FastForwardOnly,
26    /// Always create a merge shove, even for fast-forward
27    AlwaysCreateShove,
28    /// Resolve conflicts with "ours" strategy
29    Ours,
30    /// Resolve conflicts with "theirs" strategy
31    Theirs,
32}
33
34/// Result of a merge operation
35#[derive(Debug)]
36pub struct MergeResult {
37    /// Whether the merge was successful
38    pub success: bool,
39    
40    /// The resulting shove ID (if successful)
41    pub shove_id: Option<ShoveId>,
42    
43    /// Whether the merge was a fast-forward
44    pub fast_forward: bool,
45    
46    /// Conflicts that occurred during the merge
47    pub conflicts: Vec<MergeConflict>,
48}
49
50/// A conflict that occurred during a merge
51#[derive(Debug)]
52pub struct MergeConflict {
53    /// Path to the conflicted file
54    pub path: PathBuf,
55    
56    /// Base version (common ancestor)
57    pub base_id: Option<ObjectId>,
58    
59    /// "Ours" version
60    pub ours_id: Option<ObjectId>,
61    
62    /// "Theirs" version
63    pub theirs_id: Option<ObjectId>,
64    
65    /// Resolution (if any)
66    pub resolution: Option<ConflictResolution>,
67}
68
69/// Resolution for a merge conflict
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum ConflictResolution {
72    /// Use "ours" version
73    UseOurs,
74    /// Use "theirs" version
75    UseTheirs,
76    /// Use a custom merged version
77    UseMerged(ObjectId),
78}
79
80/// Merger for handling merge operations
81pub struct Merger<'a> {
82    /// Repository to operate on
83    pub repo: &'a Repository,
84    
85    /// Strategy to use for merging
86    pub strategy: MergeStrategy,
87}
88
89impl<'a> Merger<'a> {
90    /// Create a new merger with the default strategy
91    pub fn new(repo: &'a Repository) -> Self {
92        Self {
93            repo,
94            strategy: MergeStrategy::Auto,
95        }
96    }
97    
98    /// Create a new merger with a custom strategy
99    pub fn with_strategy(repo: &'a Repository, strategy: MergeStrategy) -> Self {
100        Self { repo, strategy }
101    }
102    
103    /// Merge a timeline into the current timeline
104    pub fn merge_timeline(&self, other_timeline: &Timeline) -> Result<MergeResult> {
105        // Get the current timeline and head
106        let current_timeline = &self.repo.current_timeline;
107        let current_head = current_timeline.head.as_ref()
108            .ok_or_else(|| anyhow!("Current timeline has no head"))?;
109        
110        // Get the other timeline's head
111        let other_head = other_timeline.head.as_ref()
112            .ok_or_else(|| anyhow!("Other timeline has no head"))?;
113        
114        // If the heads are the same, nothing to do
115        if current_head == other_head {
116            return Ok(MergeResult {
117                success: true,
118                shove_id: Some(current_head.clone()),
119                fast_forward: true,
120                conflicts: vec![],
121            });
122        }
123        
124        // Find the common ancestor
125        let common_ancestor = self.find_common_ancestor(current_head, other_head)?;
126        
127        // Check if this is a fast-forward merge
128        if let Some(ancestor_id) = &common_ancestor {
129            if ancestor_id == current_head {
130                // Current is an ancestor of other, so we can fast-forward
131                if self.strategy == MergeStrategy::AlwaysCreateShove {
132                    // Create a merge shove even though it's a fast-forward
133                    return self.create_merge_shove(current_head, other_head, ancestor_id);
134                } else {
135                    // Just update the current timeline to point to other's head
136                    return Ok(MergeResult {
137                        success: true,
138                        shove_id: Some(other_head.clone()),
139                        fast_forward: true,
140                        conflicts: vec![],
141                    });
142                }
143            } else if ancestor_id == other_head {
144                // Other is an ancestor of current, nothing to do
145                return Ok(MergeResult {
146                    success: true,
147                    shove_id: Some(current_head.clone()),
148                    fast_forward: true,
149                    conflicts: vec![],
150                });
151            }
152        }
153        
154        // If we're here, it's not a fast-forward merge
155        if self.strategy == MergeStrategy::FastForwardOnly {
156            return Ok(MergeResult {
157                success: false,
158                shove_id: None,
159                fast_forward: false,
160                conflicts: vec![],
161            });
162        }
163        
164        // Perform a three-way merge
165        self.three_way_merge(current_head, other_head, common_ancestor.as_ref())
166    }
167    
168    /// Find the common ancestor of two shoves
169    fn find_common_ancestor(&self, a: &ShoveId, b: &ShoveId) -> Result<Option<ShoveId>> {
170        // Load both shoves
171        let a_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", a.as_str()));
172        let b_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", b.as_str()));
173        
174        if !a_path.exists() || !b_path.exists() {
175            return Err(anyhow!("One or both shoves not found"));
176        }
177        
178        // Load the shoves
179        let a_content = fs::read_to_string(&a_path)?;
180        let b_content = fs::read_to_string(&b_path)?;
181        
182        let a_shove: Shove = toml::from_str(&a_content)?;
183        let b_shove: Shove = toml::from_str(&b_content)?;
184        
185        // If either is an ancestor of the other, return it
186        if self.is_ancestor_of(a, b)? {
187            return Ok(Some(a.clone()));
188        }
189        
190        if self.is_ancestor_of(b, a)? {
191            return Ok(Some(b.clone()));
192        }
193        
194        // Otherwise, find the most recent common ancestor
195        // Start with all ancestors of A
196        let a_ancestors = self.get_ancestors(a)?;
197        
198        // For each ancestor of B, check if it's also an ancestor of A
199        for b_ancestor in self.get_ancestors(b)? {
200            if a_ancestors.contains(&b_ancestor) {
201                return Ok(Some(b_ancestor));
202            }
203        }
204        
205        // No common ancestor found
206        Ok(None)
207    }
208    
209    // Check if a is an ancestor of b
210    fn is_ancestor_of(&self, a: &ShoveId, b: &ShoveId) -> Result<bool> {
211        if a == b {
212            return Ok(true);
213        }
214        
215        // Load b's shove
216        let b_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", b.as_str()));
217        let b_content = fs::read_to_string(&b_path)?;
218        let b_shove: Shove = toml::from_str(&b_content)?;
219        
220        // Check if a is a direct parent of b
221        for parent_id in &b_shove.parent_ids {
222            if parent_id == a {
223                return Ok(true);
224            }
225            
226            // Recursively check if a is an ancestor of any of b's parents
227            if self.is_ancestor_of(a, parent_id)? {
228                return Ok(true);
229            }
230        }
231        
232        Ok(false)
233    }
234    
235    // Get all ancestors of a shove
236    fn get_ancestors(&self, id: &ShoveId) -> Result<Vec<ShoveId>> {
237        let mut ancestors = Vec::new();
238        let mut to_process = vec![id.clone()];
239        
240        while let Some(current_id) = to_process.pop() {
241            // Skip if we've already processed this shove
242            if ancestors.contains(&current_id) {
243                continue;
244            }
245            
246            // Add to ancestors
247            ancestors.push(current_id.clone());
248            
249            // Load the shove
250            let shove_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", current_id.as_str()));
251            if !shove_path.exists() {
252                continue;
253            }
254            
255            let shove_content = fs::read_to_string(&shove_path)?;
256            let shove: Shove = toml::from_str(&shove_content)?;
257            
258            // Add all parents to the processing queue
259            for parent_id in shove.parent_ids {
260                to_process.push(parent_id);
261            }
262        }
263        
264        Ok(ancestors)
265    }
266    
267    /// Perform a three-way merge
268    fn three_way_merge(
269        &self,
270        ours: &ShoveId,
271        theirs: &ShoveId,
272        base: Option<&ShoveId>,
273    ) -> Result<MergeResult> {
274        // Get the base shove (common ancestor)
275        let base_shove = if let Some(base_id) = base {
276            // Load the base shove
277            let base_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", base_id.as_str()));
278            if base_path.exists() {
279                let base_content = fs::read_to_string(&base_path)?;
280                Some(toml::from_str::<Shove>(&base_content)?)
281            } else {
282                return Err(anyhow!("Base shove not found: {}", base_id.as_str()));
283            }
284        } else {
285            None
286        };
287        
288        // Load our shove
289        let our_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", ours.as_str()));
290        let our_content = fs::read_to_string(&our_path)?;
291        let our_shove: Shove = toml::from_str(&our_content)?;
292        
293        // Load their shove
294        let their_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", theirs.as_str()));
295        let their_content = fs::read_to_string(&their_path)?;
296        let their_shove: Shove = toml::from_str(&their_content)?;
297        
298        // Get the trees
299        let our_tree_path = self.repo.path.join(".pocket").join("objects").join(our_shove.root_tree_id.as_str());
300        let our_tree_content = fs::read_to_string(&our_tree_path)?;
301        let our_tree: Tree = toml::from_str(&our_tree_content)?;
302        
303        let their_tree_path = self.repo.path.join(".pocket").join("objects").join(their_shove.root_tree_id.as_str());
304        let their_tree_content = fs::read_to_string(&their_tree_path)?;
305        let their_tree: Tree = toml::from_str(&their_tree_content)?;
306        
307        let base_tree = if let Some(base_shove) = &base_shove {
308            let base_tree_path = self.repo.path.join(".pocket").join("objects").join(base_shove.root_tree_id.as_str());
309            let base_tree_content = fs::read_to_string(&base_tree_path)?;
310            Some(toml::from_str::<Tree>(&base_tree_content)?)
311        } else {
312            None
313        };
314        
315        // Create maps for easier lookup
316        let mut our_entries = HashMap::new();
317        for entry in our_tree.entries {
318            our_entries.insert(entry.name.clone(), entry);
319        }
320        
321        let mut their_entries = HashMap::new();
322        for entry in their_tree.entries {
323            their_entries.insert(entry.name.clone(), entry);
324        }
325        
326        let base_entries = if let Some(base_tree) = base_tree {
327            let mut entries = HashMap::new();
328            for entry in base_tree.entries {
329                entries.insert(entry.name.clone(), entry);
330            }
331            Some(entries)
332        } else {
333            None
334        };
335        
336        // Create a new tree for the merged result
337        let mut merged_entries = Vec::new();
338        let mut conflicts = Vec::new();
339        
340        // Process all files in our tree
341        for (name, our_entry) in &our_entries {
342            if our_entry.entry_type != EntryType::File {
343                // Skip non-file entries for simplicity
344                merged_entries.push(our_entry.clone());
345                continue;
346            }
347            
348            if let Some(their_entry) = their_entries.get(name) {
349                // File exists in both trees
350                if our_entry.id == their_entry.id {
351                    // Same content, no conflict
352                    merged_entries.push(our_entry.clone());
353                } else {
354                    // Different content, check base
355                    if let Some(base_entries) = &base_entries {
356                        if let Some(base_entry) = base_entries.get(name) {
357                            if our_entry.id == base_entry.id {
358                                // We didn't change, use theirs
359                                merged_entries.push(their_entry.clone());
360                            } else if their_entry.id == base_entry.id {
361                                // They didn't change, use ours
362                                merged_entries.push(our_entry.clone());
363                            } else {
364                                // Both changed, conflict
365                                match self.strategy {
366                                    MergeStrategy::Ours => {
367                                        // Use our version
368                                        merged_entries.push(our_entry.clone());
369                                    },
370                                    MergeStrategy::Theirs => {
371                                        // Use their version
372                                        merged_entries.push(their_entry.clone());
373                                    },
374                                    _ => {
375                                        // Create a conflict
376                                        conflicts.push(MergeConflict {
377                                            path: PathBuf::from(name),
378                                            base_id: Some(base_entry.id.clone()),
379                                            ours_id: Some(our_entry.id.clone()),
380                                            theirs_id: Some(their_entry.id.clone()),
381                                            resolution: None,
382                                        });
383                                        
384                                        // For now, use our version
385                                        merged_entries.push(our_entry.clone());
386                                    }
387                                }
388                            }
389                        } else {
390                            // Not in base, both added with different content
391                            match self.strategy {
392                                MergeStrategy::Ours => {
393                                    // Use our version
394                                    merged_entries.push(our_entry.clone());
395                                },
396                                MergeStrategy::Theirs => {
397                                    // Use their version
398                                    merged_entries.push(their_entry.clone());
399                                },
400                                _ => {
401                                    // Create a conflict
402                                    conflicts.push(MergeConflict {
403                                        path: PathBuf::from(name),
404                                        base_id: None,
405                                        ours_id: Some(our_entry.id.clone()),
406                                        theirs_id: Some(their_entry.id.clone()),
407                                        resolution: None,
408                                    });
409                                    
410                                    // For now, use our version
411                                    merged_entries.push(our_entry.clone());
412                                }
413                            }
414                        }
415                    } else {
416                        // No base, use strategy
417                        match self.strategy {
418                            MergeStrategy::Ours => {
419                                // Use our version
420                                merged_entries.push(our_entry.clone());
421                            },
422                            MergeStrategy::Theirs => {
423                                // Use their version
424                                merged_entries.push(their_entry.clone());
425                            },
426                            _ => {
427                                // Create a conflict
428                                conflicts.push(MergeConflict {
429                                    path: PathBuf::from(name),
430                                    base_id: None,
431                                    ours_id: Some(our_entry.id.clone()),
432                                    theirs_id: Some(their_entry.id.clone()),
433                                    resolution: None,
434                                });
435                                
436                                // For now, use our version
437                                merged_entries.push(our_entry.clone());
438                            }
439                        }
440                    }
441                }
442            } else {
443                // File only in our tree
444                if let Some(base_entries) = &base_entries {
445                    if let Some(_) = base_entries.get(name) {
446                        // In base but not in theirs, they deleted it
447                        match self.strategy {
448                            MergeStrategy::Ours => {
449                                // Keep our version
450                                merged_entries.push(our_entry.clone());
451                            },
452                            MergeStrategy::Theirs => {
453                                // They deleted it, so don't include
454                            },
455                            _ => {
456                                // Create a conflict
457                                conflicts.push(MergeConflict {
458                                    path: PathBuf::from(name),
459                                    base_id: Some(base_entries.get(name).unwrap().id.clone()),
460                                    ours_id: Some(our_entry.id.clone()),
461                                    theirs_id: None,
462                                    resolution: None,
463                                });
464                                
465                                // For now, keep our version
466                                merged_entries.push(our_entry.clone());
467                            }
468                        }
469                    } else {
470                        // Not in base, we added it
471                        merged_entries.push(our_entry.clone());
472                    }
473                } else {
474                    // No base, we added it
475                    merged_entries.push(our_entry.clone());
476                }
477            }
478        }
479        
480        // Process files only in their tree
481        for (name, their_entry) in &their_entries {
482            if their_entry.entry_type != EntryType::File {
483                // Skip non-file entries for simplicity
484                if !our_entries.contains_key(name) {
485                    merged_entries.push(their_entry.clone());
486                }
487                continue;
488            }
489            
490            if !our_entries.contains_key(name) {
491                // File only in their tree
492                if let Some(base_entries) = &base_entries {
493                    if let Some(_) = base_entries.get(name) {
494                        // In base but not in ours, we deleted it
495                        match self.strategy {
496                            MergeStrategy::Ours => {
497                                // We deleted it, so don't include
498                            },
499                            MergeStrategy::Theirs => {
500                                // Keep their version
501                                merged_entries.push(their_entry.clone());
502                            },
503                            _ => {
504                                // Create a conflict
505                                conflicts.push(MergeConflict {
506                                    path: PathBuf::from(name),
507                                    base_id: Some(base_entries.get(name).unwrap().id.clone()),
508                                    ours_id: None,
509                                    theirs_id: Some(their_entry.id.clone()),
510                                    resolution: None,
511                                });
512                                
513                                // For now, don't include (follow our deletion)
514                            }
515                        }
516                    } else {
517                        // Not in base, they added it
518                        merged_entries.push(their_entry.clone());
519                    }
520                } else {
521                    // No base, they added it
522                    merged_entries.push(their_entry.clone());
523                }
524            }
525        }
526        
527        // If there are conflicts and we're not using a strategy that resolves them automatically,
528        // return a result with conflicts
529        if !conflicts.is_empty() && self.strategy != MergeStrategy::Ours && self.strategy != MergeStrategy::Theirs {
530            return Ok(MergeResult {
531                success: false,
532                shove_id: None,
533                fast_forward: false,
534                conflicts,
535            });
536        }
537        
538        // Create a new tree with the merged entries
539        let merged_tree = Tree {
540            entries: merged_entries,
541        };
542        
543        // Store the merged tree
544        let object_store = ObjectStore::new(self.repo.path.clone());
545        let tree_id = object_store.store_tree(&merged_tree)?;
546        
547        // Create a new shove
548        let author = Author {
549            name: self.repo.config.user.name.clone(),
550            email: self.repo.config.user.email.clone(),
551            timestamp: Utc::now(),
552        };
553        
554        let mut parent_ids = vec![ours.clone()];
555        if ours != theirs {
556            parent_ids.push(theirs.clone());
557        }
558        
559        let message = format!("Merge {} into {}", 
560            their_shove.message.lines().next().unwrap_or(""),
561            our_shove.message.lines().next().unwrap_or(""));
562            
563        let shove = Shove::new(&self.repo.pile, parent_ids, author, &message, tree_id);
564        
565        // Save the shove
566        let shove_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", shove.id.as_str()));
567        shove.save(&shove_path)?;
568        
569        Ok(MergeResult {
570            success: true,
571            shove_id: Some(shove.id.clone()),
572            fast_forward: false,
573            conflicts: Vec::new(),
574        })
575    }
576    
577    /// Create a merge shove
578    fn create_merge_shove(
579        &self,
580        ours: &ShoveId,
581        theirs: &ShoveId,
582        base: &ShoveId,
583    ) -> Result<MergeResult> {
584        // Load the shoves
585        let ours_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", ours.as_str()));
586        let theirs_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", theirs.as_str()));
587        
588        let ours_content = fs::read_to_string(&ours_path)?;
589        let theirs_content = fs::read_to_string(&theirs_path)?;
590        
591        let our_shove: Shove = toml::from_str(&ours_content)?;
592        let their_shove: Shove = toml::from_str(&theirs_content)?;
593        
594        // Create a new merged tree
595        let merged_tree_id = self.merge_trees(&our_shove.root_tree_id, &their_shove.root_tree_id)?;
596        
597        // Create a new shove with both parents
598        let mut parent_ids = vec![our_shove.id.clone()];
599        parent_ids.push(their_shove.id.clone());
600        
601        // Create a merge message
602        let message = format!("Merge {} into {}", theirs.as_str(), ours.as_str());
603        
604        // Create the author information
605        let author = Author {
606            name: self.repo.config.user.name.clone(),
607            email: self.repo.config.user.email.clone(),
608            timestamp: Utc::now(),
609        };
610        
611        // Create the new shove
612        let new_shove = Shove::new(
613            &self.repo.pile,
614            parent_ids,
615            author,
616            &message,
617            merged_tree_id,
618        );
619        
620        // Save the shove
621        let shove_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", new_shove.id.as_str()));
622        new_shove.save(&shove_path)?;
623        
624        // Update the current timeline's head
625        let timeline_path = self.repo.path.join(".pocket").join("timelines").join("current");
626        let mut timeline = Timeline::load(&timeline_path)?;
627        timeline.head = Some(new_shove.id.clone());
628        timeline.save(&timeline_path)?;
629        
630        // Return the result
631        Ok(MergeResult {
632            success: true,
633            shove_id: Some(new_shove.id),
634            fast_forward: false,
635            conflicts: Vec::new(),
636        })
637    }
638    
639    fn merge_trees(&self, ours_id: &ObjectId, theirs_id: &ObjectId) -> Result<ObjectId> {
640        // Load the trees
641        let ours_path = self.repo.path.join(".pocket").join("objects").join(ours_id.as_str());
642        let theirs_path = self.repo.path.join(".pocket").join("objects").join(theirs_id.as_str());
643        
644        let ours_content = fs::read_to_string(&ours_path)?;
645        let theirs_content = fs::read_to_string(&theirs_path)?;
646        
647        let ours_tree: Tree = toml::from_str(&ours_content)?;
648        let theirs_tree: Tree = toml::from_str(&theirs_content)?;
649        
650        // Create a new tree with entries from both
651        let mut merged_entries = Vec::new();
652        let mut our_entries_map = std::collections::HashMap::new();
653        
654        // Add all our entries to the map for quick lookup
655        for entry in &ours_tree.entries {
656            our_entries_map.insert(entry.name.clone(), entry.clone());
657        }
658        
659        // Add all our entries to the merged tree
660        for entry in &ours_tree.entries {
661            merged_entries.push(entry.clone());
662        }
663        
664        // Add their entries if they don't exist in our tree
665        for entry in &theirs_tree.entries {
666            if !our_entries_map.contains_key(&entry.name) {
667                merged_entries.push(entry.clone());
668            }
669        }
670        
671        // Create the new tree
672        let merged_tree = Tree {
673            entries: merged_entries,
674        };
675        
676        // Save the tree
677        let object_store = ObjectStore::new(self.repo.path.clone());
678        let tree_id = object_store.store_tree(&merged_tree)?;
679        
680        Ok(tree_id)
681    }
682    
683    /// Resolve conflicts interactively
684    pub fn resolve_conflicts_interactively(&self, conflicts: &[MergeConflict]) -> Result<Vec<ConflictResolution>> {
685        let mut resolutions = Vec::new();
686        
687        println!("\n{} Resolving {} conflicts interactively", "🔄".bright_blue(), conflicts.len());
688        
689        for (i, conflict) in conflicts.iter().enumerate() {
690            println!("\n{} Conflict {}/{}: {}", "⚠️".yellow(), i + 1, conflicts.len(), conflict.path.display());
691            
692            // Display the conflict
693            self.display_conflict(conflict)?;
694            
695            // Ask for resolution
696            let resolution = self.prompt_for_resolution(conflict)?;
697            resolutions.push(resolution);
698            
699            println!("{} Conflict resolved", "✅".green());
700        }
701        
702        println!("\n{} All conflicts resolved", "🎉".green());
703        
704        Ok(resolutions)
705    }
706    
707    /// Display a conflict to the user
708    fn display_conflict(&self, conflict: &MergeConflict) -> Result<()> {
709        // Load the base, ours, and theirs content
710        let base_content = if let Some(id) = &conflict.base_id {
711            self.load_object_content(id)?
712        } else {
713            String::new()
714        };
715        
716        let ours_content = if let Some(id) = &conflict.ours_id {
717            self.load_object_content(id)?
718        } else {
719            String::new()
720        };
721        
722        let theirs_content = if let Some(id) = &conflict.theirs_id {
723            self.load_object_content(id)?
724        } else {
725            String::new()
726        };
727        
728        // Display the differences
729        println!("\n{} Base version:", "⚪".bright_black());
730        self.print_content(&base_content, "  ");
731        
732        println!("\n{} Our version (current timeline):", "🟢".green());
733        self.print_content(&ours_content, "  ");
734        
735        println!("\n{} Their version (incoming timeline):", "🔵".blue());
736        self.print_content(&theirs_content, "  ");
737        
738        Ok(())
739    }
740    
741    /// Print content with line numbers
742    fn print_content(&self, content: &str, prefix: &str) {
743        for (i, line) in content.lines().enumerate() {
744            println!("{}{:3} | {}", prefix, i + 1, line);
745        }
746        
747        if content.is_empty() {
748            println!("{}    | (empty file)", prefix);
749        }
750    }
751    
752    /// Prompt the user for conflict resolution
753    fn prompt_for_resolution(&self, conflict: &MergeConflict) -> Result<ConflictResolution> {
754        println!("\nHow would you like to resolve this conflict?");
755        println!("  1. {} Use our version (current timeline)", "🟢".green());
756        println!("  2. {} Use their version (incoming timeline)", "🔵".blue());
757        println!("  3. {} Edit and merge manually", "✏️".yellow());
758        
759        // In a real implementation, we would use a crate like dialoguer to get user input
760        // For now, we'll simulate the user choosing option 1
761        println!("\nSelected: 1. Use our version");
762        
763        if let Some(id) = &conflict.ours_id {
764            Ok(ConflictResolution::UseOurs)
765        } else {
766            // If our version doesn't exist, use theirs
767            Ok(ConflictResolution::UseTheirs)
768        }
769    }
770    
771    /// Load the content of an object
772    fn load_object_content(&self, id: &ObjectId) -> Result<String> {
773        let object_path = self.repo.path.join(".pocket").join("objects").join(id.as_str());
774        let content = fs::read_to_string(object_path)?;
775        Ok(content)
776    }
777    
778    /// Apply conflict resolutions to create a merged tree
779    pub fn apply_resolutions(&self, conflicts: &[MergeConflict], resolutions: &[ConflictResolution]) -> Result<Tree> {
780        // Create a new tree
781        let mut merged_tree = Tree { entries: Vec::new() };
782        
783        // Apply each resolution
784        for (conflict, resolution) in conflicts.iter().zip(resolutions.iter()) {
785            match resolution {
786                ConflictResolution::UseOurs => {
787                    if let Some(id) = &conflict.ours_id {
788                        merged_tree.entries.push(TreeEntry {
789                            name: conflict.path.to_string_lossy().to_string(),
790                            id: id.clone(),
791                            entry_type: EntryType::File,
792                            permissions: 0o644,
793                        });
794                    }
795                },
796                ConflictResolution::UseTheirs => {
797                    if let Some(id) = &conflict.theirs_id {
798                        merged_tree.entries.push(TreeEntry {
799                            name: conflict.path.to_string_lossy().to_string(),
800                            id: id.clone(),
801                            entry_type: EntryType::File,
802                            permissions: 0o644,
803                        });
804                    }
805                },
806                ConflictResolution::UseMerged(id) => {
807                    merged_tree.entries.push(TreeEntry {
808                        name: conflict.path.to_string_lossy().to_string(),
809                        id: id.clone(),
810                        entry_type: EntryType::File,
811                        permissions: 0o644,
812                    });
813                },
814            }
815        }
816        
817        Ok(merged_tree)
818    }
819}