Skip to main content

git_worktree_manager/operations/
config_ops.rs

1/// Configuration-related operations.
2///
3use 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
14/// Change the base branch for a worktree (with optional rebase + dry-run).
15pub 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        // Use resolve_worktree_target to respect lookup_mode
26        match super::helpers::resolve_worktree_target(Some(b), lookup_mode) {
27            Ok(resolved) => resolved.branch,
28            Err(_) => b.to_string(), // Fallback to raw name
29        }
30    } else {
31        git::get_current_branch(Some(&std::env::current_dir()?))?
32    };
33
34    // Verify new base exists
35    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    // Fetch
72    let _ = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, false);
73
74    // Rebase onto new base
75    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    // Find worktree path for rebase
94    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    // Update metadata
116    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
126/// Export worktree configuration to a file.
127pub 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
193/// Import worktree configuration from a file.
194pub 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    // Preview
226    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    // Apply
272    println!("\n{}\n", style("Applying import...").yellow().bold());
273
274    let repo = git::get_repo_root(None)?;
275    let mut imported = 0u32;
276
277    // Import global config
278    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    // Import worktree metadata
287    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}