1use crate::core::diff::{collect_tree_files, diff_blobs, diff_trees, DiffStat, FileDiff};
2use crate::core::{find_repo_root, Object, ObjectHash};
3use crate::response::DiffResponse;
4use crate::storage::{Index, ObjectStore};
5use std::collections::HashMap;
6use std::fs;
7
8pub fn execute(
15 staged: bool,
16 stat: bool,
17 word_diff: bool,
18 ref1: Option<String>,
19 ref2: Option<String>,
20) -> Result<DiffResponse, crate::errors::LitError> {
21 let repo_root = find_repo_root()?;
22 let store = ObjectStore::new(&repo_root);
23
24 let file_diffs = if let Some(r1) = ref1 {
25 let r2 = ref2.unwrap_or_else(|| "HEAD".to_string());
26 diff_refs(&repo_root, &store, &r1, &r2)?
28 } else if staged {
29 diff_staged(&repo_root, &store)?
31 } else {
32 diff_working(&repo_root, &store)?
34 };
35
36 let stats: Vec<DiffStat> = file_diffs
37 .iter()
38 .map(|d| DiffStat {
39 path: d.path.clone(),
40 additions: d.additions,
41 deletions: d.deletions,
42 status: d.status,
43 })
44 .collect();
45
46 let total_additions: usize = stats.iter().map(|s| s.additions).sum();
47 let total_deletions: usize = stats.iter().map(|s| s.deletions).sum();
48 let files_changed = file_diffs.len();
49
50 Ok(DiffResponse {
51 files: file_diffs,
52 stats,
53 stat_only: stat,
54 word_diff,
55 files_changed,
56 total_additions,
57 total_deletions,
58 })
59}
60
61fn diff_working(
63 repo_root: &std::path::Path,
64 _store: &ObjectStore,
65) -> Result<Vec<FileDiff>, crate::errors::LitError> {
66 let index = Index::load(repo_root)?;
67 let mut diffs = Vec::new();
68
69 for (path, entry) in &index.entries {
70 let file_path = repo_root.join(path);
71
72 if !file_path.exists() {
73 let old_content = read_blob_by_hash_str(_store, &entry.hash)?;
75 diffs.push(diff_blobs(
76 path,
77 Some(&old_content),
78 None,
79 Some(entry.hash.clone()),
80 None,
81 ));
82 continue;
83 }
84
85 let current_content =
86 fs::read(&file_path).map_err(|e| format!("Failed to read {}: {}", path, e))?;
87
88 let old_content = read_blob_by_hash_str(_store, &entry.hash)?;
89
90 if current_content != old_content {
91 diffs.push(diff_blobs(
92 path,
93 Some(&old_content),
94 Some(¤t_content),
95 Some(entry.hash.clone()),
96 None,
97 ));
98 }
99 }
100
101 diffs.sort_by(|a, b| a.path.cmp(&b.path));
102 Ok(diffs)
103}
104
105fn diff_staged(
107 repo_root: &std::path::Path,
108 store: &ObjectStore,
109) -> Result<Vec<FileDiff>, crate::errors::LitError> {
110 let index = Index::load(repo_root)?;
111
112 let head_files = get_head_tree_files(repo_root, store).unwrap_or_default();
114
115 let mut diffs = Vec::new();
116
117 for (path, entry) in &index.entries {
119 let new_content = read_blob_by_hash_str(store, &entry.hash)?;
120
121 if let Some(old_hash) = head_files.get(path) {
122 let old_hash_str = old_hash.to_string();
124 if entry.hash != old_hash_str {
125 let old_content = read_blob_by_hash_str(store, &old_hash_str)?;
126 diffs.push(diff_blobs(
127 path,
128 Some(&old_content),
129 Some(&new_content),
130 Some(old_hash_str),
131 Some(entry.hash.clone()),
132 ));
133 }
134 } else {
135 diffs.push(diff_blobs(
137 path,
138 None,
139 Some(&new_content),
140 None,
141 Some(entry.hash.clone()),
142 ));
143 }
144 }
145
146 diffs.sort_by(|a, b| a.path.cmp(&b.path));
150 Ok(diffs)
151}
152
153fn diff_refs(
155 repo_root: &std::path::Path,
156 store: &ObjectStore,
157 ref1: &str,
158 ref2: &str,
159) -> Result<Vec<FileDiff>, crate::errors::LitError> {
160 let hash1 = resolve_ref(repo_root, ref1)?;
161 let hash2 = resolve_ref(repo_root, ref2)?;
162
163 let tree1 = get_commit_tree(store, &hash1)?;
164 let tree2 = get_commit_tree(store, &hash2)?;
165
166 diff_trees(&tree1, &tree2, store).map_err(Into::into)
167}
168
169fn resolve_ref(
171 repo_root: &std::path::Path,
172 reference: &str,
173) -> Result<ObjectHash, crate::errors::LitError> {
174 if reference == "HEAD" {
175 let head = crate::core::read_head(repo_root)?;
176 return Ok(ObjectHash::from_hex(head));
177 }
178
179 if let Ok(hash) = crate::core::read_ref(repo_root, &format!("heads/{}", reference)) {
181 return Ok(ObjectHash::from_hex(hash));
182 }
183
184 if let Ok(hash) = crate::core::read_ref(repo_root, &format!("tags/{}", reference)) {
186 return Ok(ObjectHash::from_hex(hash));
187 }
188
189 Ok(ObjectHash::from_hex(reference.to_string()))
191}
192
193fn get_commit_tree(
195 store: &ObjectStore,
196 commit_hash: &ObjectHash,
197) -> Result<crate::core::Tree, crate::errors::LitError> {
198 let commit = match store.read(commit_hash)? {
199 Object::Commit(c) => c,
200 _ => return Err(format!("Expected commit object for {}", commit_hash).into()),
201 };
202
203 match store.read(&commit.tree)? {
204 Object::Tree(t) => Ok(t),
205 _ => Err(format!("Expected tree object for {}", commit.tree).into()),
206 }
207}
208
209fn get_head_tree_files(
211 repo_root: &std::path::Path,
212 store: &ObjectStore,
213) -> Result<HashMap<String, ObjectHash>, crate::errors::LitError> {
214 let head_hash = crate::core::read_head(repo_root)?;
215 let commit_hash = ObjectHash::from_hex(head_hash);
216 let tree = get_commit_tree(store, &commit_hash)?;
217 let files = collect_tree_files(&tree, store, "")
218 .map_err(|e: String| -> crate::errors::LitError { e.into() })?;
219 Ok(files.into_iter().collect())
220}
221
222fn read_blob_by_hash_str(
224 store: &ObjectStore,
225 hash: &str,
226) -> Result<Vec<u8>, crate::errors::LitError> {
227 let obj_hash = ObjectHash::from_hex(hash.to_string());
228 match store.read(&obj_hash)? {
229 Object::Blob(b) => Ok(b.content),
230 _ => Err(format!("Expected blob object for hash {}", hash).into()),
231 }
232}