1use std::path::Path;
4
5use console::style;
6
7use crate::console as cwconsole;
8use crate::constants::{
9 format_config_key, path_age_days, sanitize_branch_name, CONFIG_KEY_BASE_BRANCH,
10 CONFIG_KEY_BASE_PATH, CONFIG_KEY_INTENDED_BRANCH,
11};
12use crate::error::Result;
13use crate::git;
14
15const MIN_TABLE_WIDTH: usize = 100;
17
18pub fn get_worktree_status(path: &Path, repo: &Path, branch: Option<&str>) -> String {
29 if !path.exists() {
30 return "stale".to_string();
31 }
32
33 if let Ok(cwd) = std::env::current_dir() {
35 let cwd_str = cwd.to_string_lossy().to_string();
36 let path_str = path.to_string_lossy().to_string();
37 if cwd_str.starts_with(&path_str) {
38 return "active".to_string();
39 }
40 }
41
42 if let Some(branch_name) = branch {
44 let base_branch = {
45 let key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch_name);
46 git::get_config(&key, Some(repo))
47 .unwrap_or_else(|| git::detect_default_branch(Some(repo)))
48 };
49
50 if let Some(pr_state) = git::get_pr_state(branch_name, Some(repo)) {
53 match pr_state.as_str() {
54 "MERGED" => return "merged".to_string(),
55 "OPEN" => return "pr-open".to_string(),
56 _ => {} }
58 }
59
60 if git::is_branch_merged(branch_name, &base_branch, Some(repo)) {
63 return "merged".to_string();
64 }
65 }
66
67 if let Ok(result) = git::git_command(&["status", "--porcelain"], Some(path), false, true) {
69 if result.returncode == 0 && !result.stdout.trim().is_empty() {
70 return "modified".to_string();
71 }
72 }
73
74 "clean".to_string()
75}
76
77pub fn format_age(age_days: f64) -> String {
79 if age_days < 1.0 {
80 let hours = (age_days * 24.0) as i64;
81 if hours > 0 {
82 format!("{}h ago", hours)
83 } else {
84 "just now".to_string()
85 }
86 } else if age_days < 7.0 {
87 format!("{}d ago", age_days as i64)
88 } else if age_days < 30.0 {
89 format!("{}w ago", (age_days / 7.0) as i64)
90 } else if age_days < 365.0 {
91 format!("{}mo ago", (age_days / 30.0) as i64)
92 } else {
93 format!("{}y ago", (age_days / 365.0) as i64)
94 }
95}
96
97fn path_age_str(path: &Path) -> String {
99 if !path.exists() {
100 return String::new();
101 }
102 path_age_days(path).map(format_age).unwrap_or_default()
103}
104
105struct WorktreeRow {
107 worktree_id: String,
108 current_branch: String,
109 status: String,
110 age: String,
111 rel_path: String,
112}
113
114pub fn list_worktrees() -> Result<()> {
116 let repo = git::get_repo_root(None)?;
117 let worktrees = git::parse_worktrees(&repo)?;
118
119 println!(
120 "\n{} {}\n",
121 style("Worktrees for repository:").cyan().bold(),
122 repo.display()
123 );
124
125 let mut rows: Vec<WorktreeRow> = Vec::new();
126
127 for (branch, path) in &worktrees {
128 let current_branch = git::normalize_branch_name(branch).to_string();
129 let status = get_worktree_status(path, &repo, Some(¤t_branch));
130 let rel_path = pathdiff::diff_paths(path, &repo)
131 .map(|p: std::path::PathBuf| p.to_string_lossy().to_string())
132 .unwrap_or_else(|| path.to_string_lossy().to_string());
133 let age = path_age_str(path);
134
135 let intended_branch = lookup_intended_branch(&repo, ¤t_branch, path);
137 let worktree_id = intended_branch.unwrap_or_else(|| current_branch.clone());
138
139 rows.push(WorktreeRow {
140 worktree_id,
141 current_branch,
142 status,
143 age,
144 rel_path,
145 });
146 }
147
148 let term_width = cwconsole::terminal_width();
149 if term_width >= MIN_TABLE_WIDTH {
150 print_worktree_table(&rows);
151 } else {
152 print_worktree_compact(&rows);
153 }
154
155 let feature_count = if rows.len() > 1 { rows.len() - 1 } else { 0 };
157 if feature_count > 0 {
158 let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
159 for row in &rows {
160 *counts.entry(row.status.as_str()).or_insert(0) += 1;
161 }
162
163 let mut summary_parts = Vec::new();
164 for &status_name in &["clean", "modified", "active", "pr-open", "merged", "stale"] {
165 if let Some(&count) = counts.get(status_name) {
166 if count > 0 {
167 let styled = cwconsole::status_style(status_name)
168 .apply_to(format!("{} {}", count, status_name));
169 summary_parts.push(styled.to_string());
170 }
171 }
172 }
173
174 let summary = if summary_parts.is_empty() {
175 format!("\n{} feature worktree(s)", feature_count)
176 } else {
177 format!(
178 "\n{} feature worktree(s) — {}",
179 feature_count,
180 summary_parts.join(", ")
181 )
182 };
183 println!("{}", summary);
184 }
185
186 println!();
187 Ok(())
188}
189
190fn lookup_intended_branch(repo: &Path, current_branch: &str, path: &Path) -> Option<String> {
192 let key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, current_branch);
194 if let Some(intended) = git::get_config(&key, Some(repo)) {
195 return Some(intended);
196 }
197
198 let result = git::git_command(
200 &[
201 "config",
202 "--local",
203 "--get-regexp",
204 r"^worktree\..*\.intendedBranch",
205 ],
206 Some(repo),
207 false,
208 true,
209 )
210 .ok()?;
211
212 if result.returncode != 0 {
213 return None;
214 }
215
216 let repo_name = repo.file_name()?.to_string_lossy().to_string();
217
218 for line in result.stdout.trim().lines() {
219 let parts: Vec<&str> = line.splitn(2, char::is_whitespace).collect();
220 if parts.len() == 2 {
221 let key_parts: Vec<&str> = parts[0].split('.').collect();
222 if key_parts.len() >= 2 {
223 let branch_from_key = key_parts[1];
224 let expected_path_name =
225 format!("{}-{}", repo_name, sanitize_branch_name(branch_from_key));
226 if let Some(name) = path.file_name() {
227 if name.to_string_lossy() == expected_path_name {
228 return Some(parts[1].to_string());
229 }
230 }
231 }
232 }
233 }
234
235 None
236}
237
238fn print_worktree_table(rows: &[WorktreeRow]) {
239 let max_wt = rows.iter().map(|r| r.worktree_id.len()).max().unwrap_or(20);
240 let max_br = rows
241 .iter()
242 .map(|r| r.current_branch.len())
243 .max()
244 .unwrap_or(20);
245 let wt_col = max_wt.clamp(12, 35) + 2;
246 let br_col = max_br.clamp(12, 35) + 2;
247
248 println!(
249 " {} {:<wt_col$} {:<br_col$} {:<10} {:<12} {}",
250 style(" ").dim(),
251 style("WORKTREE").dim(),
252 style("BRANCH").dim(),
253 style("STATUS").dim(),
254 style("AGE").dim(),
255 style("PATH").dim(),
256 wt_col = wt_col,
257 br_col = br_col,
258 );
259 let line_width = (wt_col + br_col + 40).min(cwconsole::terminal_width().saturating_sub(4));
260 println!(" {}", style("─".repeat(line_width)).dim());
261
262 for row in rows {
263 let icon = cwconsole::status_icon(&row.status);
264 let st = cwconsole::status_style(&row.status);
265
266 let branch_display = if row.worktree_id != row.current_branch {
267 style(format!("{} ⚠", row.current_branch))
268 .yellow()
269 .to_string()
270 } else {
271 row.current_branch.clone()
272 };
273
274 let status_styled = st.apply_to(format!("{:<10}", row.status));
275
276 println!(
277 " {} {:<wt_col$} {:<br_col$} {} {:<12} {}",
278 st.apply_to(icon),
279 style(&row.worktree_id).bold(),
280 branch_display,
281 status_styled,
282 style(&row.age).dim(),
283 style(&row.rel_path).dim(),
284 wt_col = wt_col,
285 br_col = br_col,
286 );
287 }
288}
289
290fn print_worktree_compact(rows: &[WorktreeRow]) {
291 for row in rows {
292 let icon = cwconsole::status_icon(&row.status);
293 let st = cwconsole::status_style(&row.status);
294 let age_part = if row.age.is_empty() {
295 String::new()
296 } else {
297 format!(" {}", style(&row.age).dim())
298 };
299
300 println!(
301 " {} {} {}{}",
302 st.apply_to(icon),
303 style(&row.worktree_id).bold(),
304 st.apply_to(&row.status),
305 age_part,
306 );
307
308 let mut details = Vec::new();
309 if row.worktree_id != row.current_branch {
310 details.push(format!(
311 "branch: {}",
312 style(format!("{} ⚠", row.current_branch)).yellow()
313 ));
314 }
315 if !row.rel_path.is_empty() {
316 details.push(format!("{}", style(&row.rel_path).dim()));
317 }
318 if !details.is_empty() {
319 println!(" {}", details.join(" "));
320 }
321 }
322}
323
324pub fn show_status() -> Result<()> {
326 let repo = git::get_repo_root(None)?;
327
328 match git::get_current_branch(Some(&std::env::current_dir().unwrap_or_default())) {
329 Ok(branch) => {
330 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, &branch);
331 let path_key = format_config_key(CONFIG_KEY_BASE_PATH, &branch);
332 let base = git::get_config(&base_key, Some(&repo));
333 let base_path = git::get_config(&path_key, Some(&repo));
334
335 println!("\n{}", style("Current worktree:").cyan().bold());
336 println!(" Feature: {}", style(&branch).green());
337 println!(
338 " Base: {}",
339 style(base.as_deref().unwrap_or("N/A")).green()
340 );
341 println!(
342 " Base path: {}\n",
343 style(base_path.as_deref().unwrap_or("N/A")).blue()
344 );
345 }
346 Err(_) => {
347 println!(
348 "\n{}\n",
349 style("Current directory is not a feature worktree or is the main repository.")
350 .yellow()
351 );
352 }
353 }
354
355 list_worktrees()
356}
357
358pub fn show_tree() -> Result<()> {
360 let repo = git::get_repo_root(None)?;
361 let cwd = std::env::current_dir().unwrap_or_default();
362
363 let repo_name = repo
364 .file_name()
365 .map(|n| n.to_string_lossy().to_string())
366 .unwrap_or_else(|| "repo".to_string());
367
368 println!(
369 "\n{} (base repository)",
370 style(format!("{}/", repo_name)).cyan().bold()
371 );
372 println!("{}\n", style(repo.display().to_string()).dim());
373
374 let feature_worktrees = git::get_feature_worktrees(Some(&repo))?;
375
376 if feature_worktrees.is_empty() {
377 println!("{}\n", style(" (no feature worktrees)").dim());
378 return Ok(());
379 }
380
381 let mut sorted = feature_worktrees;
382 sorted.sort_by(|a, b| a.0.cmp(&b.0));
383
384 for (i, (branch_name, path)) in sorted.iter().enumerate() {
385 let is_last = i == sorted.len() - 1;
386 let prefix = if is_last { "└── " } else { "├── " };
387
388 let status = get_worktree_status(path, &repo, Some(branch_name.as_str()));
389 let is_current = cwd
390 .to_string_lossy()
391 .starts_with(&path.to_string_lossy().to_string());
392
393 let icon = cwconsole::status_icon(&status);
394 let st = cwconsole::status_style(&status);
395
396 let branch_display = if is_current {
397 st.clone()
398 .bold()
399 .apply_to(format!("★ {}", branch_name))
400 .to_string()
401 } else {
402 st.clone().apply_to(branch_name.as_str()).to_string()
403 };
404
405 let age = path_age_str(path);
406 let age_display = if age.is_empty() {
407 String::new()
408 } else {
409 format!(" {}", style(age).dim())
410 };
411
412 println!(
413 "{}{} {}{}",
414 prefix,
415 st.apply_to(icon),
416 branch_display,
417 age_display
418 );
419
420 let path_display = if let Ok(rel) = path.strip_prefix(repo.parent().unwrap_or(&repo)) {
421 format!("../{}", rel.display())
422 } else {
423 path.display().to_string()
424 };
425
426 let continuation = if is_last { " " } else { "│ " };
427 println!("{}{}", continuation, style(&path_display).dim());
428 }
429
430 println!("\n{}", style("Legend:").bold());
432 println!(
433 " {} active (current)",
434 cwconsole::status_style("active").apply_to("●")
435 );
436 println!(" {} clean", cwconsole::status_style("clean").apply_to("○"));
437 println!(
438 " {} modified",
439 cwconsole::status_style("modified").apply_to("◉")
440 );
441 println!(
442 " {} pr-open",
443 cwconsole::status_style("pr-open").apply_to("⬆")
444 );
445 println!(
446 " {} merged",
447 cwconsole::status_style("merged").apply_to("✓")
448 );
449 println!(" {} stale", cwconsole::status_style("stale").apply_to("x"));
450 println!(
451 " {} currently active worktree\n",
452 style("★").green().bold()
453 );
454
455 Ok(())
456}
457
458pub fn show_stats() -> Result<()> {
460 let repo = git::get_repo_root(None)?;
461 let feature_worktrees = git::get_feature_worktrees(Some(&repo))?;
462
463 if feature_worktrees.is_empty() {
464 println!("\n{}\n", style("No feature worktrees found").yellow());
465 return Ok(());
466 }
467
468 println!();
469 println!(" {}", style("Worktree Statistics").cyan().bold());
470 println!(" {}", style("─".repeat(40)).dim());
471 println!();
472
473 struct WtData {
474 branch: String,
475 status: String,
476 age_days: f64,
477 commit_count: usize,
478 }
479
480 let mut data: Vec<WtData> = Vec::new();
481
482 for (branch_name, path) in &feature_worktrees {
483 let status = get_worktree_status(path, &repo, Some(branch_name.as_str()));
484 let age_days = path_age_days(path).unwrap_or(0.0);
485
486 let commit_count = git::git_command(
487 &["rev-list", "--count", branch_name],
488 Some(path),
489 false,
490 true,
491 )
492 .ok()
493 .and_then(|r| {
494 if r.returncode == 0 {
495 r.stdout.trim().parse::<usize>().ok()
496 } else {
497 None
498 }
499 })
500 .unwrap_or(0);
501
502 data.push(WtData {
503 branch: branch_name.clone(),
504 status,
505 age_days,
506 commit_count,
507 });
508 }
509
510 let mut status_counts: std::collections::HashMap<&str, usize> =
512 std::collections::HashMap::new();
513 for d in &data {
514 *status_counts.entry(d.status.as_str()).or_insert(0) += 1;
515 }
516
517 println!(" {} {}", style("Total:").bold(), data.len());
518
519 let total = data.len();
521 let bar_width = 30;
522 let clean = *status_counts.get("clean").unwrap_or(&0);
523 let modified = *status_counts.get("modified").unwrap_or(&0);
524 let active = *status_counts.get("active").unwrap_or(&0);
525 let pr_open = *status_counts.get("pr-open").unwrap_or(&0);
526 let merged = *status_counts.get("merged").unwrap_or(&0);
527 let stale = *status_counts.get("stale").unwrap_or(&0);
528
529 let bar_clean = (clean * bar_width) / total.max(1);
530 let bar_modified = (modified * bar_width) / total.max(1);
531 let bar_active = (active * bar_width) / total.max(1);
532 let bar_pr_open = (pr_open * bar_width) / total.max(1);
533 let bar_merged = (merged * bar_width) / total.max(1);
534 let bar_stale = (stale * bar_width) / total.max(1);
535 let bar_remainder =
537 bar_width - bar_clean - bar_modified - bar_active - bar_pr_open - bar_merged - bar_stale;
538
539 print!(" ");
540 print!("{}", style("█".repeat(bar_clean + bar_remainder)).green());
541 print!("{}", style("█".repeat(bar_modified)).yellow());
542 print!("{}", style("█".repeat(bar_active)).green().bold());
543 print!("{}", style("█".repeat(bar_pr_open)).cyan());
544 print!("{}", style("█".repeat(bar_merged)).magenta());
545 print!("{}", style("█".repeat(bar_stale)).red());
546 println!();
547
548 let mut parts = Vec::new();
549 if clean > 0 {
550 parts.push(format!("{}", style(format!("○ {} clean", clean)).green()));
551 }
552 if modified > 0 {
553 parts.push(format!(
554 "{}",
555 style(format!("◉ {} modified", modified)).yellow()
556 ));
557 }
558 if active > 0 {
559 parts.push(format!(
560 "{}",
561 style(format!("● {} active", active)).green().bold()
562 ));
563 }
564 if pr_open > 0 {
565 parts.push(format!(
566 "{}",
567 style(format!("⬆ {} pr-open", pr_open)).cyan()
568 ));
569 }
570 if merged > 0 {
571 parts.push(format!(
572 "{}",
573 style(format!("✓ {} merged", merged)).magenta()
574 ));
575 }
576 if stale > 0 {
577 parts.push(format!("{}", style(format!("x {} stale", stale)).red()));
578 }
579 println!(" {}", parts.join(" "));
580 println!();
581
582 let ages: Vec<f64> = data
584 .iter()
585 .filter(|d| d.age_days > 0.0)
586 .map(|d| d.age_days)
587 .collect();
588 if !ages.is_empty() {
589 let avg = ages.iter().sum::<f64>() / ages.len() as f64;
590 let oldest = ages.iter().cloned().fold(0.0_f64, f64::max);
591 let newest = ages.iter().cloned().fold(f64::MAX, f64::min);
592
593 println!(" {} Age", style("◷").dim());
594 println!(
595 " avg {} oldest {} newest {}",
596 style(format!("{:.1}d", avg)).bold(),
597 style(format!("{:.1}d", oldest)).yellow(),
598 style(format!("{:.1}d", newest)).green(),
599 );
600 println!();
601 }
602
603 let commits: Vec<usize> = data
605 .iter()
606 .filter(|d| d.commit_count > 0)
607 .map(|d| d.commit_count)
608 .collect();
609 if !commits.is_empty() {
610 let total: usize = commits.iter().sum();
611 let avg = total as f64 / commits.len() as f64;
612 let max_c = *commits.iter().max().unwrap_or(&0);
613
614 println!(" {} Commits", style("⟲").dim());
615 println!(
616 " total {} avg {:.1} max {}",
617 style(total).bold(),
618 avg,
619 style(max_c).bold(),
620 );
621 println!();
622 }
623
624 println!(" {}", style("Oldest Worktrees").bold());
626 let mut by_age = data.iter().collect::<Vec<_>>();
627 by_age.sort_by(|a, b| b.age_days.total_cmp(&a.age_days));
628 let max_age = by_age.first().map(|d| d.age_days).unwrap_or(1.0).max(1.0);
629 for d in by_age.iter().take(5) {
630 if d.age_days > 0.0 {
631 let icon = cwconsole::status_icon(&d.status);
632 let st = cwconsole::status_style(&d.status);
633 let bar_len = ((d.age_days / max_age) * 15.0) as usize;
634 println!(
635 " {} {:<25} {} {}",
636 st.apply_to(icon),
637 d.branch,
638 style("▓".repeat(bar_len.max(1))).dim(),
639 style(format_age(d.age_days)).dim(),
640 );
641 }
642 }
643 println!();
644
645 println!(" {}", style("Most Active (by commits)").bold());
647 let mut by_commits = data.iter().collect::<Vec<_>>();
648 by_commits.sort_by(|a, b| b.commit_count.cmp(&a.commit_count));
649 let max_commits = by_commits
650 .first()
651 .map(|d| d.commit_count)
652 .unwrap_or(1)
653 .max(1);
654 for d in by_commits.iter().take(5) {
655 if d.commit_count > 0 {
656 let icon = cwconsole::status_icon(&d.status);
657 let st = cwconsole::status_style(&d.status);
658 let bar_len = (d.commit_count * 15) / max_commits;
659 println!(
660 " {} {:<25} {} {}",
661 st.apply_to(icon),
662 d.branch,
663 style("▓".repeat(bar_len.max(1))).cyan(),
664 style(format!("{} commits", d.commit_count)).dim(),
665 );
666 }
667 }
668 println!();
669
670 Ok(())
671}
672
673pub fn diff_worktrees(branch1: &str, branch2: &str, summary: bool, files: bool) -> Result<()> {
675 let repo = git::get_repo_root(None)?;
676
677 if !git::branch_exists(branch1, Some(&repo)) {
678 return Err(crate::error::CwError::InvalidBranch(format!(
679 "Branch '{}' not found",
680 branch1
681 )));
682 }
683 if !git::branch_exists(branch2, Some(&repo)) {
684 return Err(crate::error::CwError::InvalidBranch(format!(
685 "Branch '{}' not found",
686 branch2
687 )));
688 }
689
690 println!("\n{}", style("Comparing branches:").cyan().bold());
691 println!(" {} {} {}\n", branch1, style("...").yellow(), branch2);
692
693 if files {
694 let result = git::git_command(
695 &["diff", "--name-status", branch1, branch2],
696 Some(&repo),
697 true,
698 true,
699 )?;
700 println!("{}\n", style("Changed files:").bold());
701 if result.stdout.trim().is_empty() {
702 println!(" {}", style("No differences found").dim());
703 } else {
704 for line in result.stdout.trim().lines() {
705 let parts: Vec<&str> = line.splitn(2, char::is_whitespace).collect();
706 if parts.len() == 2 {
707 let (status_char, filename) = (parts[0], parts[1]);
708 let c = status_char.chars().next().unwrap_or('?');
709 let status_name = match c {
710 'M' => "Modified",
711 'A' => "Added",
712 'D' => "Deleted",
713 'R' => "Renamed",
714 'C' => "Copied",
715 _ => "Changed",
716 };
717 let styled_status = match c {
718 'M' => style(status_char).yellow(),
719 'A' => style(status_char).green(),
720 'D' => style(status_char).red(),
721 'R' | 'C' => style(status_char).cyan(),
722 _ => style(status_char),
723 };
724 println!(" {} {} ({})", styled_status, filename, status_name);
725 }
726 }
727 }
728 } else if summary {
729 let result = git::git_command(
730 &["diff", "--stat", branch1, branch2],
731 Some(&repo),
732 true,
733 true,
734 )?;
735 println!("{}\n", style("Diff summary:").bold());
736 if result.stdout.trim().is_empty() {
737 println!(" {}", style("No differences found").dim());
738 } else {
739 println!("{}", result.stdout);
740 }
741 } else {
742 let result = git::git_command(&["diff", branch1, branch2], Some(&repo), true, true)?;
743 if result.stdout.trim().is_empty() {
744 println!("{}\n", style("No differences found").dim());
745 } else {
746 println!("{}", result.stdout);
747 }
748 }
749
750 Ok(())
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn test_format_age_just_now() {
759 assert_eq!(format_age(0.0), "just now");
760 assert_eq!(format_age(0.001), "just now"); }
762
763 #[test]
764 fn test_format_age_hours() {
765 assert_eq!(format_age(1.0 / 24.0), "1h ago"); assert_eq!(format_age(0.5), "12h ago"); assert_eq!(format_age(0.99), "23h ago"); }
769
770 #[test]
771 fn test_format_age_days() {
772 assert_eq!(format_age(1.0), "1d ago");
773 assert_eq!(format_age(1.5), "1d ago");
774 assert_eq!(format_age(6.9), "6d ago");
775 }
776
777 #[test]
778 fn test_format_age_weeks() {
779 assert_eq!(format_age(7.0), "1w ago");
780 assert_eq!(format_age(14.0), "2w ago");
781 assert_eq!(format_age(29.0), "4w ago");
782 }
783
784 #[test]
785 fn test_format_age_months() {
786 assert_eq!(format_age(30.0), "1mo ago");
787 assert_eq!(format_age(60.0), "2mo ago");
788 assert_eq!(format_age(364.0), "12mo ago");
789 }
790
791 #[test]
792 fn test_format_age_years() {
793 assert_eq!(format_age(365.0), "1y ago");
794 assert_eq!(format_age(730.0), "2y ago");
795 }
796
797 #[test]
798 fn test_format_age_boundary_below_one_hour() {
799 assert_eq!(format_age(0.04), "just now"); }
802
803 #[test]
804 fn test_get_worktree_status_stale() {
805 use std::path::PathBuf;
806 let non_existent = PathBuf::from("/tmp/gw-test-nonexistent-12345");
807 let repo = PathBuf::from("/tmp");
808 assert_eq!(get_worktree_status(&non_existent, &repo, None), "stale");
809 }
810}