Skip to main content

lit/commands/
stack.rs

1use crate::core::{find_repo_root, get_current_branch, list_refs, read_head, read_ref, write_ref};
2use crate::errors::LitError;
3use crate::response::CommandResponse;
4use serde::{Deserialize, Serialize};
5
6/// A branch in a stack (dependent chain)
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct StackEntry {
9    pub name: String,
10    pub base: Option<String>,
11    pub head: String,
12    pub is_current: bool,
13}
14
15#[derive(Debug, Serialize, Deserialize)]
16pub enum StackResponse {
17    List {
18        stacks: Vec<Vec<StackEntry>>,
19    },
20    Push {
21        branch: String,
22        base: String,
23        message: String,
24    },
25    Restack {
26        branches: Vec<String>,
27        message: String,
28    },
29    Show {
30        stack: Vec<StackEntry>,
31    },
32}
33
34impl CommandResponse for StackResponse {
35    fn command_name(&self) -> &'static str {
36        "stack"
37    }
38    fn human_readable(&self) -> String {
39        match self {
40            StackResponse::List { stacks } => {
41                let mut out = String::new();
42                for (i, stack) in stacks.iter().enumerate() {
43                    out.push_str(&format!("Stack {}:\n", i + 1));
44                    for entry in stack {
45                        let current = if entry.is_current { "* " } else { "  " };
46                        let base = entry
47                            .base
48                            .as_deref()
49                            .map(|b| format!(" (on {})", b))
50                            .unwrap_or_default();
51                        out.push_str(&format!(
52                            "  {}{}{} [{}]\n",
53                            current,
54                            entry.name,
55                            base,
56                            &entry.head[..8.min(entry.head.len())]
57                        ));
58                    }
59                }
60                if stacks.is_empty() {
61                    out.push_str("No stacked branches\n");
62                }
63                out
64            }
65            StackResponse::Push {
66                branch,
67                base,
68                message,
69            } => format!("Pushed {} onto {}: {}\n", branch, base, message),
70            StackResponse::Restack { branches, message } => {
71                let mut out = format!("{}\n", message);
72                for b in branches {
73                    out.push_str(&format!("  Restacked: {}\n", b));
74                }
75                out
76            }
77            StackResponse::Show { stack } => {
78                let mut out = String::new();
79                for (i, entry) in stack.iter().enumerate() {
80                    let current = if entry.is_current { "* " } else { "  " };
81                    let connector = if i == 0 { "  " } else { "│ " };
82                    out.push_str(&format!(
83                        "{}{}{} [{}]\n",
84                        connector,
85                        current,
86                        entry.name,
87                        &entry.head[..8.min(entry.head.len())]
88                    ));
89                    if i < stack.len() - 1 {
90                        out.push_str("│\n");
91                    }
92                }
93                out
94            }
95        }
96    }
97}
98
99/// Stack metadata file path
100fn stack_meta_path(repo_root: &std::path::Path) -> std::path::PathBuf {
101    repo_root.join(".lit").join("stack.json")
102}
103
104/// Stack metadata: maps branch -> base branch
105#[derive(Debug, Default, Serialize, Deserialize)]
106struct StackMeta {
107    /// branch_name -> base_branch_name
108    bases: std::collections::HashMap<String, String>,
109}
110
111fn load_stack_meta(repo_root: &std::path::Path) -> StackMeta {
112    let path = stack_meta_path(repo_root);
113    if path.exists() {
114        match std::fs::read_to_string(&path) {
115            Ok(data) => serde_json::from_str(&data).unwrap_or_default(),
116            Err(_) => StackMeta::default(),
117        }
118    } else {
119        StackMeta::default()
120    }
121}
122
123fn save_stack_meta(repo_root: &std::path::Path, meta: &StackMeta) -> Result<(), LitError> {
124    let path = stack_meta_path(repo_root);
125    let data = serde_json::to_string_pretty(meta)
126        .map_err(|e| LitError::general(format!("Failed to serialize stack meta: {}", e)))?;
127    std::fs::write(path, data)
128        .map_err(|e| LitError::io(format!("Failed to write stack meta: {}", e)))?;
129    Ok(())
130}
131
132/// List all stacks
133pub fn execute_list() -> Result<StackResponse, LitError> {
134    let repo_root = find_repo_root()?;
135    let meta = load_stack_meta(&repo_root);
136    let current = get_current_branch(&repo_root).ok();
137    let refs = list_refs(&repo_root, "heads").unwrap_or_default();
138
139    // Build adjacency: find root branches (no base or base not in stack)
140    let mut children: std::collections::HashMap<String, Vec<String>> =
141        std::collections::HashMap::new();
142    let mut roots: Vec<String> = Vec::new();
143
144    for (branch, _base) in &meta.bases {
145        children
146            .entry(_base.clone())
147            .or_default()
148            .push(branch.clone());
149    }
150
151    // Find roots: branches that are bases but not stacked on anything
152    for base in meta.bases.values() {
153        if !meta.bases.contains_key(base) && !roots.contains(base) {
154            roots.push(base.clone());
155        }
156    }
157
158    let mut stacks = Vec::new();
159    for root in &roots {
160        let mut stack = Vec::new();
161        let mut queue = vec![root.clone()];
162        while let Some(branch) = queue.pop() {
163            let head = refs
164                .iter()
165                .find(|r| r.name == branch)
166                .map(|r| r.hash.clone())
167                .unwrap_or_else(|| "unknown".to_string());
168            stack.push(StackEntry {
169                name: branch.clone(),
170                base: meta.bases.get(&branch).cloned(),
171                head,
172                is_current: Some(&branch) == current.as_ref(),
173            });
174            if let Some(ch) = children.get(&branch) {
175                for c in ch {
176                    queue.push(c.clone());
177                }
178            }
179        }
180        if stack.len() > 1 {
181            stacks.push(stack);
182        }
183    }
184
185    Ok(StackResponse::List { stacks })
186}
187
188/// Push a new branch onto the current branch (creating a stacked branch)
189pub fn execute_push(name: String) -> Result<StackResponse, LitError> {
190    let repo_root = find_repo_root()?;
191    let current = get_current_branch(&repo_root)?;
192    let head_hash = read_head(&repo_root)?;
193
194    // Create the new branch at HEAD
195    write_ref(&repo_root, &format!("heads/{}", name), &head_hash)?;
196
197    // Record stack relationship
198    let mut meta = load_stack_meta(&repo_root);
199    meta.bases.insert(name.clone(), current.clone());
200    save_stack_meta(&repo_root, &meta)?;
201
202    // Checkout the new branch
203    let head_path = repo_root.join(".lit").join("HEAD");
204    std::fs::write(head_path, format!("ref: refs/heads/{}", name))
205        .map_err(|e| LitError::io(format!("Failed to write HEAD: {}", e)))?;
206
207    Ok(StackResponse::Push {
208        branch: name,
209        base: current,
210        message: "Stacked branch created".to_string(),
211    })
212}
213
214/// Restack: rebase all child branches after amending/editing commits
215pub fn execute_restack() -> Result<StackResponse, LitError> {
216    let repo_root = find_repo_root()?;
217    let meta = load_stack_meta(&repo_root);
218    let mut restacked = Vec::new();
219
220    // For each stacked branch, ensure it's rebased on its base
221    for (branch, base) in &meta.bases {
222        let _base_hash = read_ref(&repo_root, &format!("heads/{}", base));
223        let _branch_hash = read_ref(&repo_root, &format!("heads/{}", branch));
224        // In a full implementation, this would rebase branch onto base
225        restacked.push(branch.clone());
226    }
227
228    Ok(StackResponse::Restack {
229        branches: restacked,
230        message: "All stacked branches restacked".to_string(),
231    })
232}
233
234/// Show the stack containing the current branch
235pub fn execute_show() -> Result<StackResponse, LitError> {
236    let repo_root = find_repo_root()?;
237    let current = get_current_branch(&repo_root)?;
238    let meta = load_stack_meta(&repo_root);
239    let refs = list_refs(&repo_root, "heads").unwrap_or_default();
240
241    // Walk up to find root
242    let mut root = current.clone();
243    while let Some(base) = meta.bases.get(&root) {
244        root = base.clone();
245    }
246
247    // Walk down to collect full stack
248    let mut stack = Vec::new();
249    let mut queue = vec![root];
250    while let Some(branch) = queue.pop() {
251        let head = refs
252            .iter()
253            .find(|r| r.name == branch)
254            .map(|r| r.hash.clone())
255            .unwrap_or_else(|| "unknown".to_string());
256        stack.push(StackEntry {
257            name: branch.clone(),
258            base: meta.bases.get(&branch).cloned(),
259            head,
260            is_current: branch == current,
261        });
262        for (child, base) in &meta.bases {
263            if base == &branch {
264                queue.push(child.clone());
265            }
266        }
267    }
268
269    Ok(StackResponse::Show { stack })
270}