Skip to main content

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(&registry_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}