pocket_cli/vcs/
commands.rs

1//! Command handlers for Pocket VCS
2//!
3//! Implements the CLI commands for VCS operations.
4
5use std::path::{Path, PathBuf};
6use anyhow::{Result, anyhow};
7use colored::Colorize;
8use glob;
9use dialoguer::{theme::ColorfulTheme, Select, Input, Confirm};
10use indicatif::{ProgressBar, ProgressStyle};
11
12use crate::vcs::{
13    Repository, Timeline, Shove, ShoveId, Pile,
14    ObjectStore, MergeStrategy
15};
16use crate::vcs::remote::RemoteManager;
17
18/// Create a new repository
19pub fn new_repo_command(path: &Path, template: Option<&str>, no_default: bool) -> Result<()> {
20    println!("Creating new Pocket repository at {}", path.display());
21    
22    let repo = Repository::new(path)?;
23    
24    if !no_default {
25        // Create default files like README.md and .pocketignore
26        let readme_path = path.join("README.md");
27        if !readme_path.exists() {
28            std::fs::write(readme_path, "# New Pocket Repository\n\nCreated with Pocket VCS.\n")?;
29        }
30        
31        let ignore_path = path.join(".pocketignore");
32        if !ignore_path.exists() {
33            std::fs::write(ignore_path, "# Pocket ignore file\n.DS_Store\n*.log\n")?;
34        }
35    }
36    
37    println!("Repository created successfully.");
38    println!("Current timeline: {}", repo.current_timeline.name);
39    
40    Ok(())
41}
42
43/// Display the status of the repository
44pub fn status_command(path: &Path, verbose: bool) -> Result<()> {
45    let repo = Repository::open(path)?;
46    let status = repo.status()?;
47    
48    // Create a beautiful header
49    println!("\n{} {} {}\n", "🔍".bright_cyan(), "Pocket VCS Status".bold().bright_cyan(), "🔍".bright_cyan());
50    
51    // Current timeline
52    println!("{} {}: {}", "đŸŒŋ".green(), "Current Timeline".bold(), status.current_timeline.bright_green());
53    
54    // Head shove
55    if let Some(head) = &status.head_shove {
56        let shove_path = repo.path.join(".pocket").join("shoves").join(format!("{}.toml", head.as_str()));
57        if shove_path.exists() {
58            let shove_content = std::fs::read_to_string(shove_path)?;
59            let shove: Shove = toml::from_str(&shove_content)?;
60            println!("{} {}: {} ({})", "📌".yellow(), "HEAD Shove".bold(), 
61                head.as_str()[0..8].bright_yellow(), 
62                shove.message.lines().next().unwrap_or("").italic());
63        } else {
64            println!("{} {}: {}", "📌".yellow(), "HEAD Shove".bold(), head.as_str()[0..8].bright_yellow());
65        }
66    } else {
67        println!("{} {}: {}", "📌".yellow(), "HEAD Shove".bold(), "None".dimmed());
68    }
69    
70    // Piled files (staged)
71    if !status.piled_files.is_empty() {
72        println!("\n{} {} {}", "đŸ“Ļ".green(), "Piled Changes".bold().green(), format!("({})", status.piled_files.len()).green());
73        for entry in &status.piled_files {
74            let status_icon = match entry.status {
75                crate::vcs::PileStatus::Added => "🆕".green(),
76                crate::vcs::PileStatus::Modified => "📝".yellow(),
77                crate::vcs::PileStatus::Deleted => "đŸ—‘ī¸".red(),
78                crate::vcs::PileStatus::Renamed(_) => "📋".blue(),
79            };
80            println!("  {} {}", status_icon, entry.original_path.display().to_string().bright_white());
81        }
82    } else {
83        println!("\n{} {}", "đŸ“Ļ".dimmed(), "No Piled Changes".dimmed());
84    }
85    
86    // Modified files (unstaged)
87    if !status.modified_files.is_empty() {
88        println!("\n{} {} {}", "📄".yellow(), "Modified Files".bold().yellow(), format!("({})", status.modified_files.len()).yellow());
89        for file in &status.modified_files {
90            println!("  {} {}", "📝".yellow(), file.display().to_string().bright_white());
91        }
92    } else {
93        println!("\n{} {}", "📄".dimmed(), "No Modified Files".dimmed());
94    }
95    
96    // Untracked files
97    if !status.untracked_files.is_empty() {
98        println!("\n{} {} {}", "❓".bright_red(), "Untracked Files".bold().bright_red(), format!("({})", status.untracked_files.len()).bright_red());
99        
100        // If there are too many untracked files, only show a few
101        let max_display = if verbose { status.untracked_files.len() } else { 5.min(status.untracked_files.len()) };
102        for file in &status.untracked_files[0..max_display] {
103            println!("  {} {}", "❓".bright_red(), file.display().to_string().bright_white());
104        }
105        
106        if status.untracked_files.len() > max_display {
107            println!("  {} {} more untracked files", "⋯".bright_red(), status.untracked_files.len() - max_display);
108            println!("  {} Use {} to see all files", "💡".yellow(), "--verbose".bright_cyan());
109        }
110    } else {
111        println!("\n{} {}", "❓".dimmed(), "No Untracked Files".dimmed());
112    }
113    
114    // Conflicts
115    if !status.conflicts.is_empty() {
116        println!("\n{} {} {}", "âš ī¸".bright_red(), "Conflicts".bold().bright_red(), format!("({})", status.conflicts.len()).bright_red());
117        for file in &status.conflicts {
118            println!("  {} {}", "âš ī¸".bright_red(), file.display().to_string().bright_white());
119        }
120        println!("  {} Use {} to resolve conflicts", "💡".yellow(), "pocket merge --resolve".bright_cyan());
121    }
122    
123    // Show a helpful tip
124    println!("\n{} {}", "💡".yellow(), "Tip: Use 'pocket help' to see available commands".italic());
125    
126    Ok(())
127}
128
129/// Interactive pile command
130pub fn interactive_pile_command(path: &Path, files: Vec<String>, all: bool, pattern: Option<String>) -> Result<()> {
131    // If files, all flag, or pattern is provided, use the non-interactive pile command
132    if !files.is_empty() || all || pattern.is_some() {
133        let file_paths: Vec<&Path> = files.iter().map(|f| Path::new(f)).collect();
134        return pile_command(path, file_paths, all, pattern.as_deref());
135    }
136
137    let repo = Repository::open(path)?;
138    let status = repo.status()?;
139    
140    println!("\n{} {} {}\n", "đŸ“Ļ".green(), "Interactive Pile".bold().green(), "đŸ“Ļ".green());
141    
142    // No files to pile
143    if status.modified_files.is_empty() && status.untracked_files.is_empty() {
144        println!("{} {}", "â„šī¸".blue(), "No files to pile. Your working directory is clean.".italic());
145        return Ok(());
146    }
147    
148    // Combine modified and untracked files
149    let mut files_to_choose = Vec::new();
150    
151    for file in &status.modified_files {
152        files_to_choose.push((file.clone(), "Modified".to_string(), "📝".to_string()));
153    }
154    
155    for file in &status.untracked_files {
156        files_to_choose.push((file.clone(), "Untracked".to_string(), "❓".to_string()));
157    }
158    
159    // Sort files by path
160    files_to_choose.sort_by(|a, b| a.0.cmp(&b.0));
161    
162    // Create selection items
163    let items: Vec<String> = files_to_choose.iter()
164        .map(|(path, status, icon)| format!("{} {} ({})", icon, path.display(), status))
165        .collect();
166    
167    // Add "All files" and "Done" options
168    let all_files_option = format!("đŸ“Ļ Pile all files ({})", files_to_choose.len());
169    let done_option = "✅ Done".to_string();
170    
171    let mut selection_items = vec![all_files_option.clone()];
172    selection_items.extend(items);
173    selection_items.push(done_option.clone());
174    
175    // Track which files have been piled
176    let mut piled_files = Vec::new();
177    
178    // Create progress bar for piling
179    let progress_style = ProgressStyle::default_bar()
180        .template("{spinner:.green} [{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}")
181        .unwrap()
182        .progress_chars("##-");
183    
184    // Interactive selection loop
185    loop {
186        println!("\n{} {} files piled so far", "📊".blue(), piled_files.len());
187        
188        let selection = Select::with_theme(&ColorfulTheme::default())
189            .with_prompt("Select files to pile (↑↓ to move, Enter to select)")
190            .default(0)
191            .items(&selection_items)
192            .interact()
193            .unwrap();
194        
195        if selection_items[selection] == done_option {
196            break;
197        } else if selection_items[selection] == all_files_option {
198            // Pile all files
199            let pb = ProgressBar::new(files_to_choose.len() as u64);
200            pb.set_style(progress_style.clone());
201            pb.set_message("Piling files...");
202            
203            for (i, (file, _, _)) in files_to_choose.iter().enumerate() {
204                if !piled_files.contains(file) {
205                    // In a real implementation, we would call repo.pile.add_path() here
206                    piled_files.push(file.clone());
207                }
208                pb.set_position(i as u64 + 1);
209                pb.set_message(format!("Piled {}", file.display()));
210                std::thread::sleep(std::time::Duration::from_millis(50)); // Simulate work
211            }
212            
213            pb.finish_with_message(format!("✅ All {} files piled successfully", files_to_choose.len()));
214            break;
215        } else {
216            // Pile individual file
217            let (file, _, _) = &files_to_choose[selection - 1]; // -1 because of "All files" option
218            
219            if !piled_files.contains(file) {
220                // In a real implementation, we would call repo.pile.add_path() here
221                piled_files.push(file.clone());
222                println!("{} Piled: {}", "✅".green(), file.display());
223            } else {
224                println!("{} Already piled: {}", "â„šī¸".blue(), file.display());
225            }
226        }
227    }
228    
229    if !piled_files.is_empty() {
230        println!("\n{} {} files piled successfully", "✅".green(), piled_files.len());
231        println!("{} Use {} to create a shove", "💡".yellow(), "pocket shove".bright_cyan());
232    } else {
233        println!("\n{} No files were piled", "â„šī¸".blue());
234    }
235    
236    Ok(())
237}
238
239/// Interactive shove command
240pub fn interactive_shove_command(path: &Path) -> Result<()> {
241    let repo = Repository::open(path)?;
242    
243    println!("\n{} {} {}\n", "đŸ“Ļ".green(), "Create Shove".bold().green(), "đŸ“Ļ".green());
244    
245    // Check if there are piled changes
246    let status = repo.status()?;
247    if status.piled_files.is_empty() {
248        println!("{} {}", "â„šī¸".blue(), "No piled changes to shove.".italic());
249        
250        if !status.modified_files.is_empty() || !status.untracked_files.is_empty() {
251            println!("{} Use {} to pile changes first", "💡".yellow(), "pocket pile".bright_cyan());
252        }
253        
254        return Ok(());
255    }
256    
257    // Show piled changes
258    println!("{} {} {}", "đŸ“Ļ".green(), "Piled Changes".bold().green(), format!("({})", status.piled_files.len()).green());
259    for entry in &status.piled_files {
260        let status_icon = match entry.status {
261            crate::vcs::PileStatus::Added => "🆕".green(),
262            crate::vcs::PileStatus::Modified => "📝".yellow(),
263            crate::vcs::PileStatus::Deleted => "đŸ—‘ī¸".red(),
264            crate::vcs::PileStatus::Renamed(_) => "📋".blue(),
265        };
266        println!("  {} {}", status_icon, entry.original_path.display().to_string().bright_white());
267    }
268    
269    // Get shove message
270    println!("\n{} {}", "âœī¸".yellow(), "Enter a shove message:".bold());
271    let message = Input::<String>::with_theme(&ColorfulTheme::default())
272        .with_prompt("Message")
273        .interact_text()
274        .unwrap();
275    
276    // Confirm shove creation
277    if !Confirm::with_theme(&ColorfulTheme::default())
278        .with_prompt("Create shove with these changes?")
279        .default(true)
280        .interact()
281        .unwrap()
282    {
283        println!("\n{} Shove creation cancelled", "❌".red());
284        return Ok(());
285    }
286    
287    // Create progress bar for shoving
288    let pb = ProgressBar::new(100);
289    pb.set_style(ProgressStyle::default_bar()
290        .template("{spinner:.green} [{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}")
291        .unwrap()
292        .progress_chars("##-"));
293    
294    // Simulate shove creation
295    pb.set_message("Creating tree objects...");
296    for i in 0..30 {
297        pb.set_position(i);
298        std::thread::sleep(std::time::Duration::from_millis(10));
299    }
300    
301    pb.set_message("Calculating changes...");
302    for i in 30..60 {
303        pb.set_position(i);
304        std::thread::sleep(std::time::Duration::from_millis(10));
305    }
306    
307    pb.set_message("Creating shove...");
308    for i in 60..90 {
309        pb.set_position(i);
310        std::thread::sleep(std::time::Duration::from_millis(10));
311    }
312    
313    pb.set_message("Updating timeline...");
314    for i in 90..100 {
315        pb.set_position(i);
316        std::thread::sleep(std::time::Duration::from_millis(10));
317    }
318    
319    // In a real implementation, we would call repo.create_shove() here
320    let shove_id = "abcdef1234567890";
321    let shove_id_short = &shove_id[0..8];
322    
323    pb.finish_with_message(format!("✅ Shove created successfully: {}", shove_id_short.bright_yellow()));
324    
325    println!("\n{} {} created with message:", "✅".green(), format!("Shove {}", shove_id_short).bright_yellow());
326    println!("  {}", message.italic());
327    
328    Ok(())
329}
330
331/// Interactive timeline command
332pub fn interactive_timeline_command(path: &Path) -> Result<()> {
333    let repo = Repository::open(path)?;
334    
335    println!("\n{} {} {}\n", "đŸŒŋ".green(), "Timeline Management".bold().green(), "đŸŒŋ".green());
336    
337    // Get current timeline
338    let status = repo.status()?;
339    println!("{} {}: {}", "đŸŒŋ".green(), "Current Timeline".bold(), status.current_timeline.bright_green());
340    
341    // List available timelines
342    let timelines_dir = repo.path.join(".pocket").join("timelines");
343    let mut timelines = Vec::new();
344    
345    if timelines_dir.exists() {
346        for entry in std::fs::read_dir(timelines_dir)? {
347            let entry = entry?;
348            if entry.file_type()?.is_file() {
349                let filename = entry.file_name();
350                let filename_str = filename.to_string_lossy();
351                if filename_str.ends_with(".toml") {
352                    let timeline_name = filename_str.trim_end_matches(".toml").to_string();
353                    timelines.push(timeline_name);
354                }
355            }
356        }
357    }
358    
359    // Sort timelines
360    timelines.sort();
361    
362    // Show available timelines
363    println!("\n{} {} {}", "📋".blue(), "Available Timelines".bold().blue(), format!("({})", timelines.len()).blue());
364    for timeline in &timelines {
365        let current_marker = if timeline == &status.current_timeline { "✓ ".green() } else { "  ".normal() };
366        println!("{}{} {}", current_marker, "đŸŒŋ".green(), timeline.bright_white());
367    }
368    
369    // Show options
370    println!("\n{} {}", "🔍".cyan(), "What would you like to do?".bold());
371    
372    let options = vec![
373        "🆕 Create new timeline",
374        "🔄 Switch timeline",
375        "🔙 Back to main menu",
376    ];
377    
378    let selection = Select::with_theme(&ColorfulTheme::default())
379        .with_prompt("Select an option")
380        .default(0)
381        .items(&options)
382        .interact()
383        .unwrap();
384    
385    match selection {
386        0 => {
387            // Create new timeline
388            println!("\n{} {}", "🆕".green(), "Create New Timeline".bold());
389            
390            let name = Input::<String>::with_theme(&ColorfulTheme::default())
391                .with_prompt("Timeline name")
392                .interact_text()
393                .unwrap();
394            
395            let base_on_current = Confirm::with_theme(&ColorfulTheme::default())
396                .with_prompt(format!("Base on current timeline ({})?", status.current_timeline))
397                .default(true)
398                .interact()
399                .unwrap();
400            
401            println!("\n{} Creating timeline: {}", "âŗ".yellow(), name.bright_white());
402            
403            // In a real implementation, we would create the timeline here
404            std::thread::sleep(std::time::Duration::from_millis(500));
405            
406            println!("{} Timeline {} created successfully", "✅".green(), name.bright_green());
407            
408            if Confirm::with_theme(&ColorfulTheme::default())
409                .with_prompt(format!("Switch to new timeline ({})?", name))
410                .default(true)
411                .interact()
412                .unwrap()
413            {
414                println!("\n{} Switching to timeline: {}", "âŗ".yellow(), name.bright_white());
415                
416                // In a real implementation, we would switch to the timeline here
417                std::thread::sleep(std::time::Duration::from_millis(500));
418                
419                println!("{} Switched to timeline {}", "✅".green(), name.bright_green());
420            }
421        },
422        1 => {
423            // Switch timeline
424            println!("\n{} {}", "🔄".green(), "Switch Timeline".bold());
425            
426            if timelines.is_empty() {
427                println!("{} No timelines available", "❌".red());
428                return Ok(());
429            }
430            
431            let selection = Select::with_theme(&ColorfulTheme::default())
432                .with_prompt("Select timeline to switch to")
433                .default(0)
434                .items(&timelines)
435                .interact()
436                .unwrap();
437            
438            let selected_timeline = &timelines[selection];
439            
440            println!("\n{} Switching to timeline: {}", "âŗ".yellow(), selected_timeline.bright_white());
441            
442            // In a real implementation, we would switch to the timeline here
443            std::thread::sleep(std::time::Duration::from_millis(500));
444            
445            println!("{} Switched to timeline {}", "✅".green(), selected_timeline.bright_green());
446        },
447        _ => {
448            // Back to main menu
449            println!("\n{} Returning to main menu", "🔙".blue());
450        }
451    }
452    
453    Ok(())
454}
455
456/// Add files to the pile (staging area)
457pub fn pile_command(path: &Path, files: Vec<&Path>, all: bool, pattern: Option<&str>) -> Result<()> {
458    let repo = Repository::open(path)?;
459    let mut pile = repo.pile.clone();
460    let mut added_count = 0;
461    
462    // Read ignore patterns from .pocketignore if it exists
463    let ignore_path = repo.path.join(".pocketignore");
464    let ignore_patterns = if ignore_path.exists() {
465        read_ignore_patterns(&ignore_path)?
466    } else {
467        repo.config.core.ignore_patterns.clone()
468    };
469    
470    // Function to check if a file should be ignored
471    let should_ignore = |file_path: &Path| -> bool {
472        // Skip files in .git, .pocket, or other VCS directories
473        if file_path.to_string_lossy().contains("/.pocket/") || 
474           file_path.to_string_lossy().contains("/.git/") {
475            return true;
476        }
477        
478        // Check if the file matches any ignore pattern
479        let relative_path = if let Ok(rel_path) = file_path.strip_prefix(&repo.path) {
480            rel_path
481        } else {
482            file_path
483        };
484        
485        ignore_patterns.iter().any(|pattern| {
486            if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
487                glob_pattern.matches_path(relative_path)
488            } else {
489                false
490            }
491        })
492    };
493    
494    // If --all flag is provided, add all modified files
495    if all {
496        let status = repo.status()?;
497        for file_path in &status.modified_files {
498            if !should_ignore(file_path) {
499                pile.add_path(file_path, &repo.object_store)?;
500                println!("{} {}", "✅".green(), format!("Added: {}", file_path.display()).bright_white());
501                added_count += 1;
502            }
503        }
504        
505        for file_path in &status.untracked_files {
506            if !should_ignore(file_path) {
507                pile.add_path(file_path, &repo.object_store)?;
508                println!("{} {}", "✅".green(), format!("Added: {}", file_path.display()).bright_white());
509                added_count += 1;
510            }
511        }
512    }
513    // If pattern is provided, add files matching the pattern
514    else if let Some(pattern_str) = pattern {
515        let matches = glob::glob(pattern_str)?;
516        for entry in matches {
517            match entry {
518                Ok(path) => {
519                    if path.is_file() && !should_ignore(&path) {
520                        pile.add_path(&path, &repo.object_store)?;
521                        println!("{} {}", "✅".green(), format!("Added: {}", path.display()).bright_white());
522                        added_count += 1;
523                    } else if path.is_dir() {
524                        // Recursively add all files in the directory
525                        added_count += add_directory_recursively(&path, &mut pile, &repo.object_store, &repo.path, &ignore_patterns)?;
526                    }
527                }
528                Err(e) => {
529                    println!("{} {}", "âš ī¸".yellow(), format!("Error matching pattern: {}", e).yellow());
530                }
531            }
532        }
533    }
534    // Otherwise, add the specified files
535    else {
536        for file_path in files {
537            if file_path.is_file() && !should_ignore(file_path) {
538                pile.add_path(file_path, &repo.object_store)?;
539                println!("{} {}", "✅".green(), format!("Added: {}", file_path.display()).bright_white());
540                added_count += 1;
541            } else if file_path.is_dir() {
542                // Recursively add all files in the directory
543                added_count += add_directory_recursively(file_path, &mut pile, &repo.object_store, &repo.path, &ignore_patterns)?;
544            } else {
545                // Check if it's a glob pattern
546                let path_str = file_path.to_string_lossy();
547                if path_str.contains('*') || path_str.contains('?') || path_str.contains('[') {
548                    let matches = glob::glob(&path_str)?;
549                    for entry in matches {
550                        match entry {
551                            Ok(path) => {
552                                if path.is_file() && !should_ignore(&path) {
553                                    pile.add_path(&path, &repo.object_store)?;
554                                    println!("{} {}", "✅".green(), format!("Added: {}", path.display()).bright_white());
555                                    added_count += 1;
556                                } else if path.is_dir() {
557                                    // Recursively add all files in the directory
558                                    added_count += add_directory_recursively(&path, &mut pile, &repo.object_store, &repo.path, &ignore_patterns)?;
559                                }
560                            }
561                            Err(e) => {
562                                println!("{} {}", "âš ī¸".yellow(), format!("Error matching pattern: {}", e).yellow());
563                            }
564                        }
565                    }
566                } else {
567                    println!("{} {}", "âš ī¸".yellow(), format!("File not found: {}", file_path.display()).yellow());
568                }
569            }
570        }
571    }
572    
573    // Save the updated pile
574    let pile_path = repo.path.join(".pocket").join("piles").join("current.toml");
575    // Ensure the piles directory exists
576    std::fs::create_dir_all(pile_path.parent().unwrap())?;
577    pile.save(&pile_path)?;
578    
579    if added_count > 0 {
580        println!("\n{} {} files added to the pile", "✅".green(), added_count);
581        println!("{} Use {} to create a shove", "💡".yellow(), "pocket shove".bright_cyan());
582    } else {
583        println!("{} No files added to the pile", "â„šī¸".blue());
584    }
585    
586    Ok(())
587}
588
589/// Recursively add all files in a directory to the pile
590fn add_directory_recursively(dir_path: &Path, pile: &mut Pile, object_store: &ObjectStore, repo_path: &Path, ignore_patterns: &[String]) -> Result<usize> {
591    let mut added_count = 0;
592    
593    // Create a progress bar for directory scanning
594    let spinner = ProgressBar::new_spinner();
595    spinner.set_style(
596        ProgressStyle::default_spinner()
597            .template("{spinner:.green} {msg}")
598            .unwrap()
599    );
600    spinner.set_message(format!("Scanning directory: {}", dir_path.display()));
601    
602    // Use walkdir to recursively iterate through the directory
603    for entry in walkdir::WalkDir::new(dir_path)
604        .follow_links(false)
605        .into_iter()
606        .filter_map(|e| e.ok()) {
607            
608        spinner.tick();
609        
610        let path = entry.path();
611        if path.is_file() {
612            // Skip files in .git, .pocket, or other VCS directories
613            if path.to_string_lossy().contains("/.pocket/") || 
614               path.to_string_lossy().contains("/.git/") {
615                continue;
616            }
617            
618            // Check if the file matches any ignore pattern
619            let relative_path = if let Ok(rel_path) = path.strip_prefix(repo_path) {
620                rel_path
621            } else {
622                path
623            };
624            
625            let should_ignore = ignore_patterns.iter().any(|pattern| {
626                if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
627                    glob_pattern.matches_path(relative_path)
628                } else {
629                    false
630                }
631            });
632            
633            if should_ignore {
634                continue;
635            }
636            
637            // Add the file to the pile
638            pile.add_path(path, object_store)?;
639            spinner.set_message(format!("Added: {}", path.display()));
640            added_count += 1;
641        }
642    }
643    
644    spinner.finish_with_message(format!("Added {} files from {}", added_count, dir_path.display()));
645    
646    Ok(added_count)
647}
648
649/// Find the repository root by looking for .pocket directory
650fn find_repository_root(path: &Path) -> Result<PathBuf> {
651    let mut current = path.to_path_buf();
652    
653    loop {
654        if current.join(".pocket").exists() {
655            return Ok(current);
656        }
657        
658        if !current.pop() {
659            return Err(anyhow!("Not a pocket repository (or any parent directory)"));
660        }
661    }
662}
663
664/// Read ignore patterns from a .pocketignore file
665fn read_ignore_patterns(path: &Path) -> Result<Vec<String>> {
666    let content = std::fs::read_to_string(path)?;
667    let patterns = content.lines()
668        .filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#'))
669        .map(|line| line.trim().to_string())
670        .collect();
671    
672    Ok(patterns)
673}
674
675/// Remove files from the pile
676pub fn unpile_command(path: &Path, files: Vec<&Path>, all: bool) -> Result<()> {
677    let mut repo = Repository::open(path)?;
678    
679    if all {
680        println!("Removing all files from the pile");
681        repo.pile.clear()?;
682    } else if !files.is_empty() {
683        for file in files {
684            println!("Removing {} from the pile", file.display());
685            repo.pile.remove_path(file)?;
686        }
687    } else {
688        return Err(anyhow!("No files specified to remove from the pile"));
689    }
690    
691    // Save the pile
692    let pile_path = repo.path.join(".pocket").join("piles").join("current.toml");
693    repo.pile.save(&pile_path)?;
694    
695    Ok(())
696}
697
698/// Create a shove (commit)
699pub fn shove_command(path: &Path, message: Option<&str>, editor: bool) -> Result<()> {
700    let mut repo = Repository::open(path)?;
701    
702    // Check if there are changes to commit
703    if repo.pile.is_empty() {
704        return Err(anyhow!("No changes to shove - pile is empty"));
705    }
706    
707    // Get commit message
708    let commit_msg = if editor {
709        // Open editor for message
710        let temp_file = std::env::temp_dir().join("pocket_shove_msg");
711        if !temp_file.exists() {
712            std::fs::write(&temp_file, "# Enter your shove message here\n")?;
713        }
714        
715        let editor_cmd = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string());
716        let status = std::process::Command::new(editor_cmd)
717            .arg(&temp_file)
718            .status()?;
719            
720        if !status.success() {
721            return Err(anyhow!("Editor exited with non-zero status"));
722        }
723        
724        let msg = std::fs::read_to_string(&temp_file)?;
725        std::fs::remove_file(&temp_file)?;
726        
727        // Remove comments and trim
728        msg.lines()
729            .filter(|line| !line.starts_with('#'))
730            .collect::<Vec<_>>()
731            .join("\n")
732            .trim()
733            .to_string()
734    } else if let Some(msg) = message {
735        msg.to_string()
736    } else {
737        return Err(anyhow!("No shove message provided. Use -m or -e to specify one"));
738    };
739    
740    if commit_msg.is_empty() {
741        return Err(anyhow!("Empty shove message"));
742    }
743    
744    // Create the commit
745    let shove_id = repo.create_shove(&commit_msg)?;
746    println!("Created shove {} with message: {}", shove_id.as_str(), commit_msg);
747    
748    // Clear the pile after successful commit
749    repo.pile.clear()?;
750    let pile_path = repo.path.join(".pocket").join("piles").join("current.toml");
751    repo.pile.save(&pile_path)?;
752    
753    Ok(())
754}
755
756/// Display the commit history with beautiful formatting
757pub fn log_command(path: &Path, verbose: bool, timeline: Option<&str>) -> Result<()> {
758    let repo = Repository::open(path)?;
759    let status = repo.status()?;
760    
761    // Get the timeline to show
762    let timeline_name = timeline.unwrap_or(&status.current_timeline);
763    
764    println!("\n{} {} {}\n", "📜".bright_cyan(), format!("Pocket VCS Log ({})", timeline_name).bold().bright_cyan(), "📜".bright_cyan());
765    
766    // Get the timeline
767    let timelines_dir = repo.path.join(".pocket").join("timelines");
768    let timeline_path = timelines_dir.join(format!("{}.toml", timeline_name));
769    
770    if !timeline_path.exists() {
771        return Err(anyhow!("Timeline {} not found", timeline_name));
772    }
773    
774    // In a real implementation, we would load the timeline and its shoves
775    // For now, we'll create a simulated history
776    let shoves = simulate_shove_history();
777    
778    // Display the shoves
779    for (i, shove) in shoves.iter().enumerate() {
780        // Shove ID and message
781        println!("{} {} {}", 
782            "📌".yellow(), 
783            shove.id[0..8].bright_yellow().bold(),
784            shove.message.lines().next().unwrap_or("").bright_white()
785        );
786        
787        // Author and date
788        println!("{}  {} {} on {}", 
789            " ".repeat(4),
790            "👤".blue(),
791            shove.author.bright_blue(),
792            shove.date.dimmed()
793        );
794        
795        // Show full message if verbose
796        if verbose && shove.message.lines().count() > 1 {
797            println!();
798            for line in shove.message.lines().skip(1) {
799                println!("{}  {}", " ".repeat(4), line);
800            }
801        }
802        
803        // Show changes
804        if verbose {
805            println!("{}  {}", " ".repeat(4), "Changes:".dimmed());
806            for change in &shove.changes {
807                let icon = match change.change_type {
808                    ChangeType::Added => "🆕".green(),
809                    ChangeType::Modified => "📝".yellow(),
810                    ChangeType::Deleted => "đŸ—‘ī¸".red(),
811                    ChangeType::Renamed => "📋".blue(),
812                };
813                println!("{}    {} {}", " ".repeat(4), icon, change.path);
814            }
815        }
816        
817        // Add graph lines between shoves
818        if i < shoves.len() - 1 {
819            println!("{}  │", " ".repeat(2));
820            println!("{}  │", " ".repeat(2));
821        }
822    }
823    
824    Ok(())
825}
826
827// Simulate a shove history for demonstration
828fn simulate_shove_history() -> Vec<SimulatedShove> {
829    vec![
830        SimulatedShove {
831            id: "abcdef1234567890".to_string(),
832            message: "Implement interactive merge resolution".to_string(),
833            author: "dev@example.com".to_string(),
834            date: "2025-03-15 14:30:45".to_string(),
835            changes: vec![
836                SimulatedChange {
837                    change_type: ChangeType::Modified,
838                    path: "src/vcs/merge.rs".to_string(),
839                },
840                SimulatedChange {
841                    change_type: ChangeType::Added,
842                    path: "src/vcs/commands.rs".to_string(),
843                },
844            ],
845        },
846        SimulatedShove {
847            id: "98765432abcdef12".to_string(),
848            message: "Add remote repository functionality\n\nImplemented push, pull, and fetch operations for remote repositories.".to_string(),
849            author: "dev@example.com".to_string(),
850            date: "2025-03-14 10:15:30".to_string(),
851            changes: vec![
852                SimulatedChange {
853                    change_type: ChangeType::Modified,
854                    path: "src/vcs/remote.rs".to_string(),
855                },
856                SimulatedChange {
857                    change_type: ChangeType::Modified,
858                    path: "src/vcs/repository.rs".to_string(),
859                },
860            ],
861        },
862        SimulatedShove {
863            id: "1234567890abcdef".to_string(),
864            message: "Initial implementation of VCS".to_string(),
865            author: "dev@example.com".to_string(),
866            date: "2025-03-10 09:00:00".to_string(),
867            changes: vec![
868                SimulatedChange {
869                    change_type: ChangeType::Added,
870                    path: "src/vcs/mod.rs".to_string(),
871                },
872                SimulatedChange {
873                    change_type: ChangeType::Added,
874                    path: "src/vcs/repository.rs".to_string(),
875                },
876                SimulatedChange {
877                    change_type: ChangeType::Added,
878                    path: "src/vcs/shove.rs".to_string(),
879                },
880                SimulatedChange {
881                    change_type: ChangeType::Added,
882                    path: "src/vcs/pile.rs".to_string(),
883                },
884                SimulatedChange {
885                    change_type: ChangeType::Added,
886                    path: "src/vcs/timeline.rs".to_string(),
887                },
888            ],
889        },
890    ]
891}
892
893// Simulated shove for demonstration
894struct SimulatedShove {
895    id: String,
896    message: String,
897    author: String,
898    date: String,
899    changes: Vec<SimulatedChange>,
900}
901
902// Simulated file change for demonstration
903struct SimulatedChange {
904    change_type: ChangeType,
905    path: String,
906}
907
908// Change type enum
909#[derive(Clone, Copy)]
910enum ChangeType {
911    Added,
912    Modified,
913    Deleted,
914    Renamed,
915}
916
917/// Create a new timeline (branch)
918pub fn timeline_new_command(path: &Path, name: &str, based_on: Option<&str>) -> Result<()> {
919    let repo = Repository::open(path)?;
920    
921    // Check if timeline already exists
922    let timeline_path = repo.path.join(".pocket").join("timelines").join(format!("{}.toml", name));
923    if timeline_path.exists() {
924        return Err(anyhow!("Timeline '{}' already exists", name));
925    }
926    
927    // Get the base shove
928    let base_shove = if let Some(base) = based_on {
929        // Use specified base
930        ShoveId::from_str(base)?
931    } else if let Some(head) = &repo.current_timeline.head {
932        // Use current head
933        head.clone()
934    } else {
935        // No base
936        return Err(anyhow!("Cannot create timeline: no base shove specified and current timeline has no head"));
937    };
938    
939    // Create the timeline
940    let timeline = Timeline::new(name, Some(base_shove));
941    
942    // Save the timeline
943    timeline.save(&timeline_path)?;
944    
945    println!("Created timeline '{}' based on shove {}", name, timeline.head.as_ref().unwrap().as_str());
946    
947    Ok(())
948}
949
950/// Switch to a timeline (branch)
951pub fn timeline_switch_command(path: &Path, name: &str) -> Result<()> {
952    let repo = Repository::open(path)?;
953    
954    // Check if timeline exists
955    let timeline_path = repo.path.join(".pocket").join("timelines").join(format!("{}.toml", name));
956    if !timeline_path.exists() {
957        return Err(anyhow!("Timeline '{}' does not exist", name));
958    }
959    
960    // Load the timeline
961    let timeline = Timeline::load(&timeline_path)?;
962    
963    // Update HEAD
964    let head_path = repo.path.join(".pocket").join("HEAD");
965    std::fs::write(head_path, format!("timeline: {}\n", name))?;
966    
967    println!("Switched to timeline '{}'", name);
968    
969    Ok(())
970}
971
972/// List timelines (branches)
973pub fn timeline_list_command(path: &Path) -> Result<()> {
974    let repo = Repository::open(path)?;
975    
976    // Get all timeline files
977    let timelines_dir = repo.path.join(".pocket").join("timelines");
978    let entries = std::fs::read_dir(timelines_dir)?;
979    
980    println!("Timelines:");
981    
982    for entry in entries {
983        let entry = entry?;
984        let file_name = entry.file_name();
985        let file_name_str = file_name.to_string_lossy();
986        
987        if file_name_str.ends_with(".toml") {
988            let timeline_name = file_name_str.trim_end_matches(".toml");
989            
990            // Mark current timeline
991            if timeline_name == repo.current_timeline.name {
992                println!("* {}", timeline_name.green());
993            } else {
994                println!("  {}", timeline_name);
995            }
996        }
997    }
998    
999    Ok(())
1000}
1001
1002/// Merge a timeline into the current timeline
1003pub fn merge_command(path: &Path, name: &str, strategy: Option<&str>) -> Result<()> {
1004    let repo = Repository::open(path)?;
1005    
1006    // Check if timeline exists
1007    let timeline_path = repo.path.join(".pocket").join("timelines").join(format!("{}.toml", name));
1008    if !timeline_path.exists() {
1009        return Err(anyhow!("Timeline '{}' does not exist", name));
1010    }
1011    
1012    // Load the timeline
1013    let other_timeline = Timeline::load(&timeline_path)?;
1014    
1015    // Determine merge strategy
1016    let merge_strategy = match strategy {
1017        Some("fast-forward-only") => MergeStrategy::FastForwardOnly,
1018        Some("always-create-shove") => MergeStrategy::AlwaysCreateShove,
1019        Some("ours") => MergeStrategy::Ours,
1020        Some("theirs") => MergeStrategy::Theirs,
1021        _ => MergeStrategy::Auto,
1022    };
1023    
1024    // Create merger
1025    let merger = crate::vcs::merge::Merger::with_strategy(&repo, merge_strategy);
1026    
1027    // Perform merge
1028    let result = merger.merge_timeline(&other_timeline)?;
1029    
1030    if result.success {
1031        if result.fast_forward {
1032            println!("Fast-forward merge successful");
1033        } else {
1034            println!("Merge successful");
1035        }
1036        
1037        if let Some(shove_id) = result.shove_id {
1038            println!("Merge shove: {}", shove_id.as_str());
1039        }
1040    } else {
1041        println!("Merge failed");
1042        
1043        if !result.conflicts.is_empty() {
1044            println!("Conflicts:");
1045            for conflict in result.conflicts {
1046                println!("  {}", conflict.path.display());
1047            }
1048            println!("\nResolve conflicts and then run 'pocket shove' to complete the merge.");
1049        }
1050    }
1051    
1052    Ok(())
1053}
1054
1055/// Add a remote repository
1056pub fn remote_add_command(path: &Path, name: &str, url: &str) -> Result<()> {
1057    let repo = Repository::open(path)?;
1058    
1059    // Create remote manager
1060    let mut remote_manager = RemoteManager::new(&repo)?;
1061    
1062    // Add remote
1063    remote_manager.add_remote(name, url)?;
1064    
1065    println!("Added remote '{}' with URL '{}'", name, url);
1066    
1067    Ok(())
1068}
1069
1070/// Remove a remote repository
1071pub fn remote_remove_command(path: &Path, name: &str) -> Result<()> {
1072    let repo = Repository::open(path)?;
1073    
1074    // Create remote manager
1075    let mut remote_manager = RemoteManager::new(&repo)?;
1076    
1077    // Remove remote
1078    remote_manager.remove_remote(name)?;
1079    
1080    println!("Removed remote '{}'", name);
1081    
1082    Ok(())
1083}
1084
1085/// List remote repositories
1086pub fn remote_list_command(path: &Path) -> Result<()> {
1087    let repo = Repository::open(path)?;
1088    
1089    // Create remote manager
1090    let remote_manager = RemoteManager::new(&repo)?;
1091    
1092    println!("Remotes:");
1093    
1094    for (name, remote) in &remote_manager.remotes {
1095        println!("  {}: {}", name, remote.url);
1096    }
1097    
1098    Ok(())
1099}
1100
1101/// Fetch from a remote repository
1102pub fn fish_command(path: &Path, remote: Option<&str>) -> Result<()> {
1103    let repo = Repository::open(path)?;
1104    
1105    // Create remote manager
1106    let remote_manager = RemoteManager::new(&repo)?;
1107    
1108    // Determine remote to fetch from
1109    let remote_name = if let Some(r) = remote {
1110        r
1111    } else if let Some(default) = &repo.config.remote.default_remote {
1112        default
1113    } else {
1114        return Err(anyhow!("No remote specified and no default remote configured"));
1115    };
1116    
1117    // Fetch from remote
1118    remote_manager.fetch(remote_name)?;
1119    
1120    println!("Fetched from remote '{}'", remote_name);
1121    
1122    Ok(())
1123}
1124
1125/// Push to a remote repository
1126pub fn push_command(path: &Path, remote: Option<&str>, timeline: Option<&str>) -> Result<()> {
1127    let repo = Repository::open(path)?;
1128    
1129    // Create remote manager
1130    let remote_manager = RemoteManager::new(&repo)?;
1131    
1132    // Determine remote to push to
1133    let remote_name = if let Some(r) = remote {
1134        r
1135    } else if let Some(default) = &repo.config.remote.default_remote {
1136        default
1137    } else {
1138        return Err(anyhow!("No remote specified and no default remote configured"));
1139    };
1140    
1141    // Determine timeline to push
1142    let timeline_name = timeline.unwrap_or(&repo.current_timeline.name);
1143    
1144    // Push to remote
1145    remote_manager.push(remote_name, timeline_name)?;
1146    
1147    println!("Pushed timeline '{}' to remote '{}'", timeline_name, remote_name);
1148    
1149    Ok(())
1150}
1151
1152/// Manage ignore patterns
1153pub fn ignore_command(path: &Path, add: Option<&str>, remove: Option<&str>, list: bool) -> Result<()> {
1154    let repo = Repository::open(path)?;
1155    let mut config = repo.config.clone();
1156    let ignore_path = repo.path.join(".pocketignore");
1157    
1158    // Read existing patterns from .pocketignore file if it exists
1159    let mut patterns = if ignore_path.exists() {
1160        let content = std::fs::read_to_string(&ignore_path)?;
1161        content.lines()
1162            .filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#'))
1163            .map(|line| line.trim().to_string())
1164            .collect::<Vec<String>>()
1165    } else {
1166        config.core.ignore_patterns.clone()
1167    };
1168    
1169    if let Some(pattern) = add {
1170        // Add new pattern if it doesn't already exist
1171        if !patterns.contains(&pattern.to_string()) {
1172            patterns.push(pattern.to_string());
1173            println!("{} Added ignore pattern: {}", "✅".green(), pattern);
1174        } else {
1175            println!("{} Pattern already exists: {}", "â„šī¸".blue(), pattern);
1176        }
1177    }
1178    
1179    if let Some(pattern) = remove {
1180        // Remove pattern if it exists
1181        if let Some(pos) = patterns.iter().position(|p| p == pattern) {
1182            patterns.remove(pos);
1183            println!("{} Removed ignore pattern: {}", "✅".green(), pattern);
1184        } else {
1185            println!("{} Pattern not found: {}", "âš ī¸".yellow(), pattern);
1186        }
1187    }
1188    
1189    if list {
1190        // List all patterns
1191        println!("\n{} Ignore patterns:", "📋".bright_cyan());
1192        if patterns.is_empty() {
1193            println!("  No ignore patterns defined");
1194        } else {
1195            for pattern in &patterns {
1196                println!("  - {}", pattern);
1197            }
1198        }
1199    }
1200    
1201    // Update config and save to .pocketignore file
1202    config.core.ignore_patterns = patterns.clone();
1203    
1204    // Save patterns to .pocketignore file
1205    let mut content = "# Pocket ignore file\n".to_string();
1206    for pattern in &patterns {
1207        content.push_str(&format!("{}\n", pattern));
1208    }
1209    std::fs::write(&ignore_path, content)?;
1210    
1211    // Update repository config
1212    let config_path = repo.path.join(".pocket").join("config.toml");
1213    let config_str = toml::to_string_pretty(&config)?;
1214    std::fs::write(config_path, config_str)?;
1215    
1216    Ok(())
1217}