git_worktree_manager/operations/
config_ops.rs1use std::path::PathBuf;
5
6use console::style;
7use serde_json::Value;
8
9use crate::config;
10use crate::constants::{format_config_key, CONFIG_KEY_BASE_BRANCH, CONFIG_KEY_BASE_PATH};
11use crate::error::{CwError, Result};
12use crate::git;
13
14pub fn change_base_branch(new_base: &str, branch: Option<&str>, dry_run: bool) -> Result<()> {
16 let repo = git::get_repo_root(None)?;
17
18 let feature_branch = if let Some(b) = branch {
19 b.to_string()
20 } else {
21 git::get_current_branch(Some(&std::env::current_dir()?))?
22 };
23
24 if !git::branch_exists(new_base, Some(&repo)) {
26 return Err(CwError::InvalidBranch(format!(
27 "Base branch '{}' not found",
28 new_base
29 )));
30 }
31
32 let key = format_config_key(CONFIG_KEY_BASE_BRANCH, &feature_branch);
33 let old_base = git::get_config(&key, Some(&repo));
34
35 println!("\n{}", style("Changing base branch:").cyan().bold());
36 println!(" Worktree: {}", style(&feature_branch).green());
37 if let Some(ref old) = old_base {
38 println!(" Current base: {}", style(old).yellow());
39 }
40 println!(" New base: {}\n", style(new_base).green());
41
42 if dry_run {
43 println!(
44 "{}\n",
45 style("DRY RUN MODE — No changes will be made")
46 .yellow()
47 .bold()
48 );
49 println!(
50 "{}\n",
51 style("The following operations would be performed:").bold()
52 );
53 println!(" 1. Fetch updates from remote");
54 println!(" 2. Rebase {} onto {}", feature_branch, new_base);
55 println!(
56 " 3. Update base branch metadata: {} -> {}",
57 old_base.as_deref().unwrap_or("none"),
58 new_base
59 );
60 println!("\n{}\n", style("Run without --dry-run to execute.").dim());
61 return Ok(());
62 }
63
64 let _ = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, false);
66
67 let rebase_target = {
69 let origin = format!("origin/{}", new_base);
70 if git::branch_exists(&origin, Some(&repo)) {
71 origin
72 } else {
73 new_base.to_string()
74 }
75 };
76
77 println!(
78 "{}",
79 style(format!(
80 "Rebasing {} onto {}...",
81 feature_branch, rebase_target
82 ))
83 .yellow()
84 );
85
86 let wt_path = git::find_worktree_by_branch(&repo, &feature_branch)?
88 .or(git::find_worktree_by_branch(
89 &repo,
90 &format!("refs/heads/{}", feature_branch),
91 )?)
92 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
93
94 match git::git_command(&["rebase", &rebase_target], Some(&wt_path), false, true) {
95 Ok(r) if r.returncode == 0 => {
96 println!("{} Rebase successful\n", style("*").green().bold());
97 }
98 _ => {
99 let _ = git::git_command(&["rebase", "--abort"], Some(&wt_path), false, false);
100 return Err(CwError::Rebase(format!(
101 "Rebase failed. Resolve conflicts manually:\n cd {}\n git rebase {}",
102 wt_path.display(),
103 rebase_target
104 )));
105 }
106 }
107
108 git::set_config(&key, new_base, Some(&repo))?;
110 println!(
111 "{} Base branch changed to '{}'\n",
112 style("*").green().bold(),
113 new_base
114 );
115
116 Ok(())
117}
118
119pub fn export_config(output: Option<&str>) -> Result<()> {
121 let repo = git::get_repo_root(None)?;
122 let cfg = config::load_config()?;
123
124 let mut worktrees_data: Vec<Value> = Vec::new();
125
126 for (branch_name, path) in git::get_feature_worktrees(Some(&repo))? {
127 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, &branch_name);
128 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, &branch_name);
129 let base_branch = git::get_config(&bb_key, Some(&repo));
130 let base_path = git::get_config(&bp_key, Some(&repo));
131
132 worktrees_data.push(serde_json::json!({
133 "branch": branch_name,
134 "base_branch": base_branch,
135 "base_path": base_path,
136 "path": path.to_string_lossy(),
137 }));
138 }
139
140 let export_data = serde_json::json!({
141 "export_version": "1.0",
142 "exported_at": crate::session::chrono_now_iso_pub(),
143 "repository": repo.to_string_lossy(),
144 "config": serde_json::to_value(&cfg)?,
145 "worktrees": worktrees_data,
146 });
147
148 let timestamp = crate::session::chrono_now_iso_pub()
149 .replace([':', '-'], "")
150 .split('T')
151 .collect::<Vec<_>>()
152 .join("-")
153 .trim_end_matches('Z')
154 .to_string();
155
156 let output_path = output
157 .map(|s| s.to_string())
158 .unwrap_or_else(|| format!("cw-export-{}.json", timestamp));
159
160 println!(
161 "\n{} {}",
162 style("Exporting configuration to:").yellow(),
163 output_path
164 );
165
166 let content = serde_json::to_string_pretty(&export_data)?;
167 std::fs::write(&output_path, content)?;
168
169 println!("{} Export complete!\n", style("*").green().bold());
170 println!("{}", style("Exported:").bold());
171 println!(" - {} worktree(s)", worktrees_data.len());
172 println!(" - Configuration settings");
173 println!(
174 "\n{}\n",
175 style("Transfer this file and use 'gw import' to restore.").dim()
176 );
177
178 Ok(())
179}
180
181pub fn import_config(import_file: &str, apply: bool) -> Result<()> {
183 let path = PathBuf::from(import_file);
184 if !path.exists() {
185 return Err(CwError::Config(format!(
186 "Import file not found: {}",
187 import_file
188 )));
189 }
190
191 println!(
192 "\n{} {}\n",
193 style("Loading import file:").yellow(),
194 import_file
195 );
196
197 let content = std::fs::read_to_string(&path)?;
198 let data: Value = serde_json::from_str(&content)
199 .map_err(|e| CwError::Config(format!("Failed to read import file: {}", e)))?;
200
201 if data.get("export_version").is_none() {
202 return Err(CwError::Config("Invalid export file format".to_string()));
203 }
204
205 println!("{}\n", style("Import Preview:").cyan().bold());
207 println!(
208 "{} {}",
209 style("Exported from:").bold(),
210 data.get("repository")
211 .and_then(|v| v.as_str())
212 .unwrap_or("unknown")
213 );
214 println!(
215 "{} {}",
216 style("Exported at:").bold(),
217 data.get("exported_at")
218 .and_then(|v| v.as_str())
219 .unwrap_or("unknown")
220 );
221 let worktrees = data
222 .get("worktrees")
223 .and_then(|v| v.as_array())
224 .cloned()
225 .unwrap_or_default();
226 println!("{} {}\n", style("Worktrees:").bold(), worktrees.len());
227
228 for wt in &worktrees {
229 println!(
230 " - {}",
231 wt.get("branch")
232 .and_then(|v| v.as_str())
233 .unwrap_or("unknown")
234 );
235 println!(
236 " Base: {}",
237 wt.get("base_branch")
238 .and_then(|v| v.as_str())
239 .unwrap_or("unknown")
240 );
241 }
242
243 if !apply {
244 println!(
245 "\n{} No changes made. Use --apply to import configuration.\n",
246 style("Preview mode:").yellow().bold()
247 );
248 return Ok(());
249 }
250
251 println!("\n{}\n", style("Applying import...").yellow().bold());
253
254 let repo = git::get_repo_root(None)?;
255 let mut imported = 0u32;
256
257 if let Some(cfg_val) = data.get("config") {
259 if let Ok(cfg) = serde_json::from_value::<config::Config>(cfg_val.clone()) {
260 println!("{}", style("Importing global configuration...").yellow());
261 config::save_config(&cfg)?;
262 println!("{} Configuration imported\n", style("*").green().bold());
263 }
264 }
265
266 println!("{}\n", style("Importing worktree metadata...").yellow());
268 for wt in &worktrees {
269 let branch = wt.get("branch").and_then(|v| v.as_str());
270 let base = wt.get("base_branch").and_then(|v| v.as_str());
271
272 if let (Some(b), Some(bb)) = (branch, base) {
273 if !git::branch_exists(b, Some(&repo)) {
274 println!(
275 "{} Branch '{}' not found locally. Create with 'gw new {} --base {}'",
276 style("!").yellow(),
277 b,
278 b,
279 bb
280 );
281 continue;
282 }
283
284 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, b);
285 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, b);
286 let _ = git::set_config(&bb_key, bb, Some(&repo));
287 let _ = git::set_config(&bp_key, &repo.to_string_lossy(), Some(&repo));
288 println!("{} Imported metadata for: {}", style("*").green().bold(), b);
289 imported += 1;
290 }
291 }
292
293 println!(
294 "\n{}\n",
295 style(format!(
296 "* Import complete! Imported {} worktree(s)",
297 imported
298 ))
299 .green()
300 .bold()
301 );
302
303 Ok(())
304}