Skip to main content

git_claw/workflow/
run.rs

1//! Contextual command runner with environment injection.
2
3use crate::infra::git::{resolve_git_common_dir, resolve_toplevel};
4use crate::infra::registry::SlotRegistry;
5use crate::workflow::error::WorkflowError;
6use std::collections::BTreeMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11pub struct RunOptions {
12    pub name: Option<String>,
13    pub command: Vec<String>,
14    pub repo_root: Option<PathBuf>,
15}
16
17/// Parses a `.env.worktree` file into key-value pairs.
18pub fn parse_env_file(path: &Path) -> BTreeMap<String, String> {
19    let mut map = BTreeMap::new();
20    if let Ok(content) = fs::read_to_string(path) {
21        for line in content.lines() {
22            let trimmed = line.trim();
23            if trimmed.is_empty() || trimmed.starts_with('#') {
24                continue;
25            }
26            if let Some((k, v)) = trimmed.split_once('=') {
27                map.insert(k.trim().to_string(), v.trim().to_string());
28            }
29        }
30    }
31    map
32}
33
34/// Executes a command in the worktree directory with .env.worktree injected.
35pub fn run_command(options: RunOptions) -> Result<i32, WorkflowError> {
36    let toplevel = resolve_toplevel(options.repo_root.as_deref())?;
37    let common_dir = resolve_git_common_dir(Some(&toplevel))?;
38
39    let registry_path = common_dir.join("claw/slots.json");
40    let registry = SlotRegistry::load_or_empty(&registry_path)?;
41
42    // Determine target worktree and final command args
43    let (slot, cmd_args) = match options.name {
44        Some(name_str) => {
45            if let Some(s) = registry.find_by_name(&name_str) {
46                // Name matches an existing worktree
47                (s.clone(), options.command)
48            } else {
49                // Name does not match a worktree. Check if cwd is inside a worktree.
50                let cwd = std::env::current_dir().unwrap_or_default();
51                let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
52                let found = registry.slots.iter().find(|s| {
53                    let p = PathBuf::from(&s.path);
54                    let canonical_p = p.canonicalize().unwrap_or(p);
55                    canonical_cwd.starts_with(&canonical_p)
56                });
57
58                if let Some(s) = found {
59                    let mut full_cmd = vec![name_str];
60                    full_cmd.extend(options.command);
61                    (s.clone(), full_cmd)
62                } else {
63                    return Err(WorkflowError::WorktreeNotFound(name_str));
64                }
65            }
66        }
67        None => {
68            let cwd = std::env::current_dir().unwrap_or_default();
69            let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
70            let found = registry.slots.iter().find(|s| {
71                let p = PathBuf::from(&s.path);
72                let canonical_p = p.canonicalize().unwrap_or(p);
73                canonical_cwd.starts_with(&canonical_p)
74            });
75
76            match found {
77                Some(s) => (s.clone(), options.command),
78                None => {
79                    return Err(WorkflowError::CannotInferWorktree(
80                        canonical_cwd.to_string_lossy().to_string(),
81                    ));
82                }
83            }
84        }
85    };
86
87    if cmd_args.is_empty() {
88        return Err(WorkflowError::NoCommandSpecified);
89    }
90
91    let worktree_path = PathBuf::from(&slot.path);
92    let env_file_path = worktree_path.join(".env.worktree");
93    let env_vars = parse_env_file(&env_file_path);
94
95    let mut cmd = Command::new(&cmd_args[0]);
96    cmd.args(&cmd_args[1..]);
97    cmd.current_dir(&worktree_path);
98
99    for (k, v) in env_vars {
100        cmd.env(k, v);
101    }
102
103    let status = cmd.status().map_err(|e| {
104        WorkflowError::CommandExecutionFailed(format!("failed to execute '{}': {}", cmd_args[0], e))
105    })?;
106
107    Ok(status.code().unwrap_or(1))
108}