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