Skip to main content

lit/commands/
clean.rs

1use crate::core::{find_repo_root, get_current_branch, list_refs};
2use crate::errors::LitError;
3use crate::response::CommandResponse;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Serialize, Deserialize)]
7pub struct CleanResponse {
8    pub removed: Vec<String>,
9    pub message: String,
10}
11
12impl CommandResponse for CleanResponse {
13    fn command_name(&self) -> &'static str {
14        "clean"
15    }
16    fn human_readable(&self) -> String {
17        if self.removed.is_empty() {
18            "No empty branches to clean\n".to_string()
19        } else {
20            let mut out = format!("{}\n", self.message);
21            for b in &self.removed {
22                out.push_str(&format!("  Removed: {}\n", b));
23            }
24            out
25        }
26    }
27}
28
29/// Remove empty branches from the workspace.
30/// A branch is considered "empty" if it points to the same commit as its
31/// base/parent branch or is identical to the default branch.
32pub fn execute(dry_run: bool) -> Result<CleanResponse, LitError> {
33    let repo_root = find_repo_root()?;
34    let current = get_current_branch(&repo_root)?;
35    let refs = list_refs(&repo_root, "heads").unwrap_or_default();
36
37    // Load stack metadata to understand branch relationships
38    let stack_meta_path = repo_root.join(".lit").join("stack.json");
39    let stack_bases: std::collections::HashMap<String, String> = if stack_meta_path.exists() {
40        #[derive(Deserialize)]
41        struct SM {
42            bases: std::collections::HashMap<String, String>,
43        }
44        match std::fs::read_to_string(&stack_meta_path) {
45            Ok(data) => serde_json::from_str::<SM>(&data)
46                .map(|s| s.bases)
47                .unwrap_or_default(),
48            Err(_) => std::collections::HashMap::new(),
49        }
50    } else {
51        std::collections::HashMap::new()
52    };
53
54    // Find branches that point to the same commit as their base
55    let mut to_remove = Vec::new();
56    let default_branch_hash = refs
57        .iter()
58        .find(|r| r.name == "main" || r.name == "master")
59        .map(|r| r.hash.clone());
60
61    for r in &refs {
62        if r.name == current {
63            continue; // Never remove current branch
64        }
65        if r.name == "main" || r.name == "master" {
66            continue; // Never remove default branch
67        }
68
69        let is_empty = if let Some(base_name) = stack_bases.get(&r.name) {
70            // Check if branch points to same commit as its stack base
71            refs.iter()
72                .find(|br| br.name == *base_name)
73                .map(|br| br.hash == r.hash)
74                .unwrap_or(false)
75        } else if let Some(ref dh) = default_branch_hash {
76            // Check if branch points to same commit as default branch
77            &r.hash == dh
78        } else {
79            false
80        };
81
82        if is_empty {
83            to_remove.push(r.name.clone());
84        }
85    }
86
87    if !dry_run {
88        for name in &to_remove {
89            let _ = crate::core::delete_ref(&repo_root, &format!("heads/{}", name));
90        }
91    }
92
93    let msg = if dry_run {
94        format!("Would remove {} empty branch(es)", to_remove.len())
95    } else {
96        format!("Removed {} empty branch(es)", to_remove.len())
97    };
98
99    Ok(CleanResponse {
100        removed: to_remove,
101        message: msg,
102    })
103}