use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq)]
pub struct Branch {
pub name: String,
pub guid: u64,
pub parent: Option<String>,
pub fork_txg: u64,
pub head_txg: u64,
pub created: u64,
pub is_default: bool,
}
impl Branch {
pub fn new(
name: String,
guid: u64,
parent: Option<String>,
fork_txg: u64,
created: u64,
) -> Self {
Self {
name,
guid,
parent,
fork_txg,
head_txg: fork_txg,
created,
is_default: false,
}
}
pub fn main(guid: u64, txg: u64, created: u64) -> Self {
Self {
name: "main".into(),
guid,
parent: None,
fork_txg: txg,
head_txg: txg,
created,
is_default: true,
}
}
pub fn is_root(&self) -> bool {
self.parent.is_none()
}
pub fn txgs_since_fork(&self) -> u64 {
self.head_txg.saturating_sub(self.fork_txg)
}
}
#[derive(Debug, Clone)]
pub struct Commit {
pub hash: [u8; 32],
pub parent: Option<[u8; 32]>,
pub txg: u64,
pub message: String,
pub author: String,
pub timestamp: u64,
pub changes: Vec<FileChange>,
}
impl Commit {
pub fn short_hash(&self) -> String {
use alloc::format;
format!(
"{:02x}{:02x}{:02x}{:02x}",
self.hash[0], self.hash[1], self.hash[2], self.hash[3]
)
}
pub fn hash_hex(&self) -> String {
use alloc::format;
let mut s = String::with_capacity(64);
for byte in &self.hash {
s.push_str(&format!("{:02x}", byte));
}
s
}
pub fn is_initial(&self) -> bool {
self.parent.is_none()
}
pub fn files_changed(&self) -> usize {
self.changes.len()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FileChange {
pub path: String,
pub change_type: ChangeType,
pub old_checksum: Option<[u64; 4]>,
pub new_checksum: Option<[u64; 4]>,
pub old_size: Option<u64>,
pub new_size: Option<u64>,
}
impl FileChange {
pub fn created(path: String, checksum: [u64; 4], size: u64) -> Self {
Self {
path,
change_type: ChangeType::Created,
old_checksum: None,
new_checksum: Some(checksum),
old_size: None,
new_size: Some(size),
}
}
pub fn modified(
path: String,
old_checksum: [u64; 4],
new_checksum: [u64; 4],
old_size: u64,
new_size: u64,
) -> Self {
Self {
path,
change_type: ChangeType::Modified,
old_checksum: Some(old_checksum),
new_checksum: Some(new_checksum),
old_size: Some(old_size),
new_size: Some(new_size),
}
}
pub fn deleted(path: String, checksum: [u64; 4], size: u64) -> Self {
Self {
path,
change_type: ChangeType::Deleted,
old_checksum: Some(checksum),
new_checksum: None,
old_size: Some(size),
new_size: None,
}
}
pub fn renamed(old_path: String, new_path: String, checksum: [u64; 4], size: u64) -> Self {
Self {
path: new_path,
change_type: ChangeType::Renamed { old_path },
old_checksum: Some(checksum),
new_checksum: Some(checksum),
old_size: Some(size),
new_size: Some(size),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ChangeType {
Created,
Modified,
Deleted,
Renamed {
old_path: String,
},
}
impl ChangeType {
pub fn short_name(&self) -> &'static str {
match self {
ChangeType::Created => "A",
ChangeType::Modified => "M",
ChangeType::Deleted => "D",
ChangeType::Renamed { .. } => "R",
}
}
pub fn name(&self) -> &'static str {
match self {
ChangeType::Created => "created",
ChangeType::Modified => "modified",
ChangeType::Deleted => "deleted",
ChangeType::Renamed { .. } => "renamed",
}
}
}
#[derive(Debug, Clone)]
pub struct MergeResult {
pub merged_files: usize,
pub conflicts: Vec<MergeConflict>,
pub result_txg: Option<u64>,
pub merge_commit: Option<Commit>,
}
impl MergeResult {
pub fn is_success(&self) -> bool {
self.conflicts.is_empty() && self.result_txg.is_some()
}
pub fn has_conflicts(&self) -> bool {
!self.conflicts.is_empty()
}
pub fn conflict_count(&self) -> usize {
self.conflicts.len()
}
}
#[derive(Debug, Clone)]
pub struct MergeConflict {
pub path: String,
pub conflict_type: ConflictType,
pub base: Option<FileVersion>,
pub ours: Option<FileVersion>,
pub theirs: Option<FileVersion>,
}
impl MergeConflict {
pub fn both_modified(
path: String,
base: FileVersion,
ours: FileVersion,
theirs: FileVersion,
) -> Self {
Self {
path,
conflict_type: ConflictType::BothModified,
base: Some(base),
ours: Some(ours),
theirs: Some(theirs),
}
}
pub fn modify_delete(path: String, base: FileVersion, ours: FileVersion) -> Self {
Self {
path,
conflict_type: ConflictType::ModifyDelete,
base: Some(base),
ours: Some(ours),
theirs: None,
}
}
pub fn delete_modify(path: String, base: FileVersion, theirs: FileVersion) -> Self {
Self {
path,
conflict_type: ConflictType::DeleteModify,
base: Some(base),
ours: None,
theirs: Some(theirs),
}
}
pub fn both_created(path: String, ours: FileVersion, theirs: FileVersion) -> Self {
Self {
path,
conflict_type: ConflictType::BothCreated,
base: None,
ours: Some(ours),
theirs: Some(theirs),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConflictType {
BothModified,
ModifyDelete,
DeleteModify,
BothCreated,
}
impl ConflictType {
pub fn description(&self) -> &'static str {
match self {
ConflictType::BothModified => "both modified",
ConflictType::ModifyDelete => "modified here, deleted there",
ConflictType::DeleteModify => "deleted here, modified there",
ConflictType::BothCreated => "both created",
}
}
}
#[derive(Debug, Clone)]
pub struct FileVersion {
pub txg: u64,
pub size: u64,
pub checksum: [u64; 4],
pub mtime: u64,
}
impl FileVersion {
pub fn new(txg: u64, size: u64, checksum: [u64; 4], mtime: u64) -> Self {
Self {
txg,
size,
checksum,
mtime,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MergeStrategy {
#[default]
Normal,
Ours,
Theirs,
ConflictMarkers,
}
#[derive(Debug, Clone)]
pub enum BranchError {
BranchExists(String),
BranchNotFound(String),
CannotDeleteCurrent(String),
CannotDeleteDefault(String),
UnmergedChanges(String),
CommitNotFound(String),
DatasetNotFound(String),
MergeConflict(usize),
InvalidBranchName(String),
AlreadyOnBranch(String),
RebaseInProgress,
MergeInProgress,
NoCommonAncestor,
IoError(String),
Internal(String),
}
impl core::fmt::Display for BranchError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
BranchError::BranchExists(name) => write!(f, "branch '{}' already exists", name),
BranchError::BranchNotFound(name) => write!(f, "branch '{}' not found", name),
BranchError::CannotDeleteCurrent(name) => {
write!(f, "cannot delete current branch '{}'", name)
}
BranchError::CannotDeleteDefault(name) => {
write!(f, "cannot delete default branch '{}'", name)
}
BranchError::UnmergedChanges(name) => {
write!(f, "branch '{}' has unmerged changes", name)
}
BranchError::CommitNotFound(hash) => write!(f, "commit '{}' not found", hash),
BranchError::DatasetNotFound(name) => write!(f, "dataset '{}' not found", name),
BranchError::MergeConflict(count) => write!(f, "merge conflict: {} files", count),
BranchError::InvalidBranchName(name) => {
write!(f, "invalid branch name: '{}'", name)
}
BranchError::AlreadyOnBranch(name) => write!(f, "already on branch '{}'", name),
BranchError::RebaseInProgress => write!(f, "rebase already in progress"),
BranchError::MergeInProgress => write!(f, "merge already in progress"),
BranchError::NoCommonAncestor => write!(f, "no common ancestor found"),
BranchError::IoError(msg) => write!(f, "IO error: {}", msg),
BranchError::Internal(msg) => write!(f, "internal error: {}", msg),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn test_branch_creation() {
let branch = Branch::new(
"feature".into(),
12345,
Some("main".into()),
100,
1704067200,
);
assert_eq!(branch.name, "feature");
assert_eq!(branch.guid, 12345);
assert_eq!(branch.parent, Some("main".into()));
assert_eq!(branch.fork_txg, 100);
assert_eq!(branch.head_txg, 100);
assert!(!branch.is_default);
assert!(!branch.is_root());
}
#[test]
fn test_main_branch() {
let main = Branch::main(1, 0, 1704067200);
assert_eq!(main.name, "main");
assert!(main.is_default);
assert!(main.is_root());
assert_eq!(main.parent, None);
}
#[test]
fn test_commit_hash() {
let commit = Commit {
hash: [
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc,
0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78,
0x9a, 0xbc, 0xde, 0xf0,
],
parent: None,
txg: 100,
message: "Initial commit".into(),
author: "test".into(),
timestamp: 1704067200,
changes: vec![],
};
assert_eq!(commit.short_hash(), "12345678");
assert!(commit.hash_hex().starts_with("123456789abcdef0"));
assert!(commit.is_initial());
}
#[test]
fn test_file_change() {
let created = FileChange::created("/new.txt".into(), [1; 4], 100);
assert!(matches!(created.change_type, ChangeType::Created));
assert_eq!(created.old_checksum, None);
assert_eq!(created.new_checksum, Some([1; 4]));
let deleted = FileChange::deleted("/old.txt".into(), [2; 4], 200);
assert!(matches!(deleted.change_type, ChangeType::Deleted));
assert_eq!(deleted.old_checksum, Some([2; 4]));
assert_eq!(deleted.new_checksum, None);
}
#[test]
fn test_merge_result() {
let result = MergeResult {
merged_files: 5,
conflicts: vec![],
result_txg: Some(200),
merge_commit: None,
};
assert!(result.is_success());
assert!(!result.has_conflicts());
let with_conflict = MergeResult {
merged_files: 3,
conflicts: vec![MergeConflict {
path: "/conflict.txt".into(),
conflict_type: ConflictType::BothModified,
base: None,
ours: None,
theirs: None,
}],
result_txg: None,
merge_commit: None,
};
assert!(!with_conflict.is_success());
assert!(with_conflict.has_conflicts());
assert_eq!(with_conflict.conflict_count(), 1);
}
#[test]
fn test_conflict_types() {
assert_eq!(ConflictType::BothModified.description(), "both modified");
assert_eq!(
ConflictType::ModifyDelete.description(),
"modified here, deleted there"
);
}
#[test]
fn test_change_type_names() {
assert_eq!(ChangeType::Created.short_name(), "A");
assert_eq!(ChangeType::Modified.short_name(), "M");
assert_eq!(ChangeType::Deleted.short_name(), "D");
assert_eq!(
ChangeType::Renamed {
old_path: "".into()
}
.short_name(),
"R"
);
}
#[test]
fn test_merge_strategy_default() {
assert_eq!(MergeStrategy::default(), MergeStrategy::Normal);
}
}