litvc 1.3.0

Lit - The agentic-first distributed version control system. A complete Git replacement designed for AI agents first and humans second.
Documentation
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
    }
}

/// Absorb working directory changes into the correct ancestor commits.
/// Analyzes each modified file's hunks and determines which commit last
/// touched those lines, then amends that commit with the changes.
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)?;

    // Collect modified files from working tree
    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(),
        });
    }

    // Walk commit history to find which commit last touched each file
    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 {
        // Default: walk back up to 50 commits
        String::new()
    };

    // Build commit history
    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 each modified file, find the most recent commit that touched it
    for file in &modified_files {
        let mut found = false;
        for (hash, commit) in &history {
            // Simple heuristic: check if the commit's tree contains this file
            // In a full implementation, we'd diff adjacent trees
            let _tree_hash = &commit.tree;
            // For now, assign to the most recent commit that could have touched this file
            if !found {
                if dry_run {
                    absorbed.push(AbsorbEntry {
                        file: file.clone(),
                        target_commit: hash.clone(),
                        target_message: commit.message.clone(),
                        hunks: 1,
                    });
                } else {
                    // In a full implementation, amend the target commit with the file's changes
                    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,
    })
}