Skip to main content

lit/commands/
reset.rs

1use crate::core::{
2    find_repo_root, get_current_branch, read_head, set_head_detached, write_ref, Object, ObjectHash,
3};
4use crate::response::ResetResponse;
5use crate::storage::{Index, ObjectStore};
6use std::fs;
7
8pub fn execute(
9    target: String,
10    soft: bool,
11    hard: bool,
12) -> Result<ResetResponse, crate::errors::LitError> {
13    let repo_root = find_repo_root()?;
14    let store = ObjectStore::new(&repo_root);
15
16    // Resolve target to a commit hash
17    let commit_hash = execute_resolve(&repo_root, &target)?;
18
19    // Verify it's a commit
20    let hash_obj = ObjectHash::from_hex(commit_hash.clone());
21    let commit = match store.read(&hash_obj)? {
22        Object::Commit(c) => c,
23        _ => return Err(format!("'{}' is not a commit", target).into()),
24    };
25
26    let mode = if soft {
27        "soft"
28    } else if hard {
29        "hard"
30    } else {
31        "mixed"
32    };
33
34    // Soft: move HEAD only
35    move_head(&repo_root, &commit_hash)?;
36
37    if mode != "soft" {
38        // Mixed + Hard: reset index to match commit's tree
39        reset_index_to_tree(&repo_root, &store, &commit.tree)?;
40    }
41
42    if mode == "hard" {
43        // Hard: also reset working tree
44        reset_working_tree(&repo_root, &store, &commit.tree)?;
45    }
46
47    Ok(ResetResponse {
48        target: commit_hash[..16.min(commit_hash.len())].to_string(),
49        mode: mode.to_string(),
50        message: format!(
51            "HEAD is now at {} {}",
52            &commit_hash[..16.min(commit_hash.len())],
53            commit.message
54        ),
55    })
56}
57
58pub fn execute_resolve(
59    repo_root: &std::path::Path,
60    target: &str,
61) -> Result<String, crate::errors::LitError> {
62    // Try HEAD~N syntax
63    if target.starts_with("HEAD~") || target.starts_with("HEAD^") {
64        let count: usize = target[5..].parse().unwrap_or(1);
65        let mut current = read_head(repo_root)?;
66        let store = ObjectStore::new(repo_root);
67
68        for _ in 0..count {
69            let hash = ObjectHash::from_hex(current);
70            let commit = match store.read(&hash)? {
71                Object::Commit(c) => c,
72                _ => return Err("Not a commit in history".into()),
73            };
74            current = commit
75                .parents
76                .first()
77                .ok_or("No parent commit")?
78                .to_string();
79        }
80        return Ok(current);
81    }
82
83    // Try HEAD
84    if target == "HEAD" {
85        return Ok(read_head(repo_root)?);
86    }
87
88    // Try as branch ref
89    if let Ok(hash) = crate::core::read_ref(repo_root, &format!("heads/{}", target)) {
90        return Ok(hash);
91    }
92
93    // Try as tag ref
94    if let Ok(hash) = crate::core::read_ref(repo_root, &format!("tags/{}", target)) {
95        return Ok(hash);
96    }
97
98    // Treat as raw hash
99    if target.len() >= 16 && target.chars().all(|c| c.is_ascii_hexdigit()) {
100        return Ok(target.to_string());
101    }
102
103    Err(format!("Cannot resolve '{}' to a commit", target).into())
104}
105
106fn move_head(
107    repo_root: &std::path::Path,
108    commit_hash: &str,
109) -> Result<(), crate::errors::LitError> {
110    match get_current_branch(repo_root) {
111        Ok(branch) => Ok(write_ref(
112            repo_root,
113            &format!("heads/{}", branch),
114            commit_hash,
115        )?),
116        Err(_) => Ok(set_head_detached(repo_root, commit_hash)?),
117    }
118}
119
120fn reset_index_to_tree(
121    repo_root: &std::path::Path,
122    store: &ObjectStore,
123    tree_hash: &ObjectHash,
124) -> Result<(), crate::errors::LitError> {
125    let tree = match store.read(tree_hash)? {
126        Object::Tree(t) => t,
127        _ => return Err("Not a tree".into()),
128    };
129
130    let mut index = Index::new();
131    for entry in &tree.entries {
132        index.add(
133            entry.name.clone(),
134            entry.hash.to_string(),
135            entry.mode.clone(),
136        );
137    }
138    index.save(repo_root).map_err(Into::into)
139}
140
141fn reset_working_tree(
142    repo_root: &std::path::Path,
143    store: &ObjectStore,
144    tree_hash: &ObjectHash,
145) -> Result<(), crate::errors::LitError> {
146    let tree = match store.read(tree_hash)? {
147        Object::Tree(t) => t,
148        _ => return Err("Not a tree".into()),
149    };
150
151    // Remove tracked files that aren't in the tree
152    // (simple approach: write all files from tree)
153    for entry in &tree.entries {
154        if entry.object_type == "blob" {
155            let blob = match store.read(&entry.hash)? {
156                Object::Blob(b) => b,
157                _ => continue,
158            };
159            let full_path = repo_root.join(&entry.name);
160            if let Some(parent) = full_path.parent() {
161                fs::create_dir_all(parent)
162                    .map_err(|e| format!("Failed to create directory: {}", e))?;
163            }
164            fs::write(&full_path, &blob.content)
165                .map_err(|e| format!("Failed to write file: {}", e))?;
166        }
167    }
168    Ok(())
169}