1use crate::core::{
2 find_repo_root, get_current_branch, read_head, write_ref, Commit, Object, ObjectHash, Tree,
3};
4use crate::response::RebaseResponse;
5use crate::storage::ObjectStore;
6use serde::{Deserialize, Serialize};
7use std::fs;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RebaseTodoEntry {
12 pub action: String,
13 pub hash: String,
14 pub short_hash: String,
15 pub message: String,
16}
17
18pub fn execute(
19 base: String,
20 interactive: bool,
21 onto: Option<String>,
22 abort: bool,
23 cont: bool,
24) -> Result<RebaseResponse, crate::errors::LitError> {
25 let repo_root = find_repo_root()?;
26
27 if abort {
28 return rebase_abort(&repo_root);
29 }
30
31 if cont {
32 return rebase_continue(&repo_root);
33 }
34
35 if interactive {
36 return rebase_interactive(&repo_root, &base);
37 }
38
39 rebase_noninteractive(&repo_root, &base, onto)
40}
41
42fn rebase_noninteractive(
43 repo_root: &std::path::Path,
44 base: &str,
45 onto: Option<String>,
46) -> Result<RebaseResponse, crate::errors::LitError> {
47 let store = ObjectStore::new(repo_root);
48
49 let base_hash = resolve_rev(repo_root, base)?;
50 let head_hash = read_head(repo_root)?;
51 let current_branch = get_current_branch(repo_root)?;
52
53 let onto_hash = match &onto {
54 Some(o) => resolve_rev(repo_root, o)?,
55 None => base_hash.clone(),
56 };
57
58 let commits_to_replay = collect_commits_since(&store, &head_hash, &base_hash)?;
60
61 if commits_to_replay.is_empty() {
62 return Ok(RebaseResponse {
63 rebased_commits: 0,
64 onto: onto_hash[..16.min(onto_hash.len())].to_string(),
65 branch: current_branch,
66 message: "Already up to date, nothing to rebase".to_string(),
67 todo: None,
68 });
69 }
70
71 save_rebase_state(repo_root, ¤t_branch, &head_hash, &onto_hash)?;
73
74 let mut current_parent = onto_hash.clone();
76 let mut replayed = 0;
77
78 for (_commit_hash, commit) in commits_to_replay.iter().rev() {
79 let new_parent_obj = ObjectHash::from_hex(current_parent.clone());
80
81 let new_tree = apply_commit_onto(&store, commit, &new_parent_obj)?;
83
84 let new_commit = Commit::new(
85 new_tree,
86 vec![new_parent_obj],
87 commit.author.clone(),
88 commit.message.clone(),
89 );
90
91 let new_hash = store.write(&Object::Commit(new_commit))?;
92 current_parent = new_hash.to_string();
93 replayed += 1;
94 }
95
96 write_ref(
98 repo_root,
99 &format!("heads/{}", current_branch),
100 ¤t_parent,
101 )?;
102
103 cleanup_rebase_state(repo_root)?;
105
106 Ok(RebaseResponse {
107 rebased_commits: replayed,
108 onto: onto_hash[..16.min(onto_hash.len())].to_string(),
109 branch: current_branch,
110 message: format!(
111 "Successfully rebased {} commit(s) onto {}",
112 replayed,
113 &onto_hash[..16.min(onto_hash.len())]
114 ),
115 todo: None,
116 })
117}
118
119fn rebase_interactive(
120 repo_root: &std::path::Path,
121 base: &str,
122) -> Result<RebaseResponse, crate::errors::LitError> {
123 let store = ObjectStore::new(repo_root);
124
125 let base_hash = resolve_rev(repo_root, base)?;
126 let head_hash = read_head(repo_root)?;
127 let current_branch = get_current_branch(repo_root)?;
128
129 let commits = collect_commits_since(&store, &head_hash, &base_hash)?;
130
131 if commits.is_empty() {
132 return Ok(RebaseResponse {
133 rebased_commits: 0,
134 onto: base_hash[..16.min(base_hash.len())].to_string(),
135 branch: current_branch,
136 message: "Nothing to rebase".to_string(),
137 todo: None,
138 });
139 }
140
141 let todo: Vec<RebaseTodoEntry> = commits
143 .iter()
144 .rev()
145 .map(|(hash, commit)| RebaseTodoEntry {
146 action: "pick".to_string(),
147 hash: hash.clone(),
148 short_hash: hash[..16.min(hash.len())].to_string(),
149 message: commit.message.clone(),
150 })
151 .collect();
152
153 save_rebase_state(repo_root, ¤t_branch, &head_hash, &base_hash)?;
155 let todo_path = repo_root.join(".lit").join("rebase").join("todo.json");
156 let todo_json = serde_json::to_string_pretty(&todo)
157 .map_err(|e| format!("Failed to serialize rebase todo: {}", e))?;
158 fs::write(&todo_path, &todo_json).map_err(|e| format!("Failed to write rebase todo: {}", e))?;
159
160 Ok(RebaseResponse {
161 rebased_commits: 0,
162 onto: base_hash[..16.min(base_hash.len())].to_string(),
163 branch: current_branch,
164 message: format!(
165 "Interactive rebase started with {} commit(s). Edit .lit/rebase/todo.json and run `lit rebase --continue`",
166 todo.len()
167 ),
168 todo: Some(serde_json::to_value(&todo).unwrap_or_default()),
169 })
170}
171
172fn rebase_continue(repo_root: &std::path::Path) -> Result<RebaseResponse, crate::errors::LitError> {
173 let rebase_dir = repo_root.join(".lit").join("rebase");
174 if !rebase_dir.exists() {
175 return Err("No rebase in progress".into());
176 }
177
178 let store = ObjectStore::new(repo_root);
179
180 let branch = fs::read_to_string(rebase_dir.join("branch"))
181 .map_err(|e| format!("Failed to read rebase state: {}", e))?
182 .trim()
183 .to_string();
184
185 let onto_hash = fs::read_to_string(rebase_dir.join("onto"))
186 .map_err(|e| format!("Failed to read rebase state: {}", e))?
187 .trim()
188 .to_string();
189
190 let todo_path = rebase_dir.join("todo.json");
191 let todo_json =
192 fs::read_to_string(&todo_path).map_err(|e| format!("Failed to read rebase todo: {}", e))?;
193 let todo: Vec<RebaseTodoEntry> = serde_json::from_str(&todo_json)
194 .map_err(|e| format!("Failed to parse rebase todo: {}", e))?;
195
196 let mut current_parent = onto_hash.clone();
197 let mut replayed = 0;
198
199 for entry in &todo {
200 match entry.action.as_str() {
201 "pick" | "p" => {
202 let hash = ObjectHash::from_hex(entry.hash.clone());
203 let commit = match store.read(&hash)? {
204 Object::Commit(c) => c,
205 _ => return Err(format!("'{}' is not a commit", entry.hash).into()),
206 };
207
208 let parent_obj = ObjectHash::from_hex(current_parent.clone());
209 let new_tree = apply_commit_onto(&store, &commit, &parent_obj)?;
210
211 let new_commit = Commit::new(
212 new_tree,
213 vec![parent_obj],
214 commit.author.clone(),
215 commit.message.clone(),
216 );
217
218 let new_hash = store.write(&Object::Commit(new_commit))?;
219 current_parent = new_hash.to_string();
220 replayed += 1;
221 }
222 "drop" | "d" => {
223 continue;
225 }
226 "reword" | "r" => {
227 let hash = ObjectHash::from_hex(entry.hash.clone());
228 let commit = match store.read(&hash)? {
229 Object::Commit(c) => c,
230 _ => return Err(format!("'{}' is not a commit", entry.hash).into()),
231 };
232
233 let parent_obj = ObjectHash::from_hex(current_parent.clone());
234 let new_tree = apply_commit_onto(&store, &commit, &parent_obj)?;
235
236 let new_commit = Commit::new(
238 new_tree,
239 vec![parent_obj],
240 commit.author.clone(),
241 entry.message.clone(),
242 );
243
244 let new_hash = store.write(&Object::Commit(new_commit))?;
245 current_parent = new_hash.to_string();
246 replayed += 1;
247 }
248 other => {
249 return Err(format!("Unknown rebase action: '{}'", other).into());
250 }
251 }
252 }
253
254 write_ref(repo_root, &format!("heads/{}", branch), ¤t_parent)?;
255
256 cleanup_rebase_state(repo_root)?;
257
258 Ok(RebaseResponse {
259 rebased_commits: replayed,
260 onto: onto_hash[..16.min(onto_hash.len())].to_string(),
261 branch,
262 message: format!("Successfully rebased {} commit(s)", replayed),
263 todo: None,
264 })
265}
266
267fn rebase_abort(repo_root: &std::path::Path) -> Result<RebaseResponse, crate::errors::LitError> {
268 let rebase_dir = repo_root.join(".lit").join("rebase");
269 if !rebase_dir.exists() {
270 return Err("No rebase in progress".into());
271 }
272
273 let branch = fs::read_to_string(rebase_dir.join("branch"))
274 .map_err(|e| format!("Failed to read rebase state: {}", e))?
275 .trim()
276 .to_string();
277
278 let orig_head = fs::read_to_string(rebase_dir.join("orig_head"))
279 .map_err(|e| format!("Failed to read rebase state: {}", e))?
280 .trim()
281 .to_string();
282
283 write_ref(repo_root, &format!("heads/{}", branch), &orig_head)?;
285
286 cleanup_rebase_state(repo_root)?;
287
288 Ok(RebaseResponse {
289 rebased_commits: 0,
290 onto: String::new(),
291 branch,
292 message: "Rebase aborted, HEAD restored to original position".to_string(),
293 todo: None,
294 })
295}
296
297fn collect_commits_since(
298 store: &ObjectStore,
299 head: &str,
300 base: &str,
301) -> Result<Vec<(String, Commit)>, String> {
302 let mut commits = Vec::new();
303 let mut current = head.to_string();
304
305 loop {
306 if current == base {
307 break;
308 }
309
310 let hash = ObjectHash::from_hex(current.clone());
311 let commit = match store.read(&hash)? {
312 Object::Commit(c) => c,
313 _ => return Err("Not a commit in history".into()),
314 };
315
316 let parent = commit.parents.first().map(|p| p.to_string());
317 commits.push((current, commit));
318
319 match parent {
320 Some(p) => current = p,
321 None => break,
322 }
323 }
324
325 Ok(commits)
326}
327
328fn apply_commit_onto(
329 store: &ObjectStore,
330 commit: &Commit,
331 new_parent: &ObjectHash,
332) -> Result<ObjectHash, crate::errors::LitError> {
333 let parent_commit = match store.read(new_parent)? {
335 Object::Commit(c) => c,
336 _ => return Err("Parent is not a commit".into()),
337 };
338
339 let orig_parent_tree = if let Some(orig_parent) = commit.parents.first() {
341 match store.read(orig_parent)? {
342 Object::Commit(c) => load_tree_files(store, &c.tree)?,
343 _ => std::collections::HashMap::new(),
344 }
345 } else {
346 std::collections::HashMap::new()
347 };
348
349 let commit_tree_files = load_tree_files(store, &commit.tree)?;
350 let new_base_files = load_tree_files(store, &parent_commit.tree)?;
351
352 let mut result_tree = Tree::new();
353
354 for (path, (hash, mode)) in &new_base_files {
356 let in_orig = orig_parent_tree.get(path);
357 let in_commit = commit_tree_files.get(path);
358
359 match (in_orig, in_commit) {
360 (Some(ov), Some(cv)) if ov.0 != cv.0 => {
362 result_tree.add_entry(
363 cv.1.clone(),
364 path.clone(),
365 ObjectHash::from_hex(cv.0.clone()),
366 "blob".to_string(),
367 );
368 }
369 (Some(_), None) => continue,
371 _ => {
373 result_tree.add_entry(
374 mode.clone(),
375 path.clone(),
376 ObjectHash::from_hex(hash.clone()),
377 "blob".to_string(),
378 );
379 }
380 }
381 }
382
383 for (path, (hash, mode)) in &commit_tree_files {
385 if !orig_parent_tree.contains_key(path) && !new_base_files.contains_key(path) {
386 result_tree.add_entry(
387 mode.clone(),
388 path.clone(),
389 ObjectHash::from_hex(hash.clone()),
390 "blob".to_string(),
391 );
392 }
393 }
394
395 store.write(&Object::Tree(result_tree)).map_err(Into::into)
396}
397
398fn load_tree_files(
399 store: &ObjectStore,
400 tree_hash: &ObjectHash,
401) -> Result<std::collections::HashMap<String, (String, String)>, crate::errors::LitError> {
402 let tree = match store.read(tree_hash)? {
403 Object::Tree(t) => t,
404 _ => return Err("Not a tree".into()),
405 };
406 let mut files = std::collections::HashMap::new();
407 for entry in &tree.entries {
408 files.insert(
409 entry.name.clone(),
410 (entry.hash.to_string(), entry.mode.clone()),
411 );
412 }
413 Ok(files)
414}
415
416fn save_rebase_state(
417 repo_root: &std::path::Path,
418 branch: &str,
419 orig_head: &str,
420 onto: &str,
421) -> Result<(), crate::errors::LitError> {
422 let rebase_dir = repo_root.join(".lit").join("rebase");
423 fs::create_dir_all(&rebase_dir)
424 .map_err(|e| format!("Failed to create rebase directory: {}", e))?;
425 fs::write(rebase_dir.join("branch"), branch)
426 .map_err(|e| format!("Failed to save rebase state: {}", e))?;
427 fs::write(rebase_dir.join("orig_head"), orig_head)
428 .map_err(|e| format!("Failed to save rebase state: {}", e))?;
429 fs::write(rebase_dir.join("onto"), onto)
430 .map_err(|e| format!("Failed to save rebase state: {}", e))?;
431 Ok(())
432}
433
434fn cleanup_rebase_state(repo_root: &std::path::Path) -> Result<(), crate::errors::LitError> {
435 let rebase_dir = repo_root.join(".lit").join("rebase");
436 if rebase_dir.exists() {
437 fs::remove_dir_all(&rebase_dir)
438 .map_err(|e| format!("Failed to clean up rebase state: {}", e))?;
439 }
440 Ok(())
441}
442
443fn resolve_rev(
444 repo_root: &std::path::Path,
445 target: &str,
446) -> Result<String, crate::errors::LitError> {
447 if target.starts_with("HEAD~") || target.starts_with("HEAD^") {
448 let count: usize = target[5..].parse().unwrap_or(1);
449 let mut current = read_head(repo_root)?;
450 let store = ObjectStore::new(repo_root);
451 for _ in 0..count {
452 let hash = ObjectHash::from_hex(current);
453 let commit = match store.read(&hash)? {
454 Object::Commit(c) => c,
455 _ => return Err("Not a commit in history".into()),
456 };
457 current = commit
458 .parents
459 .first()
460 .ok_or("No parent commit")?
461 .to_string();
462 }
463 return Ok(current);
464 }
465 if target == "HEAD" {
466 return Ok(read_head(repo_root)?);
467 }
468 if let Ok(hash) = crate::core::read_ref(repo_root, &format!("heads/{}", target)) {
469 return Ok(hash);
470 }
471 if let Ok(hash) = crate::core::read_ref(repo_root, &format!("tags/{}", target)) {
472 return Ok(hash);
473 }
474 if target.len() >= 16 && target.chars().all(|c| c.is_ascii_hexdigit()) {
475 return Ok(target.to_string());
476 }
477 Err(format!("Cannot resolve '{}' to a commit", target).into())
478}