git_claw/workflow/nav.rs
1//! Navigation and editor integration workflows (`git claw cd` and `git claw open`).
2
3use crate::core::config::Config;
4use crate::infra::git::{resolve_git_common_dir, resolve_primary_repo_root, resolve_repo_name};
5use crate::infra::registry::SlotRegistry;
6use crate::workflow::error::WorkflowError;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10/// Options for navigating to a worktree or primary repository root.
11///
12/// :param name: Target branch, slot name, or root alias ("main", "master", "root").
13/// :param repo_root: Optional repository root path.
14///
15/// # Example
16///
17/// ```
18/// use git_claw::workflow::CdOptions;
19///
20/// let options = CdOptions {
21/// name: Some("main"),
22/// repo_root: None,
23/// };
24/// assert_eq!(options.name, Some("main"));
25/// ```
26pub struct CdOptions<'a> {
27 pub name: Option<&'a str>,
28 pub repo_root: Option<&'a Path>,
29}
30
31/// Options for opening a worktree in an editor.
32///
33/// :param name: Target worktree name or alias.
34/// :param editor: Optional editor binary or command name.
35/// :param repo_root: Optional repository root path.
36///
37/// # Example
38///
39/// ```
40/// use git_claw::workflow::OpenOptions;
41///
42/// let options = OpenOptions {
43/// name: Some("feature-x"),
44/// editor: Some("nano"),
45/// repo_root: None,
46/// };
47/// assert_eq!(options.editor, Some("nano"));
48/// ```
49pub struct OpenOptions<'a> {
50 pub name: Option<&'a str>,
51 pub editor: Option<&'a str>,
52 pub repo_root: Option<&'a Path>,
53}
54
55/// Resolves the worktree path from name or current working directory.
56///
57/// If `name` is `"main"`, `"master"`, `"root"`, or matches the configured main branch,
58/// returns the primary repository root directory.
59/// If `name` is omitted and the shell is outside any active worktree, falls back to the
60/// primary repository root directory.
61///
62/// :param name: Optional target worktree name or primary alias.
63/// :param repo_root: Optional repository root path override.
64/// :return: Resolved [`PathBuf`] on success, or [`WorkflowError`] on failure.
65fn resolve_target_worktree_path(
66 name: Option<&str>,
67 repo_root: Option<&Path>,
68) -> Result<PathBuf, WorkflowError> {
69 let toplevel = resolve_primary_repo_root(repo_root)?;
70 let common_dir = resolve_git_common_dir(Some(&toplevel))?;
71
72 let config_path = toplevel.join(".git-claw.toml");
73 let repo_name = resolve_repo_name(&toplevel);
74 let config = Config::load_or_default(&config_path, &repo_name)?;
75
76 let registry_path = common_dir.join("claw/slots.json");
77 let registry = SlotRegistry::load_or_empty(®istry_path)?;
78
79 match name {
80 Some(target_name) => {
81 if target_name == "main"
82 || target_name == "master"
83 || target_name == "root"
84 || target_name == config.main_branch()
85 {
86 return Ok(toplevel);
87 }
88 let slot = registry
89 .find_by_name(target_name)
90 .ok_or_else(|| WorkflowError::WorktreeNotFound(target_name.to_string()))?;
91 Ok(PathBuf::from(&slot.path))
92 }
93 None => {
94 let cwd = repo_root
95 .map(|p| p.to_path_buf())
96 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
97 let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
98 let found = registry.slots.iter().find(|s| {
99 let p = PathBuf::from(&s.path);
100 let canonical_p = p.canonicalize().unwrap_or(p);
101 canonical_cwd.starts_with(&canonical_p)
102 });
103
104 match found {
105 Some(s) => Ok(PathBuf::from(&s.path)),
106 None => Ok(toplevel),
107 }
108 }
109 }
110}
111
112/// Resolves and prints the raw worktree path to stdout for shell integration (`cd $(git claw cd <name>)`).
113///
114/// :param options: CD navigation options containing target name and optional repo root.
115/// :return: Resolved [`PathBuf`] on success, or [`WorkflowError`] on failure.
116///
117/// # Example
118///
119/// ```no_run
120/// use git_claw::workflow::{cd_worktree, CdOptions};
121///
122/// let path = cd_worktree(CdOptions {
123/// name: Some("main"),
124/// repo_root: None,
125/// });
126/// ```
127pub fn cd_worktree(options: CdOptions<'_>) -> Result<PathBuf, WorkflowError> {
128 let path = resolve_target_worktree_path(options.name, options.repo_root)?;
129 println!("{}", path.display());
130 Ok(path)
131}
132
133/// Launches the configured editor targeting the worktree directory.
134///
135/// :param options: Editor launch options.
136/// :return: Unit result on success, or [`WorkflowError`] on failure.
137///
138/// # Example
139///
140/// ```no_run
141/// use git_claw::workflow::{open_worktree, OpenOptions};
142///
143/// let res = open_worktree(OpenOptions {
144/// name: Some("main"),
145/// editor: Some("true"),
146/// repo_root: None,
147/// });
148/// ```
149pub fn open_worktree(options: OpenOptions<'_>) -> Result<(), WorkflowError> {
150 let path = resolve_target_worktree_path(options.name, options.repo_root)?;
151
152 let editor_cmd = match options.editor {
153 Some(e) => e.to_string(),
154 None => std::env::var("EDITOR")
155 .or_else(|_| std::env::var("VISUAL"))
156 .unwrap_or_else(|_| "code".to_string()),
157 };
158
159 let status = Command::new(&editor_cmd).arg(&path).status().map_err(|e| {
160 WorkflowError::CommandExecutionFailed(format!(
161 "failed to launch editor '{}': {}",
162 editor_cmd, e
163 ))
164 })?;
165
166 if !status.success() {
167 return Err(WorkflowError::CommandExecutionFailed(format!(
168 "editor '{}' exited with non-zero status",
169 editor_cmd
170 )));
171 }
172
173 Ok(())
174}