1use 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
18pub 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 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
43pub fn status_command(path: &Path, verbose: bool) -> Result<()> {
45 let repo = Repository::open(path)?;
46 let status = repo.status()?;
47
48 println!("\n{} {} {}\n", "đ".bright_cyan(), "Pocket VCS Status".bold().bright_cyan(), "đ".bright_cyan());
50
51 println!("{} {}: {}", "đŋ".green(), "Current Timeline".bold(), status.current_timeline.bright_green());
53
54 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 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 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 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 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 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 println!("\n{} {}", "đĄ".yellow(), "Tip: Use 'pocket help' to see available commands".italic());
125
126 Ok(())
127}
128
129pub fn interactive_pile_command(path: &Path, files: Vec<String>, all: bool, pattern: Option<String>) -> Result<()> {
131 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 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 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 files_to_choose.sort_by(|a, b| a.0.cmp(&b.0));
161
162 let items: Vec<String> = files_to_choose.iter()
164 .map(|(path, status, icon)| format!("{} {} ({})", icon, path.display(), status))
165 .collect();
166
167 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 let mut piled_files = Vec::new();
177
178 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 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 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 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)); }
212
213 pb.finish_with_message(format!("â
All {} files piled successfully", files_to_choose.len()));
214 break;
215 } else {
216 let (file, _, _) = &files_to_choose[selection - 1]; if !piled_files.contains(file) {
220 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
239pub 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 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 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 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 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 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 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 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
331pub 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 let status = repo.status()?;
339 println!("{} {}: {}", "đŋ".green(), "Current Timeline".bold(), status.current_timeline.bright_green());
340
341 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 timelines.sort();
361
362 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 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 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 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 std::thread::sleep(std::time::Duration::from_millis(500));
418
419 println!("{} Switched to timeline {}", "â
".green(), name.bright_green());
420 }
421 },
422 1 => {
423 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 std::thread::sleep(std::time::Duration::from_millis(500));
444
445 println!("{} Switched to timeline {}", "â
".green(), selected_timeline.bright_green());
446 },
447 _ => {
448 println!("\n{} Returning to main menu", "đ".blue());
450 }
451 }
452
453 Ok(())
454}
455
456pub 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 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 let should_ignore = |file_path: &Path| -> bool {
472 if file_path.to_string_lossy().contains("/.pocket/") ||
474 file_path.to_string_lossy().contains("/.git/") {
475 return true;
476 }
477
478 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 {
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 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 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 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 added_count += add_directory_recursively(file_path, &mut pile, &repo.object_store, &repo.path, &ignore_patterns)?;
544 } else {
545 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 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 let pile_path = repo.path.join(".pocket").join("piles").join("current.toml");
575 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
589fn 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 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 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 if path.to_string_lossy().contains("/.pocket/") ||
614 path.to_string_lossy().contains("/.git/") {
615 continue;
616 }
617
618 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 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
649fn 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
664fn 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
675pub 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 let pile_path = repo.path.join(".pocket").join("piles").join("current.toml");
693 repo.pile.save(&pile_path)?;
694
695 Ok(())
696}
697
698pub fn shove_command(path: &Path, message: Option<&str>, editor: bool) -> Result<()> {
700 let mut repo = Repository::open(path)?;
701
702 if repo.pile.is_empty() {
704 return Err(anyhow!("No changes to shove - pile is empty"));
705 }
706
707 let commit_msg = if editor {
709 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 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 let shove_id = repo.create_shove(&commit_msg)?;
746 println!("Created shove {} with message: {}", shove_id.as_str(), commit_msg);
747
748 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
756pub fn log_command(path: &Path, verbose: bool, timeline: Option<&str>) -> Result<()> {
758 let repo = Repository::open(path)?;
759 let status = repo.status()?;
760
761 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 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 let shoves = simulate_shove_history();
777
778 for (i, shove) in shoves.iter().enumerate() {
780 println!("{} {} {}",
782 "đ".yellow(),
783 shove.id[0..8].bright_yellow().bold(),
784 shove.message.lines().next().unwrap_or("").bright_white()
785 );
786
787 println!("{} {} {} on {}",
789 " ".repeat(4),
790 "đ¤".blue(),
791 shove.author.bright_blue(),
792 shove.date.dimmed()
793 );
794
795 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 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 if i < shoves.len() - 1 {
819 println!("{} â", " ".repeat(2));
820 println!("{} â", " ".repeat(2));
821 }
822 }
823
824 Ok(())
825}
826
827fn 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
893struct SimulatedShove {
895 id: String,
896 message: String,
897 author: String,
898 date: String,
899 changes: Vec<SimulatedChange>,
900}
901
902struct SimulatedChange {
904 change_type: ChangeType,
905 path: String,
906}
907
908#[derive(Clone, Copy)]
910enum ChangeType {
911 Added,
912 Modified,
913 Deleted,
914 Renamed,
915}
916
917pub fn timeline_new_command(path: &Path, name: &str, based_on: Option<&str>) -> Result<()> {
919 let repo = Repository::open(path)?;
920
921 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 let base_shove = if let Some(base) = based_on {
929 ShoveId::from_str(base)?
931 } else if let Some(head) = &repo.current_timeline.head {
932 head.clone()
934 } else {
935 return Err(anyhow!("Cannot create timeline: no base shove specified and current timeline has no head"));
937 };
938
939 let timeline = Timeline::new(name, Some(base_shove));
941
942 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
950pub fn timeline_switch_command(path: &Path, name: &str) -> Result<()> {
952 let repo = Repository::open(path)?;
953
954 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 let timeline = Timeline::load(&timeline_path)?;
962
963 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
972pub fn timeline_list_command(path: &Path) -> Result<()> {
974 let repo = Repository::open(path)?;
975
976 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 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
1002pub fn merge_command(path: &Path, name: &str, strategy: Option<&str>) -> Result<()> {
1004 let repo = Repository::open(path)?;
1005
1006 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 let other_timeline = Timeline::load(&timeline_path)?;
1014
1015 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 let merger = crate::vcs::merge::Merger::with_strategy(&repo, merge_strategy);
1026
1027 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
1055pub fn remote_add_command(path: &Path, name: &str, url: &str) -> Result<()> {
1057 let repo = Repository::open(path)?;
1058
1059 let mut remote_manager = RemoteManager::new(&repo)?;
1061
1062 remote_manager.add_remote(name, url)?;
1064
1065 println!("Added remote '{}' with URL '{}'", name, url);
1066
1067 Ok(())
1068}
1069
1070pub fn remote_remove_command(path: &Path, name: &str) -> Result<()> {
1072 let repo = Repository::open(path)?;
1073
1074 let mut remote_manager = RemoteManager::new(&repo)?;
1076
1077 remote_manager.remove_remote(name)?;
1079
1080 println!("Removed remote '{}'", name);
1081
1082 Ok(())
1083}
1084
1085pub fn remote_list_command(path: &Path) -> Result<()> {
1087 let repo = Repository::open(path)?;
1088
1089 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
1101pub fn fish_command(path: &Path, remote: Option<&str>) -> Result<()> {
1103 let repo = Repository::open(path)?;
1104
1105 let remote_manager = RemoteManager::new(&repo)?;
1107
1108 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 remote_manager.fetch(remote_name)?;
1119
1120 println!("Fetched from remote '{}'", remote_name);
1121
1122 Ok(())
1123}
1124
1125pub fn push_command(path: &Path, remote: Option<&str>, timeline: Option<&str>) -> Result<()> {
1127 let repo = Repository::open(path)?;
1128
1129 let remote_manager = RemoteManager::new(&repo)?;
1131
1132 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 let timeline_name = timeline.unwrap_or(&repo.current_timeline.name);
1143
1144 remote_manager.push(remote_name, timeline_name)?;
1146
1147 println!("Pushed timeline '{}' to remote '{}'", timeline_name, remote_name);
1148
1149 Ok(())
1150}
1151
1152pub 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 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 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 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 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 config.core.ignore_patterns = patterns.clone();
1203
1204 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 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}