Skip to main content

git_worktree_manager/operations/
config_ops.rs

1/// Configuration-related operations.
2///
3/// Mirrors src/git_worktree_manager/operations/config_ops.py.
4use 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
14/// Change the base branch for a worktree (with optional rebase + dry-run).
15pub 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    // Verify new base exists
25    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    // Fetch
65    let _ = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, false);
66
67    // Rebase onto new base
68    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    // Find worktree path for rebase
87    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    // Update metadata
109    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
119/// Export worktree configuration to a file.
120pub 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
181/// Import worktree configuration from a file.
182pub 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    // Preview
206    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    // Apply
252    println!("\n{}\n", style("Applying import...").yellow().bold());
253
254    let repo = git::get_repo_root(None)?;
255    let mut imported = 0u32;
256
257    // Import global config
258    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    // Import worktree metadata
267    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}