wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The baseline two machines start from.
//!
//! Convergence is three-way, so it needs a base: the state both sides last
//! agreed on. On the very first synchronisation there has been no agreement
//! yet — and without a base, every file that exists on both machines reads as
//! "created independently on both", which is a conflict for every file in the
//! repository.
//!
//! But there IS a state both sides have agreed on, and git holds it: they are
//! on the same branch at the same commit, which is the condition for
//! synchronising at all. The committed tree at that commit is, by definition,
//! a state both machines have and neither invented. Everything that differs
//! from it is the work in progress this feature exists to carry.
//!
//! So a fresh relationship seeds its baseline from HEAD. Both machines compute
//! it independently from the same commit and get the same answer, without
//! exchanging anything.
//!
//! # Why the contents are hashed rather than git's own ids used
//!
//! A git blob id is a SHA-1 over a header plus the content; continuity's
//! entries are a SHA-256 over the content alone. Reusing git's id would mean
//! two hash spaces that can never be compared, so the bytes are read and
//! hashed the way every other entry in this feature is.

use std::path::Path;

use super::converge::{hash_bytes, Baseline, Entry, EntryKind};
use super::scan;

/// The committed tree at HEAD, as a baseline.
///
/// An unborn branch — a repository with no commits — has an empty baseline,
/// which is the truthful answer: nothing has been agreed, and every file is
/// work in progress.
pub fn from_head(workdir: &Path) -> Result<Baseline, String> {
    let repo = crate::git_backend::open_repo(workdir)
        .map_err(|e| format!("cannot open {}: {e}", workdir.display()))?;
    let Ok(head) = repo.head_commit() else {
        return Ok(Baseline::new());
    };
    let tree = head
        .tree()
        .map_err(|e| format!("cannot read the commit's tree: {e}"))?;

    let mut baseline = Baseline::new();
    let mut walk = |relative: String, entry: gix::object::tree::EntryRef<'_, '_>| -> Result<(), String> {
        let mode = entry.mode();
        // A gitlink is the submodule's own business: the superproject records
        // a commit id, and the files under it belong to that repository's own
        // baseline.
        if mode.is_commit() {
            return Ok(());
        }
        if scan::is_refused(Path::new(&relative)) {
            return Ok(());
        }
        let object = entry
            .object()
            .map_err(|e| format!("cannot read {relative}: {e}"))?;
        let bytes = object.data.clone();
        let kind = if mode.is_link() {
            EntryKind::Symlink
        } else {
            EntryKind::File
        };
        // A symlink's identity is its target text, exactly as a scan of the
        // working tree computes it.
        let hash = hash_bytes(&bytes);
        baseline.insert(
            relative,
            Entry {
                kind,
                hash,
                size: bytes.len() as u64,
                executable: mode.is_executable(),
            },
        );
        Ok(())
    };

    let mut stack: Vec<(String, gix::Tree<'_>)> = vec![(String::new(), tree)];
    while let Some((prefix, tree)) = stack.pop() {
        for entry in tree.iter() {
            let entry = entry.map_err(|e| format!("cannot read a tree entry: {e}"))?;
            let name = entry.filename().to_string();
            let relative = if prefix.is_empty() {
                name.clone()
            } else {
                format!("{prefix}/{name}")
            };
            if entry.mode().is_tree() {
                if scan::is_refused(Path::new(&relative)) {
                    continue;
                }
                let object = entry
                    .object()
                    .map_err(|e| format!("cannot read {relative}: {e}"))?;
                let subtree = object
                    .try_into_tree()
                    .map_err(|e| format!("{relative} is not a tree: {e}"))?;
                stack.push((relative, subtree));
            } else {
                walk(relative, entry)?;
            }
        }
    }
    Ok(baseline)
}

#[cfg(test)]
#[path = "seed_tests.rs"]
mod tests;