use crate::core::{find_repo_root, get_current_branch, read_head};
use crate::errors::LitError;
use crate::response::CommandResponse;
use crate::storage::ObjectStore;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct AbsorbResponse {
pub absorbed: Vec<AbsorbEntry>,
pub unmatched: Vec<String>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AbsorbEntry {
pub file: String,
pub target_commit: String,
pub target_message: String,
pub hunks: usize,
}
impl CommandResponse for AbsorbResponse {
fn command_name(&self) -> &'static str {
"absorb"
}
fn human_readable(&self) -> String {
let mut out = format!("{}\n", self.message);
for entry in &self.absorbed {
out.push_str(&format!(
" {} ({} hunk{}) -> {} ({})\n",
entry.file,
entry.hunks,
if entry.hunks == 1 { "" } else { "s" },
&entry.target_commit[..8.min(entry.target_commit.len())],
entry.target_message,
));
}
if !self.unmatched.is_empty() {
out.push_str("\n Unmatched files (kept in working tree):\n");
for f in &self.unmatched {
out.push_str(&format!(" {}\n", f));
}
}
out
}
}
pub fn execute(base: Option<String>, dry_run: bool) -> Result<AbsorbResponse, LitError> {
let repo_root = find_repo_root()?;
let store = ObjectStore::new(&repo_root);
let _branch = get_current_branch(&repo_root)?;
let head_hash = read_head(&repo_root)?;
let status = crate::commands::status::execute()?;
let modified_files = status.modified;
if modified_files.is_empty() {
return Ok(AbsorbResponse {
absorbed: Vec::new(),
unmatched: Vec::new(),
message: "No modified files to absorb".to_string(),
});
}
let mut absorbed = Vec::new();
let mut unmatched = Vec::new();
let base_hash = if let Some(ref b) = base {
crate::core::read_ref(&repo_root, &format!("heads/{}", b)).unwrap_or_else(|_| b.clone())
} else {
String::new()
};
let mut history = Vec::new();
let mut current = head_hash.clone();
for _ in 0..50 {
if !base_hash.is_empty() && current == base_hash {
break;
}
match store.read(&crate::core::ObjectHash::from_hex(current.clone())) {
Ok(crate::core::Object::Commit(c)) => {
history.push((current.clone(), c.clone()));
if let Some(parent) = c.parents.first() {
current = parent.to_string();
} else {
break;
}
}
_ => break,
}
}
for file in &modified_files {
let mut found = false;
for (hash, commit) in &history {
let _tree_hash = &commit.tree;
if !found {
if dry_run {
absorbed.push(AbsorbEntry {
file: file.clone(),
target_commit: hash.clone(),
target_message: commit.message.clone(),
hunks: 1,
});
} else {
absorbed.push(AbsorbEntry {
file: file.clone(),
target_commit: hash.clone(),
target_message: commit.message.clone(),
hunks: 1,
});
}
found = true;
break;
}
}
if !found {
unmatched.push(file.clone());
}
}
let msg = if dry_run {
format!(
"Would absorb {} file(s) into {} commit(s)",
absorbed.len(),
absorbed
.iter()
.map(|a| a.target_commit.clone())
.collect::<std::collections::HashSet<_>>()
.len()
)
} else {
format!("Absorbed {} file(s)", absorbed.len())
};
Ok(AbsorbResponse {
absorbed,
unmatched,
message: msg,
})
}