Skip to main content

git_claw/workflow/
publish.rs

1//! Remote worktree publish workflow (`git claw [type] publish [name]` and `git claw publish [name]`).
2//!
3//! Pushes worktree branches to remote Git repositories with upstream tracking.
4
5use crate::cli::output::print_success;
6use crate::core::branch::BranchType;
7use crate::core::config::Config;
8use crate::infra::git::{
9    git_branch_exists, git_get_remotes, git_push_upstream, resolve_git_common_dir,
10    resolve_primary_repo_root, resolve_repo_name,
11};
12use crate::infra::registry::{SlotRecord, SlotRegistry};
13use crate::workflow::error::WorkflowError;
14use std::path::{Path, PathBuf};
15
16/// Options for the worktree publish workflow.
17///
18/// # Example
19///
20/// ```
21/// use git_claw::workflow::PublishOptions;
22/// use git_claw::core::branch::BranchType;
23///
24/// let options = PublishOptions {
25///     branch_type: Some(BranchType::Feature),
26///     name: Some("login"),
27///     remote: Some("origin"),
28///     repo_root: None,
29/// };
30/// assert_eq!(options.name, Some("login"));
31/// ```
32#[derive(Debug, Clone, Default)]
33pub struct PublishOptions<'a> {
34    pub branch_type: Option<BranchType>,
35    pub name: Option<&'a str>,
36    pub remote: Option<&'a str>,
37    pub repo_root: Option<&'a Path>,
38}
39
40/// Resolves the active worktree slot from the current working directory or explicit repo root.
41///
42/// # Example
43///
44/// ```
45/// use git_claw::infra::registry::SlotRegistry;
46/// use git_claw::workflow::publish::resolve_active_worktree_slot;
47///
48/// let registry = SlotRegistry::empty();
49/// let result = resolve_active_worktree_slot(&registry, None);
50/// assert!(result.is_err());
51/// ```
52pub fn resolve_active_worktree_slot<'a>(
53    registry: &'a SlotRegistry,
54    repo_root: Option<&Path>,
55) -> Result<&'a SlotRecord, WorkflowError> {
56    let cwd = if let Some(root) = repo_root {
57        let canonical = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
58        if registry.slots.iter().any(|s| {
59            let p = PathBuf::from(&s.path);
60            let cp = p.canonicalize().unwrap_or(p);
61            canonical.starts_with(&cp)
62        }) {
63            canonical
64        } else {
65            std::env::current_dir().unwrap_or_default()
66        }
67    } else {
68        std::env::current_dir().unwrap_or_default()
69    };
70    let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
71    let found = registry.slots.iter().find(|s| {
72        let p = PathBuf::from(&s.path);
73        let canonical_p = p.canonicalize().unwrap_or(p);
74        canonical_cwd.starts_with(&canonical_p)
75    });
76
77    found.ok_or_else(|| {
78        WorkflowError::CannotInferWorktree(canonical_cwd.to_string_lossy().to_string())
79    })
80}
81
82/// Orchestrates publishing a worktree branch to a remote Git repository with upstream tracking.
83///
84/// # Example
85///
86/// ```no_run
87/// use std::path::Path;
88/// use git_claw::core::branch::BranchType;
89/// use git_claw::workflow::{publish_worktree, PublishOptions};
90///
91/// # fn run() -> Result<(), git_claw::workflow::WorkflowError> {
92/// publish_worktree(PublishOptions {
93///     branch_type: Some(BranchType::Feature),
94///     name: Some("login"),
95///     remote: Some("origin"),
96///     repo_root: Some(Path::new(".")),
97/// })?;
98/// # Ok(())
99/// # }
100/// ```
101pub fn publish_worktree(options: PublishOptions<'_>) -> Result<(), WorkflowError> {
102    let toplevel = resolve_primary_repo_root(options.repo_root)?;
103    let common_dir = resolve_git_common_dir(Some(&toplevel))?;
104    let repo_name = resolve_repo_name(&toplevel);
105
106    let config_path = toplevel.join(".git-claw.toml");
107    let config = Config::load_or_default(&config_path, &repo_name)?;
108
109    let registry_path = common_dir.join("claw/slots.json");
110    let registry = SlotRegistry::load_or_empty(&registry_path)?;
111
112    let slot = match options.name {
113        Some(target_name) => {
114            let found = registry.slots.iter().find(|s| {
115                if let Some(bt) = options.branch_type {
116                    (s.name == target_name
117                        || s.branch == target_name
118                        || s.branch == format!("{}/{}", bt.as_str(), target_name))
119                        && s.branch_type == bt.as_str()
120                } else {
121                    s.name == target_name || s.branch == target_name
122                }
123            });
124            found.ok_or_else(|| WorkflowError::WorktreeNotFound(target_name.to_string()))?
125        }
126        None => {
127            let slot = resolve_active_worktree_slot(&registry, options.repo_root)?;
128            if let Some(bt) = options.branch_type {
129                if slot.branch_type != bt.as_str() {
130                    let cwd = std::env::current_dir().unwrap_or_default();
131                    return Err(WorkflowError::CannotInferWorktree(
132                        cwd.to_string_lossy().to_string(),
133                    ));
134                }
135            }
136            slot
137        }
138    };
139
140    let worktree_path = PathBuf::from(&slot.path);
141    if !worktree_path.exists() {
142        return Err(WorkflowError::WorktreeNotFound(slot.name.clone()));
143    }
144
145    if !git_branch_exists(&toplevel, &slot.branch)? {
146        return Err(WorkflowError::WorktreeNotFound(slot.name.clone()));
147    }
148
149    let remotes = git_get_remotes(&toplevel)?;
150    if remotes.is_empty() {
151        return Err(WorkflowError::NoRemoteConfigured);
152    }
153
154    let remote = options
155        .remote
156        .map(|r| r.to_string())
157        .or_else(|| config.remote().map(|r| r.to_string()))
158        .unwrap_or_else(|| "origin".to_string());
159
160    git_push_upstream(&toplevel, &remote, &slot.branch)?;
161
162    print_success(format!(
163        "Branch '{}' successfully published to '{}' with upstream tracking",
164        slot.branch, remote
165    ));
166
167    Ok(())
168}