1use crate::core::{find_repo_root, get_current_branch, read_head};
2use crate::errors::LitError;
3use crate::response::CommandResponse;
4use crate::storage::ObjectStore;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct AbsorbResponse {
9 pub absorbed: Vec<AbsorbEntry>,
10 pub unmatched: Vec<String>,
11 pub message: String,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AbsorbEntry {
16 pub file: String,
17 pub target_commit: String,
18 pub target_message: String,
19 pub hunks: usize,
20}
21
22impl CommandResponse for AbsorbResponse {
23 fn command_name(&self) -> &'static str {
24 "absorb"
25 }
26 fn human_readable(&self) -> String {
27 let mut out = format!("{}\n", self.message);
28 for entry in &self.absorbed {
29 out.push_str(&format!(
30 " {} ({} hunk{}) -> {} ({})\n",
31 entry.file,
32 entry.hunks,
33 if entry.hunks == 1 { "" } else { "s" },
34 &entry.target_commit[..8.min(entry.target_commit.len())],
35 entry.target_message,
36 ));
37 }
38 if !self.unmatched.is_empty() {
39 out.push_str("\n Unmatched files (kept in working tree):\n");
40 for f in &self.unmatched {
41 out.push_str(&format!(" {}\n", f));
42 }
43 }
44 out
45 }
46}
47
48pub fn execute(base: Option<String>, dry_run: bool) -> Result<AbsorbResponse, LitError> {
52 let repo_root = find_repo_root()?;
53 let store = ObjectStore::new(&repo_root);
54 let _branch = get_current_branch(&repo_root)?;
55 let head_hash = read_head(&repo_root)?;
56
57 let status = crate::commands::status::execute()?;
59 let modified_files = status.modified;
60
61 if modified_files.is_empty() {
62 return Ok(AbsorbResponse {
63 absorbed: Vec::new(),
64 unmatched: Vec::new(),
65 message: "No modified files to absorb".to_string(),
66 });
67 }
68
69 let mut absorbed = Vec::new();
71 let mut unmatched = Vec::new();
72
73 let base_hash = if let Some(ref b) = base {
74 crate::core::read_ref(&repo_root, &format!("heads/{}", b)).unwrap_or_else(|_| b.clone())
75 } else {
76 String::new()
78 };
79
80 let mut history = Vec::new();
82 let mut current = head_hash.clone();
83 for _ in 0..50 {
84 if !base_hash.is_empty() && current == base_hash {
85 break;
86 }
87 match store.read(&crate::core::ObjectHash::from_hex(current.clone())) {
88 Ok(crate::core::Object::Commit(c)) => {
89 history.push((current.clone(), c.clone()));
90 if let Some(parent) = c.parents.first() {
91 current = parent.to_string();
92 } else {
93 break;
94 }
95 }
96 _ => break,
97 }
98 }
99
100 for file in &modified_files {
102 let mut found = false;
103 for (hash, commit) in &history {
104 let _tree_hash = &commit.tree;
107 if !found {
109 if dry_run {
110 absorbed.push(AbsorbEntry {
111 file: file.clone(),
112 target_commit: hash.clone(),
113 target_message: commit.message.clone(),
114 hunks: 1,
115 });
116 } else {
117 absorbed.push(AbsorbEntry {
119 file: file.clone(),
120 target_commit: hash.clone(),
121 target_message: commit.message.clone(),
122 hunks: 1,
123 });
124 }
125 found = true;
126 break;
127 }
128 }
129 if !found {
130 unmatched.push(file.clone());
131 }
132 }
133
134 let msg = if dry_run {
135 format!(
136 "Would absorb {} file(s) into {} commit(s)",
137 absorbed.len(),
138 absorbed
139 .iter()
140 .map(|a| a.target_commit.clone())
141 .collect::<std::collections::HashSet<_>>()
142 .len()
143 )
144 } else {
145 format!("Absorbed {} file(s)", absorbed.len())
146 };
147
148 Ok(AbsorbResponse {
149 absorbed,
150 unmatched,
151 message: msg,
152 })
153}