use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use super::types::{BranchError, Commit, FileChange};
pub struct CommitBuilder {
parent: Option<[u8; 32]>,
txg: u64,
message: String,
author: String,
timestamp: u64,
changes: Vec<FileChange>,
}
impl CommitBuilder {
pub fn new(txg: u64) -> Self {
Self {
parent: None,
txg,
message: String::new(),
author: String::new(),
timestamp: 0,
changes: Vec::new(),
}
}
pub fn parent(mut self, hash: [u8; 32]) -> Self {
self.parent = Some(hash);
self
}
pub fn message(mut self, msg: impl Into<String>) -> Self {
self.message = msg.into();
self
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = author.into();
self
}
pub fn timestamp(mut self, ts: u64) -> Self {
self.timestamp = ts;
self
}
pub fn change(mut self, change: FileChange) -> Self {
self.changes.push(change);
self
}
pub fn changes(mut self, changes: Vec<FileChange>) -> Self {
self.changes.extend(changes);
self
}
pub fn build(self) -> Commit {
let hash = compute_commit_hash(
self.parent.as_ref(),
self.txg,
&self.message,
&self.author,
&self.changes,
);
Commit {
hash,
parent: self.parent,
txg: self.txg,
message: self.message,
author: self.author,
timestamp: self.timestamp,
changes: self.changes,
}
}
}
fn compute_commit_hash(
parent: Option<&[u8; 32]>,
txg: u64,
message: &str,
author: &str,
changes: &[FileChange],
) -> [u8; 32] {
let mut data = Vec::new();
match parent {
Some(p) => data.extend_from_slice(p),
None => data.extend_from_slice(&[0u8; 32]),
}
data.extend_from_slice(&txg.to_le_bytes());
let msg_bytes = message.as_bytes();
data.extend_from_slice(&(msg_bytes.len() as u32).to_le_bytes());
data.extend_from_slice(msg_bytes);
let author_bytes = author.as_bytes();
data.extend_from_slice(&(author_bytes.len() as u32).to_le_bytes());
data.extend_from_slice(author_bytes);
data.extend_from_slice(&(changes.len() as u32).to_le_bytes());
for change in changes {
let path_bytes = change.path.as_bytes();
data.extend_from_slice(&(path_bytes.len() as u32).to_le_bytes());
data.extend_from_slice(path_bytes);
let change_type_byte = match &change.change_type {
super::types::ChangeType::Created => 0u8,
super::types::ChangeType::Modified => 1u8,
super::types::ChangeType::Deleted => 2u8,
super::types::ChangeType::Renamed { .. } => 3u8,
};
data.push(change_type_byte);
if let Some(cksum) = change.old_checksum {
data.push(1);
for v in cksum {
data.extend_from_slice(&v.to_le_bytes());
}
} else {
data.push(0);
}
if let Some(cksum) = change.new_checksum {
data.push(1);
for v in cksum {
data.extend_from_slice(&v.to_le_bytes());
}
} else {
data.push(0);
}
}
blake3_hash(&data)
}
fn blake3_hash(data: &[u8]) -> [u8; 32] {
#[cfg(feature = "blake3")]
{
let hash = blake3::hash(data);
*hash.as_bytes()
}
#[cfg(not(feature = "blake3"))]
{
let mut state = [0u8; 32];
for (i, byte) in state.iter_mut().enumerate() {
*byte = (i as u8).wrapping_mul(0x9E).wrapping_add(0x3B);
}
for (i, &byte) in data.iter().enumerate() {
let idx = i % 32;
state[idx] = state[idx]
.wrapping_add(byte)
.wrapping_mul(0x9E)
.rotate_left(5);
let next_idx = (idx + 1) % 32;
state[next_idx] ^= state[idx];
}
for _ in 0..4 {
for i in 0..32 {
let prev = state[(i + 31) % 32];
let next = state[(i + 1) % 32];
state[i] = state[i].wrapping_add(prev ^ next).rotate_left(3);
}
}
state
}
}
#[derive(Debug, Clone)]
pub struct CommitStore {
commits: BTreeMap<[u8; 32], Commit>,
heads: BTreeMap<String, [u8; 32]>,
txg_commits: BTreeMap<u64, [u8; 32]>,
}
impl CommitStore {
pub fn new() -> Self {
Self {
commits: BTreeMap::new(),
heads: BTreeMap::new(),
txg_commits: BTreeMap::new(),
}
}
pub fn len(&self) -> usize {
self.commits.len()
}
pub fn is_empty(&self) -> bool {
self.commits.is_empty()
}
pub fn add_commit(&mut self, commit: Commit) {
let hash = commit.hash;
let txg = commit.txg;
self.txg_commits.insert(txg, hash);
self.commits.insert(hash, commit);
}
pub fn add_commit_to_branch(&mut self, commit: Commit, branch: &str) {
let hash = commit.hash;
self.add_commit(commit);
self.heads.insert(branch.to_string(), hash);
}
pub fn get(&self, hash: &[u8; 32]) -> Option<&Commit> {
self.commits.get(hash)
}
pub fn get_by_short_hash(&self, short: &str) -> Option<&Commit> {
let short_lower = short.to_lowercase();
for (hash, commit) in &self.commits {
let hash_hex = commit.hash_hex();
if hash_hex.starts_with(&short_lower) {
return Some(commit);
}
}
None
}
pub fn get_by_txg(&self, txg: u64) -> Option<&Commit> {
self.txg_commits.get(&txg).and_then(|h| self.commits.get(h))
}
pub fn get_head(&self, branch: &str) -> Option<&Commit> {
self.heads.get(branch).and_then(|h| self.commits.get(h))
}
pub fn get_head_hash(&self, branch: &str) -> Option<[u8; 32]> {
self.heads.get(branch).copied()
}
pub fn set_head(&mut self, branch: &str, hash: [u8; 32]) {
self.heads.insert(branch.to_string(), hash);
}
pub fn contains(&self, hash: &[u8; 32]) -> bool {
self.commits.contains_key(hash)
}
pub fn get_parent(&self, commit: &Commit) -> Option<&Commit> {
commit.parent.as_ref().and_then(|h| self.commits.get(h))
}
pub fn ancestry(&self, hash: &[u8; 32], max_depth: Option<usize>) -> Vec<&Commit> {
let mut result = Vec::new();
let mut current = hash;
let max = max_depth.unwrap_or(usize::MAX);
while let Some(commit) = self.commits.get(current) {
result.push(commit);
if result.len() >= max {
break;
}
match &commit.parent {
Some(parent) => current = parent,
None => break,
}
}
result
}
pub fn common_ancestor(&self, hash_a: &[u8; 32], hash_b: &[u8; 32]) -> Option<&Commit> {
let ancestry_a: Vec<[u8; 32]> =
self.ancestry(hash_a, None).iter().map(|c| c.hash).collect();
let mut current = hash_b;
while let Some(commit) = self.commits.get(current) {
if ancestry_a.contains(&commit.hash) {
return Some(commit);
}
match &commit.parent {
Some(parent) => current = parent,
None => break,
}
}
None
}
pub fn range(&self, start: Option<&[u8; 32]>, end: &[u8; 32]) -> Vec<&Commit> {
let mut result = Vec::new();
let mut current = end;
while let Some(commit) = self.commits.get(current) {
if start.is_some() && start == Some(current) {
break;
}
result.push(commit);
match &commit.parent {
Some(parent) => current = parent,
None => break,
}
}
result.reverse();
result
}
pub fn iter(&self) -> impl Iterator<Item = &Commit> {
self.commits.values()
}
pub fn branch_commits(&self, branch: &str, limit: Option<usize>) -> Vec<&Commit> {
match self.heads.get(branch) {
Some(head) => self.ancestry(head, limit),
None => Vec::new(),
}
}
}
impl Default for CommitStore {
fn default() -> Self {
Self::new()
}
}
pub struct CommitValidator;
impl CommitValidator {
pub fn verify_hash(commit: &Commit) -> bool {
let computed = compute_commit_hash(
commit.parent.as_ref(),
commit.txg,
&commit.message,
&commit.author,
&commit.changes,
);
computed == commit.hash
}
pub fn validate(commit: &Commit) -> Result<(), BranchError> {
if !Self::verify_hash(commit) {
return Err(BranchError::Internal("commit hash mismatch".into()));
}
if commit.message.is_empty() {
return Err(BranchError::Internal(
"commit message cannot be empty".into(),
));
}
if commit.author.is_empty() {
return Err(BranchError::Internal(
"commit author cannot be empty".into(),
));
}
Ok(())
}
pub fn validate_chain(commits: &[&Commit]) -> Result<(), BranchError> {
for (i, commit) in commits.iter().enumerate() {
Self::validate(commit)?;
if i + 1 < commits.len() {
let expected_parent = commits[i + 1].hash;
match commit.parent {
Some(parent) if parent == expected_parent => {}
Some(_) => {
return Err(BranchError::Internal("broken commit chain".into()));
}
None => {
return Err(BranchError::Internal("missing parent reference".into()));
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::branch::types::ChangeType;
#[test]
fn test_commit_builder() {
let commit = CommitBuilder::new(100)
.message("Initial commit")
.author("test@example.com")
.timestamp(1704067200)
.change(FileChange::created("/file.txt".into(), [1; 4], 100))
.build();
assert_eq!(commit.txg, 100);
assert_eq!(commit.message, "Initial commit");
assert_eq!(commit.author, "test@example.com");
assert!(commit.parent.is_none());
assert_eq!(commit.changes.len(), 1);
}
#[test]
fn test_commit_hash_deterministic() {
let commit1 = CommitBuilder::new(100)
.message("Test")
.author("test")
.timestamp(1000)
.build();
let commit2 = CommitBuilder::new(100)
.message("Test")
.author("test")
.timestamp(1000)
.build();
assert_eq!(commit1.hash, commit2.hash);
}
#[test]
fn test_commit_hash_changes_with_input() {
let commit1 = CommitBuilder::new(100)
.message("Test A")
.author("test")
.timestamp(1000)
.build();
let commit2 = CommitBuilder::new(100)
.message("Test B")
.author("test")
.timestamp(1000)
.build();
assert_ne!(commit1.hash, commit2.hash);
}
#[test]
fn test_commit_with_parent() {
let parent = CommitBuilder::new(100)
.message("Parent")
.author("test")
.timestamp(1000)
.build();
let child = CommitBuilder::new(101)
.parent(parent.hash)
.message("Child")
.author("test")
.timestamp(2000)
.build();
assert_eq!(child.parent, Some(parent.hash));
}
#[test]
fn test_commit_store() {
let mut store = CommitStore::new();
let commit = CommitBuilder::new(100)
.message("Test")
.author("test")
.timestamp(1000)
.build();
let hash = commit.hash;
store.add_commit_to_branch(commit, "main");
assert!(store.contains(&hash));
assert_eq!(store.len(), 1);
assert_eq!(store.get_head("main").unwrap().hash, hash);
}
#[test]
fn test_ancestry() {
let mut store = CommitStore::new();
let c1 = CommitBuilder::new(100)
.message("First")
.author("test")
.timestamp(1000)
.build();
let c2 = CommitBuilder::new(101)
.parent(c1.hash)
.message("Second")
.author("test")
.timestamp(2000)
.build();
let c3 = CommitBuilder::new(102)
.parent(c2.hash)
.message("Third")
.author("test")
.timestamp(3000)
.build();
let c3_hash = c3.hash;
store.add_commit(c1);
store.add_commit(c2);
store.add_commit_to_branch(c3, "main");
let ancestry = store.ancestry(&c3_hash, None);
assert_eq!(ancestry.len(), 3);
assert_eq!(ancestry[0].message, "Third");
assert_eq!(ancestry[1].message, "Second");
assert_eq!(ancestry[2].message, "First");
}
#[test]
fn test_common_ancestor() {
let mut store = CommitStore::new();
let c1 = CommitBuilder::new(100)
.message("First")
.author("test")
.timestamp(1000)
.build();
let c2 = CommitBuilder::new(101)
.parent(c1.hash)
.message("Second")
.author("test")
.timestamp(2000)
.build();
let c2_hash = c2.hash;
let c3 = CommitBuilder::new(102)
.parent(c2.hash)
.message("Third on main")
.author("test")
.timestamp(3000)
.build();
let c4 = CommitBuilder::new(103)
.parent(c2.hash)
.message("Fourth on feature")
.author("test")
.timestamp(3000)
.build();
let c3_hash = c3.hash;
let c4_hash = c4.hash;
store.add_commit(c1);
store.add_commit(c2);
store.add_commit_to_branch(c3, "main");
store.add_commit_to_branch(c4, "feature");
let ancestor = store.common_ancestor(&c3_hash, &c4_hash);
assert!(ancestor.is_some());
assert_eq!(ancestor.unwrap().hash, c2_hash);
}
#[test]
fn test_get_by_short_hash() {
let mut store = CommitStore::new();
let commit = CommitBuilder::new(100)
.message("Test")
.author("test")
.timestamp(1000)
.build();
let short = commit.short_hash();
store.add_commit(commit);
let found = store.get_by_short_hash(&short);
assert!(found.is_some());
assert_eq!(found.unwrap().message, "Test");
}
#[test]
fn test_get_by_txg() {
let mut store = CommitStore::new();
let commit = CommitBuilder::new(42)
.message("Test")
.author("test")
.timestamp(1000)
.build();
store.add_commit(commit);
let found = store.get_by_txg(42);
assert!(found.is_some());
assert_eq!(found.unwrap().message, "Test");
assert!(store.get_by_txg(999).is_none());
}
#[test]
fn test_range() {
let mut store = CommitStore::new();
let c1 = CommitBuilder::new(100)
.message("First")
.author("test")
.timestamp(1000)
.build();
let c2 = CommitBuilder::new(101)
.parent(c1.hash)
.message("Second")
.author("test")
.timestamp(2000)
.build();
let c1_hash = c1.hash;
let c2_hash = c2.hash;
let c3 = CommitBuilder::new(102)
.parent(c2.hash)
.message("Third")
.author("test")
.timestamp(3000)
.build();
let c4 = CommitBuilder::new(103)
.parent(c3.hash)
.message("Fourth")
.author("test")
.timestamp(4000)
.build();
let c4_hash = c4.hash;
store.add_commit(c1);
store.add_commit(c2);
store.add_commit(c3);
store.add_commit(c4);
let range = store.range(Some(&c2_hash), &c4_hash);
assert_eq!(range.len(), 2);
assert_eq!(range[0].message, "Third");
assert_eq!(range[1].message, "Fourth");
}
#[test]
fn test_validator_verify_hash() {
let commit = CommitBuilder::new(100)
.message("Test")
.author("test")
.timestamp(1000)
.build();
assert!(CommitValidator::verify_hash(&commit));
}
#[test]
fn test_validator_validate() {
let commit = CommitBuilder::new(100)
.message("Test")
.author("test")
.timestamp(1000)
.build();
assert!(CommitValidator::validate(&commit).is_ok());
}
#[test]
fn test_validator_empty_message() {
let commit = CommitBuilder::new(100)
.message("")
.author("test")
.timestamp(1000)
.build();
assert!(CommitValidator::validate(&commit).is_err());
}
#[test]
fn test_validator_empty_author() {
let commit = CommitBuilder::new(100)
.message("Test")
.author("")
.timestamp(1000)
.build();
assert!(CommitValidator::validate(&commit).is_err());
}
#[test]
fn test_branch_commits() {
let mut store = CommitStore::new();
let c1 = CommitBuilder::new(100)
.message("First")
.author("test")
.timestamp(1000)
.build();
let c2 = CommitBuilder::new(101)
.parent(c1.hash)
.message("Second")
.author("test")
.timestamp(2000)
.build();
store.add_commit(c1);
store.add_commit_to_branch(c2, "main");
let commits = store.branch_commits("main", None);
assert_eq!(commits.len(), 2);
let limited = store.branch_commits("main", Some(1));
assert_eq!(limited.len(), 1);
}
}