Skip to main content

git_worktree_manager/
messages.rs

1//! Standardized error and informational messages for git-worktree-manager.
2//!
3
4pub fn worktree_not_found(branch: &str) -> String {
5    format!(
6        "No worktree found for branch '{}'. Use 'gw list' to see available worktrees.",
7        branch
8    )
9}
10
11pub fn branch_not_found(branch: &str) -> String {
12    format!("Branch '{}' not found", branch)
13}
14
15pub fn invalid_branch_name(error_msg: &str) -> String {
16    format!(
17        "Invalid branch name: {}\nHint: Use alphanumeric characters, hyphens, and slashes. \
18         Avoid special characters like emojis, backslashes, or control characters.",
19        error_msg
20    )
21}
22
23pub fn cannot_determine_branch() -> String {
24    "Cannot determine current branch".to_string()
25}
26
27pub fn cannot_determine_base_branch() -> String {
28    "Cannot determine base branch. Specify with --base or checkout a branch first.".to_string()
29}
30
31pub fn missing_metadata(branch: &str) -> String {
32    format!(
33        "Missing metadata for branch '{}'. Was this worktree created with 'gw new'?",
34        branch
35    )
36}
37
38pub fn base_repo_not_found(path: &str) -> String {
39    format!("Base repository not found at: {}", path)
40}
41
42pub fn worktree_dir_not_found(path: &str) -> String {
43    format!("Worktree directory does not exist: {}", path)
44}
45
46pub fn rebase_failed(
47    worktree_path: &str,
48    rebase_target: &str,
49    conflicted_files: Option<&[String]>,
50) -> String {
51    let mut msg = format!(
52        "Rebase failed. Please resolve conflicts manually:\n  cd {}\n  git rebase {}",
53        worktree_path, rebase_target
54    );
55    if let Some(files) = conflicted_files {
56        msg.push_str(&format!("\n\nConflicted files ({}):", files.len()));
57        for file in files {
58            msg.push_str(&format!("\n  \u{2022} {}", file));
59        }
60        msg.push_str("\n\nTip: Use --ai-merge flag to get AI assistance with conflicts");
61    }
62    msg
63}
64
65pub fn merge_failed(base_path: &str, feature_branch: &str) -> String {
66    format!(
67        "Fast-forward merge failed. Manual intervention required:\n  cd {}\n  git merge {}",
68        base_path, feature_branch
69    )
70}
71
72pub fn pr_creation_failed(stderr: &str) -> String {
73    format!("Failed to create pull request: {}", stderr)
74}
75
76pub fn gh_cli_not_found() -> String {
77    "GitHub CLI (gh) is required to create pull requests.\n\
78     Install it from: https://cli.github.com/"
79        .to_string()
80}
81
82pub fn cannot_delete_main_worktree() -> String {
83    "Cannot delete main repository worktree".to_string()
84}
85
86pub fn stash_not_found(stash_ref: &str) -> String {
87    format!(
88        "Stash '{}' not found. Use 'gw stash list' to see available stashes.",
89        stash_ref
90    )
91}
92
93pub fn backup_not_found(backup_id: &str, branch: &str) -> String {
94    format!("Backup '{}' not found for branch '{}'", backup_id, branch)
95}
96
97pub fn import_file_not_found(import_file: &str) -> String {
98    format!("Import file not found: {}", import_file)
99}
100
101pub fn detached_head_warning() -> String {
102    "Worktree is detached or branch not found. Specify branch with --branch or skip with --force."
103        .to_string()
104}
105
106// ---------------------------------------------------------------------------
107// Status / progress messages (used in styled println! calls)
108// ---------------------------------------------------------------------------
109
110pub fn rebase_in_progress(branch: &str, target: &str) -> String {
111    format!("Rebasing {} onto {}...", branch, target)
112}
113
114pub fn pushing_to_origin(branch: &str) -> String {
115    format!("Pushing {} to origin...", branch)
116}
117
118pub fn deleting_local_branch(branch: &str) -> String {
119    format!("Deleting local branch: {}", branch)
120}
121
122pub fn deleting_remote_branch(branch: &str) -> String {
123    format!("Deleting remote branch: origin/{}", branch)
124}
125
126pub fn removing_worktree(path: &std::path::Path) -> String {
127    format!("Removing worktree: {}", path.display())
128}
129
130pub fn cleanup_complete(deleted: u32) -> String {
131    format!("* Cleanup complete! Deleted {} worktree(s)", deleted)
132}
133
134pub fn starting_ai_tool_foreground(tool_name: &str) -> String {
135    format!("Starting {} (Ctrl+C to exit)...", tool_name)
136}
137
138pub fn starting_ai_tool_in(tool_name: &str) -> String {
139    format!("Starting {} in:", tool_name)
140}
141
142pub fn resuming_ai_tool_in(tool_name: &str) -> String {
143    format!("Resuming {} in:", tool_name)
144}
145
146pub fn switched_to_worktree(path: &std::path::Path) -> String {
147    format!("Switched to worktree: {}", path.display())
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_worktree_not_found() {
156        let msg = worktree_not_found("feature-x");
157        assert!(msg.contains("feature-x"));
158        assert!(msg.contains("gw list"));
159        assert!(msg.contains("No worktree found"));
160    }
161
162    #[test]
163    fn test_branch_not_found() {
164        let msg = branch_not_found("my-branch");
165        assert!(msg.contains("my-branch"));
166        assert!(msg.contains("not found"));
167    }
168
169    #[test]
170    fn test_invalid_branch_name() {
171        let msg = invalid_branch_name("contains spaces");
172        assert!(msg.contains("contains spaces"));
173        assert!(msg.contains("Invalid branch name"));
174        assert!(msg.contains("Hint"));
175        assert!(msg.contains("alphanumeric"));
176    }
177
178    #[test]
179    fn test_cannot_determine_branch() {
180        let msg = cannot_determine_branch();
181        assert!(msg.contains("Cannot determine current branch"));
182    }
183
184    #[test]
185    fn test_cannot_determine_base_branch() {
186        let msg = cannot_determine_base_branch();
187        assert!(msg.contains("Cannot determine base branch"));
188        assert!(msg.contains("--base"));
189    }
190
191    #[test]
192    fn test_missing_metadata() {
193        let msg = missing_metadata("feat-login");
194        assert!(msg.contains("feat-login"));
195        assert!(msg.contains("Missing metadata"));
196        assert!(msg.contains("gw new"));
197    }
198
199    #[test]
200    fn test_base_repo_not_found() {
201        let msg = base_repo_not_found("/tmp/repo");
202        assert!(msg.contains("/tmp/repo"));
203        assert!(msg.contains("Base repository not found"));
204    }
205
206    #[test]
207    fn test_worktree_dir_not_found() {
208        let msg = worktree_dir_not_found("/tmp/worktree");
209        assert!(msg.contains("/tmp/worktree"));
210        assert!(msg.contains("does not exist"));
211    }
212
213    #[test]
214    fn test_rebase_failed_without_conflicts() {
215        let msg = rebase_failed("/tmp/wt", "main", None);
216        assert!(msg.contains("Rebase failed"));
217        assert!(msg.contains("cd /tmp/wt"));
218        assert!(msg.contains("git rebase main"));
219        assert!(!msg.contains("Conflicted files"));
220    }
221
222    #[test]
223    fn test_rebase_failed_with_conflicts() {
224        let files = vec!["src/main.rs".to_string(), "Cargo.toml".to_string()];
225        let msg = rebase_failed("/tmp/wt", "main", Some(&files));
226        assert!(msg.contains("Rebase failed"));
227        assert!(msg.contains("cd /tmp/wt"));
228        assert!(msg.contains("git rebase main"));
229        assert!(msg.contains("Conflicted files (2)"));
230        assert!(msg.contains("src/main.rs"));
231        assert!(msg.contains("Cargo.toml"));
232        assert!(msg.contains("--ai-merge"));
233    }
234
235    #[test]
236    fn test_rebase_failed_with_empty_conflicts() {
237        let files: Vec<String> = vec![];
238        let msg = rebase_failed("/tmp/wt", "main", Some(&files));
239        assert!(msg.contains("Conflicted files (0)"));
240    }
241
242    #[test]
243    fn test_merge_failed() {
244        let msg = merge_failed("/tmp/base", "feature-api");
245        assert!(msg.contains("Fast-forward merge failed"));
246        assert!(msg.contains("cd /tmp/base"));
247        assert!(msg.contains("git merge feature-api"));
248    }
249
250    #[test]
251    fn test_pr_creation_failed() {
252        let msg = pr_creation_failed("permission denied");
253        assert!(msg.contains("Failed to create pull request"));
254        assert!(msg.contains("permission denied"));
255    }
256
257    #[test]
258    fn test_gh_cli_not_found() {
259        let msg = gh_cli_not_found();
260        assert!(msg.contains("GitHub CLI (gh)"));
261        assert!(msg.contains("https://cli.github.com/"));
262    }
263
264    #[test]
265    fn test_cannot_delete_main_worktree() {
266        let msg = cannot_delete_main_worktree();
267        assert!(msg.contains("Cannot delete main repository worktree"));
268    }
269
270    #[test]
271    fn test_stash_not_found() {
272        let msg = stash_not_found("stash@{0}");
273        assert!(msg.contains("stash@{0}"));
274        assert!(msg.contains("gw stash list"));
275    }
276
277    #[test]
278    fn test_backup_not_found() {
279        let msg = backup_not_found("abc123", "feature-x");
280        assert!(msg.contains("abc123"));
281        assert!(msg.contains("feature-x"));
282        assert!(msg.contains("not found"));
283    }
284
285    #[test]
286    fn test_import_file_not_found() {
287        let msg = import_file_not_found("/tmp/export.json");
288        assert!(msg.contains("/tmp/export.json"));
289        assert!(msg.contains("Import file not found"));
290    }
291
292    #[test]
293    fn test_detached_head_warning() {
294        let msg = detached_head_warning();
295        assert!(msg.contains("detached"));
296        assert!(msg.contains("--branch"));
297        assert!(msg.contains("--force"));
298    }
299
300    #[test]
301    fn test_rebase_in_progress() {
302        let msg = rebase_in_progress("feat-x", "main");
303        assert!(msg.contains("Rebasing feat-x onto main"));
304    }
305
306    #[test]
307    fn test_pushing_to_origin() {
308        let msg = pushing_to_origin("feat-x");
309        assert!(msg.contains("Pushing feat-x to origin"));
310    }
311
312    #[test]
313    fn test_deleting_local_branch() {
314        let msg = deleting_local_branch("feat-x");
315        assert!(msg.contains("Deleting local branch: feat-x"));
316    }
317
318    #[test]
319    fn test_deleting_remote_branch() {
320        let msg = deleting_remote_branch("feat-x");
321        assert!(msg.contains("origin/feat-x"));
322    }
323
324    #[test]
325    fn test_removing_worktree() {
326        let msg = removing_worktree(std::path::Path::new("/tmp/wt"));
327        assert!(msg.contains("Removing worktree:"));
328        assert!(msg.contains("/tmp/wt"));
329    }
330
331    #[test]
332    fn test_cleanup_complete() {
333        let msg = cleanup_complete(3);
334        assert!(msg.contains("3 worktree(s)"));
335    }
336
337    #[test]
338    fn test_starting_ai_tool_foreground() {
339        let msg = starting_ai_tool_foreground("claude");
340        assert!(msg.contains("Starting claude"));
341        assert!(msg.contains("Ctrl+C"));
342    }
343
344    #[test]
345    fn test_starting_ai_tool_in() {
346        let msg = starting_ai_tool_in("claude");
347        assert_eq!(msg, "Starting claude in:");
348    }
349
350    #[test]
351    fn test_resuming_ai_tool_in() {
352        let msg = resuming_ai_tool_in("claude");
353        assert_eq!(msg, "Resuming claude in:");
354    }
355
356    #[test]
357    fn test_switched_to_worktree() {
358        let msg = switched_to_worktree(std::path::Path::new("/tmp/wt"));
359        assert!(msg.contains("Switched to worktree:"));
360        assert!(msg.contains("/tmp/wt"));
361    }
362}