git_claw/workflow/
publish.rs1use 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_repo_name, resolve_toplevel,
11};
12use crate::infra::registry::{SlotRecord, SlotRegistry};
13use crate::workflow::error::WorkflowError;
14use std::path::{Path, PathBuf};
15
16#[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
40pub 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
82pub fn publish_worktree(options: PublishOptions<'_>) -> Result<(), WorkflowError> {
102 let toplevel = resolve_toplevel(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(®istry_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(®istry, 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}