Skip to main content

lit/commands/
revert.rs

1use crate::core::{
2    find_repo_root, get_current_branch, read_head, write_ref, Commit, Object, ObjectHash, Tree,
3};
4use crate::response::RevertResponse;
5use crate::storage::ObjectStore;
6
7pub fn execute(target: String) -> Result<RevertResponse, crate::errors::LitError> {
8    let repo_root = find_repo_root()?;
9    let store = ObjectStore::new(&repo_root);
10
11    // Resolve target to commit
12    let commit_hash = crate::commands::reset::execute_resolve(&repo_root, &target)?;
13    let hash_obj = ObjectHash::from_hex(commit_hash.clone());
14
15    let commit = match store.read(&hash_obj)? {
16        Object::Commit(c) => c,
17        _ => return Err(format!("'{}' is not a commit", target).into()),
18    };
19
20    // Get the parent of the commit to revert
21    let parent_hash = commit
22        .parents
23        .first()
24        .ok_or("Cannot revert a root commit")?;
25
26    let parent_commit = match store.read(parent_hash)? {
27        Object::Commit(c) => c,
28        _ => return Err("Parent is not a commit".into()),
29    };
30
31    // Get current HEAD tree
32    let head_hash = read_head(&repo_root)?;
33    let head_obj = ObjectHash::from_hex(head_hash.clone());
34    let head_commit = match store.read(&head_obj)? {
35        Object::Commit(c) => c,
36        _ => return Err("HEAD is not a commit".into()),
37    };
38
39    // Build the inverse: for each file changed in commit, restore to parent state
40    let commit_tree = load_tree_files(&store, &commit.tree)?;
41    let parent_tree = load_tree_files(&store, &parent_commit.tree)?;
42    let head_tree = load_tree_files(&store, &head_commit.tree)?;
43
44    let mut new_tree = Tree::new();
45    let mut reverted_files = Vec::new();
46
47    // Start with HEAD tree, apply inverse changes
48    for (path, (hash, mode)) in &head_tree {
49        let commit_version = commit_tree.get(path);
50        let parent_version = parent_tree.get(path);
51
52        match (commit_version, parent_version) {
53            // File was added in the commit → remove it in revert
54            (Some(_), None) => {
55                reverted_files.push(path.clone());
56                continue; // Don't add to new tree
57            }
58            // File was modified in the commit → restore parent version
59            (Some(cv), Some(pv)) if cv.0 != pv.0 => {
60                reverted_files.push(path.clone());
61                new_tree.add_entry(
62                    pv.1.clone(),
63                    path.clone(),
64                    ObjectHash::from_hex(pv.0.clone()),
65                    "blob".to_string(),
66                );
67            }
68            // File not changed by the commit → keep current
69            _ => {
70                new_tree.add_entry(
71                    mode.clone(),
72                    path.clone(),
73                    ObjectHash::from_hex(hash.clone()),
74                    "blob".to_string(),
75                );
76            }
77        }
78    }
79
80    // Files deleted in the commit → restore them
81    for (path, (hash, mode)) in &parent_tree {
82        if !commit_tree.contains_key(path) && !head_tree.contains_key(path) {
83            reverted_files.push(path.clone());
84            new_tree.add_entry(
85                mode.clone(),
86                path.clone(),
87                ObjectHash::from_hex(hash.clone()),
88                "blob".to_string(),
89            );
90        }
91    }
92
93    let tree_hash = store.write(&Object::Tree(new_tree))?;
94
95    let author = std::env::var("USER")
96        .or_else(|_| std::env::var("USERNAME"))
97        .unwrap_or_else(|_| "Unknown".to_string());
98
99    let revert_commit = Commit::new(
100        tree_hash,
101        vec![ObjectHash::from_hex(head_hash)],
102        author,
103        format!("Revert \"{}\"", commit.message),
104    );
105
106    let revert_hash = store.write(&Object::Commit(revert_commit))?;
107
108    // Update branch ref
109    let branch = get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string());
110    write_ref(
111        &repo_root,
112        &format!("heads/{}", branch),
113        revert_hash.as_str(),
114    )?;
115
116    Ok(RevertResponse {
117        reverted_commit: commit_hash[..16.min(commit_hash.len())].to_string(),
118        new_commit: revert_hash.short(),
119        files_changed: reverted_files.len(),
120        message: format!(
121            "Reverted commit {} in {}",
122            &commit_hash[..16.min(commit_hash.len())],
123            revert_hash.short()
124        ),
125    })
126}
127
128fn load_tree_files(
129    store: &ObjectStore,
130    tree_hash: &ObjectHash,
131) -> Result<std::collections::HashMap<String, (String, String)>, String> {
132    let tree = match store.read(tree_hash)? {
133        Object::Tree(t) => t,
134        _ => return Err("Not a tree".into()),
135    };
136
137    let mut files = std::collections::HashMap::new();
138    for entry in &tree.entries {
139        files.insert(
140            entry.name.clone(),
141            (entry.hash.to_string(), entry.mode.clone()),
142        );
143    }
144    Ok(files)
145}