Skip to main content

lit/commands/
squash.rs

1use crate::core::{
2    find_repo_root, get_current_branch, read_head, write_ref, Commit, Object, ObjectHash,
3};
4use crate::response::CommitResponse;
5use crate::storage::ObjectStore;
6
7/// Squash the last N commits into a single commit.
8pub fn execute(
9    count: usize,
10    message: Option<String>,
11) -> Result<CommitResponse, crate::errors::LitError> {
12    if count < 2 {
13        return Err("Squash requires at least 2 commits".into());
14    }
15
16    let repo_root = find_repo_root()?;
17    let store = ObjectStore::new(&repo_root);
18
19    let head_hash = read_head(&repo_root)?;
20
21    // Walk back `count` commits to find the base
22    let mut current_hash = head_hash.clone();
23    let mut messages: Vec<String> = Vec::new();
24    let mut final_tree = String::new();
25    let mut final_author = String::new();
26
27    for i in 0..count {
28        let obj = store.read(&ObjectHash::from_hex(current_hash.clone()))?;
29        match obj {
30            Object::Commit(c) => {
31                if i == 0 {
32                    final_tree = c.tree.to_string();
33                    final_author = c.author.clone();
34                }
35                messages.push(c.message.clone());
36                if let Some(parent) = c.parents.first() {
37                    current_hash = parent.to_string();
38                } else if i < count - 1 {
39                    return Err(format!(
40                        "Only {} commits available, cannot squash {}",
41                        i + 1,
42                        count
43                    )
44                    .into());
45                }
46            }
47            _ => return Err("Unexpected non-commit object in history".into()),
48        }
49    }
50
51    messages.reverse();
52
53    // The parent of the squashed commit is the parent of the oldest squashed commit
54    let base_obj = store.read(&ObjectHash::from_hex(current_hash.clone()))?;
55    let parents = match base_obj {
56        Object::Commit(_) => vec![crate::core::ObjectHash::from_hex(current_hash.clone())],
57        _ => vec![],
58    };
59    let parent_str = Some(current_hash);
60
61    let squash_message = message.unwrap_or_else(|| {
62        let mut msg = String::from("Squashed commits:\n\n");
63        for m in &messages {
64            msg.push_str(&format!("* {}\n", m));
65        }
66        msg
67    });
68
69    let tree_hash = crate::core::ObjectHash::from_hex(final_tree.clone());
70    let commit = Commit::new(
71        tree_hash,
72        parents,
73        final_author.clone(),
74        squash_message.clone(),
75    );
76    let timestamp = commit.timestamp;
77    let commit_object = Object::Commit(commit);
78    let commit_hash = store.write(&commit_object)?;
79
80    let branch = get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string());
81    write_ref(
82        &repo_root,
83        &format!("heads/{}", branch),
84        commit_hash.as_str(),
85    )?;
86
87    Ok(CommitResponse {
88        hash: commit_hash.to_string(),
89        short_hash: commit_hash.short().to_string(),
90        tree: final_tree,
91        parent: parent_str,
92        author: final_author,
93        message: squash_message,
94        timestamp,
95    })
96}