// ============================================================================= // llimphi-voxel :: historia — versiones y alternativas de un proyecto // ----------------------------------------------------------------------------- // El DAG sobre el que se monta todo el trabajo creativo. Estilo git, alineado // al repo (BLAKE3 + DAG + postcard): // // * Cada SNAPSHOT del proyecto es un BLOB direccionado por su contenido // (`BLAKE3(postcard(Project))`). Dos snapshots idénticos comparten el mismo // hash → dedup automático, sin remapear ids (a diferencia de `combinar`). // * Una [`Version`] (commit) apunta a un snapshot y a su(s) PADRE(s). La // cadena de padres es la HISTORIA; dos versiones con el mismo padre son // ALTERNATIVAS. // * Las RAMAS son alternativas nombradas: una punta móvil de la historia. // // La [`Historia`] entera —almacén + versiones + ramas— serializa a un archivo // (postcard). No reescribe el modelo `Project`: los `u64` ids viven DENTRO del // snapshot; el direccionamiento por contenido es la capa de almacenamiento. // Determinista: el orden de las versiones lo da un contador, no el reloj. // ============================================================================= use std::collections::BTreeMap; use std::path::Path; use serde::{Deserialize, Serialize}; use crate::documento::ErrorDoc; use crate::studio::Project; /// Hash BLAKE3 de un contenido — la dirección de un blob o el id de una versión. pub type Hash = [u8; 32]; /// La rama por defecto. pub const RAMA_PRINCIPAL: &str = "principal"; fn hash(bytes: &[u8]) -> Hash { *blake3::hash(bytes).as_bytes() } /// Los primeros bytes de un hash en hex — para mostrarle una versión al usuario /// sin volcar 64 caracteres. pub fn hex_corto(h: &Hash) -> String { h[..4].iter().map(|b| format!("{b:02x}")).collect() } /// Una VERSIÓN (commit): un snapshot del proyecto atado a su historia. Se /// direcciona por su propio contenido ([`id`](Self::id)). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Version { /// Hash del snapshot del proyecto (contenido en el almacén). pub snapshot: Hash, /// Versiones anteriores (vacío = raíz; >1 = una fusión). pub padres: Vec, /// Etiqueta humana: "el nacimiento", "más bruma", "sin montañas"… pub etiqueta: String, /// Orden monótono de creación (sin reloj: determinista y reproducible). pub orden: u64, } impl Version { /// El id de la versión = hash de su contenido. Dos versiones idénticas /// (mismo snapshot, padres, etiqueta y orden) tienen el mismo id. pub fn id(&self) -> Hash { hash(&postcard::to_allocvec(self).expect("postcard version")) } } /// La HISTORIA: el almacén direccionado por contenido, el grafo de versiones y /// las ramas. Todo lo que el usuario crea se acumula aquí y se puede revisitar, /// ramificar y fusionar. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Historia { /// Almacén: hash → bytes. Snapshots de proyecto (y, a futuro, artefactos). contenidos: BTreeMap>, /// Versiones por su id. versiones: BTreeMap, /// Ramas (alternativas nombradas): nombre → id de su versión punta. ramas: BTreeMap, /// La rama sobre la que se commitea. rama_actual: String, /// La CABEZA (HEAD): la versión en la que estamos parados. Un `commit` cuelga /// de aquí; un `checkout` la mueve a cualquier versión (para revisitar o /// bifurcar desde el pasado). `None` = historia recién nacida. cabeza: Option, /// Siguiente número de orden (monótono). siguiente_orden: u64, } impl Default for Historia { fn default() -> Self { Self { contenidos: BTreeMap::new(), versiones: BTreeMap::new(), ramas: BTreeMap::new(), rama_actual: RAMA_PRINCIPAL.to_string(), cabeza: None, siguiente_orden: 0, } } } impl Historia { /// Una historia vacía, en la rama principal. pub fn nueva() -> Self { Self::default() } /// Guarda un contenido crudo y devuelve su hash. Idempotente: el mismo /// contenido no se almacena dos veces (dedup por contenido). fn guardar_contenido(&mut self, bytes: Vec) -> Hash { let h = hash(&bytes); self.contenidos.entry(h).or_insert(bytes); h } /// Recupera un contenido por su hash. fn contenido(&self, h: &Hash) -> Option<&[u8]> { self.contenidos.get(h).map(Vec::as_slice) } /// La CABEZA: la versión en la que estamos parados (`None` si aún no hay /// ninguna). Alias histórico [`punta`](Self::punta). pub fn actual(&self) -> Option { self.cabeza } /// La versión en la que estamos parados. Sinónimo de [`actual`](Self::actual). pub fn punta(&self) -> Option { self.cabeza } /// El nombre de la rama actual. pub fn rama_actual(&self) -> &str { &self.rama_actual } /// Mueve la CABEZA a `version_id` (checkout) y devuelve su proyecto. Es el /// "navegar": pararse en cualquier versión para revisitarla o bifurcar desde /// ahí. `None` si la versión no existe. No toca las ramas (queda "suelta" /// hasta el próximo `ramificar`/`cambiar_rama`). pub fn checkout(&mut self, version_id: &Hash) -> Option { let p = self.proyecto_de(version_id)?; self.cabeza = Some(*version_id); Some(p) } /// Todas las versiones ordenadas por creación (`orden`), con su id. Es la /// lista que la UI pinta como árbol/historial navegable. pub fn versiones_ordenadas(&self) -> Vec<(Hash, &Version)> { let mut v: Vec<(Hash, &Version)> = self.versiones.iter().map(|(h, ver)| (*h, ver)).collect(); v.sort_by_key(|(_, ver)| ver.orden); v } /// Registra `proyecto` como una versión nueva sobre la punta de la rama /// actual, con la `etiqueta` dada. Avanza la rama a la versión nueva. /// Devuelve su id. El snapshot se dedupea por contenido. pub fn commit(&mut self, proyecto: &Project, etiqueta: impl Into) -> Result { let bytes = postcard::to_allocvec(proyecto).map_err(|e| ErrorDoc::Serializar(e.to_string()))?; let snapshot = self.guardar_contenido(bytes); let padres = self.cabeza.into_iter().collect(); let orden = self.siguiente_orden; self.siguiente_orden += 1; let version = Version { snapshot, padres, etiqueta: etiqueta.into(), orden }; let id = version.id(); self.versiones.insert(id, version); // La cabeza avanza a la versión nueva, y la rama actual la sigue. self.cabeza = Some(id); self.ramas.insert(self.rama_actual.clone(), id); Ok(id) } /// El proyecto de una versión (reconstruye el snapshot). `None` si la versión /// o su snapshot no existen, o si el snapshot no decodifica. pub fn proyecto_de(&self, version_id: &Hash) -> Option { let v = self.versiones.get(version_id)?; let bytes = self.contenido(&v.snapshot)?; postcard::from_bytes(bytes).ok() } /// El proyecto de la CABEZA (donde estamos parados). pub fn proyecto_actual(&self) -> Option { self.proyecto_de(&self.cabeza?) } /// Una versión por su id. pub fn version(&self, id: &Hash) -> Option<&Version> { self.versiones.get(id) } /// Crea (o remueve) una rama `nombre` apuntando a la punta actual — una /// ALTERNATIVA que parte de aquí. No cambia la rama actual. pub fn ramificar(&mut self, nombre: impl Into) -> Result<(), ErrorDoc> { let cabeza = self.cabeza.ok_or(ErrorDoc::EscenaAusente(0))?; self.ramas.insert(nombre.into(), cabeza); Ok(()) } /// Cambia la rama actual a `nombre` y mueve la cabeza a su punta. `false` si /// la rama no existe. pub fn cambiar_rama(&mut self, nombre: &str) -> bool { if let Some(&punta) = self.ramas.get(nombre) { self.rama_actual = nombre.to_string(); self.cabeza = Some(punta); true } else { false } } /// Las ramas (alternativas nombradas): nombre → punta. pub fn ramas(&self) -> &BTreeMap { &self.ramas } /// La HISTORIA de una versión: ella y su linaje hacia atrás (primer padre), /// de la más nueva a la más vieja. La cadena de versiones. pub fn linaje(&self, version_id: &Hash) -> Vec { let mut out = Vec::new(); let mut actual = Some(*version_id); while let Some(id) = actual { if !self.versiones.contains_key(&id) || out.contains(&id) { break; } out.push(id); actual = self.versiones.get(&id).and_then(|v| v.padres.first().copied()); } out } /// Las ALTERNATIVAS de una versión: las otras versiones que comparten alguno /// de sus padres (sus hermanas en el DAG). Vacío para una versión sin padres /// o sin hermanas. pub fn alternativas(&self, version_id: &Hash) -> Vec { let Some(v) = self.versiones.get(version_id) else { return Vec::new(); }; let mut out = Vec::new(); for (id, otra) in &self.versiones { if id == version_id { continue; } if otra.padres.iter().any(|p| v.padres.contains(p)) { out.push(*id); } } out } /// Cuántas versiones acumula la historia. pub fn len(&self) -> usize { self.versiones.len() } /// ¿Sin ninguna versión todavía? pub fn is_empty(&self) -> bool { self.versiones.is_empty() } // --- Persistencia (postcard nativo) --- /// Serializa la historia entera al format nativo compacto. pub fn a_bytes(&self) -> Result, ErrorDoc> { postcard::to_allocvec(self).map_err(|e| ErrorDoc::Serializar(e.to_string())) } /// Reconstruye una historia desde el format nativo. pub fn de_bytes(bytes: &[u8]) -> Result { postcard::from_bytes(bytes).map_err(|e| ErrorDoc::Parsear(e.to_string())) } /// Guarda la historia en `path`. pub fn guardar(&self, path: impl AsRef) -> Result<(), ErrorDoc> { std::fs::write(path, self.a_bytes()?).map_err(|e| ErrorDoc::Io(e.to_string())) } /// Abre una historia desde `path`. pub fn abrir(path: impl AsRef) -> Result { let bytes = std::fs::read(path).map_err(|e| ErrorDoc::Io(e.to_string()))?; Self::de_bytes(&bytes) } }