git_worktree_manager/operations/
busy_messages.rs1use crate::operations::busy::{BusyInfo, BusySource};
6
7fn fmt_age(secs: u64) -> String {
8 if secs < 60 {
9 format!("{}s ago", secs)
10 } else if secs < 3600 {
11 format!(
12 "{} minute{} ago",
13 secs / 60,
14 if secs / 60 == 1 { "" } else { "s" }
15 )
16 } else {
17 format!(
18 "{} hour{} ago",
19 secs / 3600,
20 if secs / 3600 == 1 { "" } else { "s" }
21 )
22 }
23}
24
25fn render_hard_section(out: &mut String, hard: &[BusyInfo]) {
26 for h in hard {
27 match h.source {
28 BusySource::ClaudeSession => {
29 out.push_str(" Active Claude session\n");
30 if let Some(secs) = h.started_secs_ago {
31 out.push_str(&format!(" last activity: {}\n", fmt_age(secs)));
32 }
33 if let Some(id_part) = h.cmd.strip_prefix("claude (session ") {
35 let id = id_part.trim_end_matches(')');
36 out.push_str(&format!(" session: {}\n", id));
37 }
38 }
39 BusySource::Lockfile => {
40 out.push_str(&format!(" Lockfile holder: PID {} ({})\n", h.pid, h.cmd));
41 }
42 BusySource::ProcessScan => {
43 out.push_str(&format!(" PID {} {}\n", h.pid, h.cmd));
45 }
46 }
47 out.push('\n');
48 }
49}
50
51fn render_soft_list(out: &mut String, soft: &[BusyInfo]) {
52 for s in soft {
53 let tty_label = match s.tty {
54 Some(true) => "(interactive)",
55 Some(false) => "(no tty)",
56 None => "",
57 };
58 let age_label = match s.started_secs_ago {
59 Some(secs) if secs < 90 => format!(" (started {})", fmt_age(secs)),
60 _ => String::new(),
61 };
62 out.push_str(&format!(
63 " PID {:>6} {} {}{}\n",
64 s.pid, s.cmd, tty_label, age_label
65 ));
66 }
67}
68
69pub fn render_busy_block(branch_display: &str, hard: &[BusyInfo], soft: &[BusyInfo]) -> String {
75 let mut out = String::new();
76 if hard.is_empty() && soft.is_empty() {
77 return out;
78 }
79 out.push_str(&format!(
80 "⚠ Worktree '{}' may be in use:\n\n",
81 branch_display
82 ));
83 if !hard.is_empty() {
84 render_hard_section(&mut out, hard);
85 }
86 if !soft.is_empty() {
87 out.push_str(" Processes with cwd in this worktree:\n");
88 render_soft_list(&mut out, soft);
89 }
90 out
91}
92
93pub fn render_refusal(branch_display: &str, hard: &[BusyInfo], soft: &[BusyInfo]) -> String {
97 let mut out = String::new();
98 match (hard.is_empty(), soft.is_empty()) {
99 (true, true) => return out,
100 (true, false) => {
101 out.push_str(&format!(
102 "⚠ Worktree '{}' may be in use:\n\n",
103 branch_display
104 ));
105 out.push_str(" Processes with cwd in this worktree:\n");
106 render_soft_list(&mut out, soft);
107 out.push('\n');
108 out.push_str(" These may malfunction if the worktree is deleted.\n");
109 out.push_str(" Re-run with --force to delete anyway.\n");
110 }
111 (false, true) => {
112 out.push_str(&format!(
113 "✗ Cannot delete worktree '{}' — in use:\n\n",
114 branch_display
115 ));
116 render_hard_section(&mut out, hard);
117 out.push_str(" Use --force to delete anyway.\n");
118 }
119 (false, false) => {
120 out.push_str(&format!(
121 "✗ Cannot delete worktree '{}' — in use:\n\n",
122 branch_display
123 ));
124 render_hard_section(&mut out, hard);
125 out.push_str(" Additional processes with cwd in this worktree:\n");
126 render_soft_list(&mut out, soft);
127 out.push('\n');
128 out.push_str(" Use --force to delete anyway.\n");
129 }
130 }
131 out
132}