use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::cherrypick::{CherryPickOptions, CherryPickResult, CherryPicker};
use super::commit::{CommitBuilder, CommitStore, CommitValidator};
use super::log::{BranchStats, LogOptions, LogViewer};
use super::merge::{FileStateProvider, MergeAnalysis, ThreeWayMerge};
use super::ops::{BranchOps, BranchStatus, DatasetProvider};
use super::registry::BranchRegistry;
use super::types::{Branch, BranchError, Commit, FileChange, MergeResult, MergeStrategy};
pub struct BranchManager<P>
where
P: DatasetProvider + FileStateProvider,
{
provider: P,
registry: BranchRegistry,
commits: CommitStore,
default_author: String,
}
impl<P> BranchManager<P>
where
P: DatasetProvider + FileStateProvider,
{
pub fn new(provider: P, author: impl Into<String>) -> Self {
let timestamp = provider.current_timestamp();
Self {
provider,
registry: BranchRegistry::new(0, timestamp),
commits: CommitStore::new(),
default_author: author.into(),
}
}
pub fn with_state(
provider: P,
registry: BranchRegistry,
commits: CommitStore,
author: impl Into<String>,
) -> Self {
Self {
provider,
registry,
commits,
default_author: author.into(),
}
}
pub fn provider(&self) -> &P {
&self.provider
}
pub fn provider_mut(&mut self) -> &mut P {
&mut self.provider
}
pub fn registry(&self) -> &BranchRegistry {
&self.registry
}
pub fn commits(&self) -> &CommitStore {
&self.commits
}
pub fn set_author(&mut self, author: impl Into<String>) {
self.default_author = author.into();
}
pub fn current_branch(&self) -> Option<&Branch> {
self.registry.current_branch()
}
pub fn current_branch_name(&self) -> &str {
self.registry.current()
}
pub fn get_branch(&self, name: &str) -> Option<&Branch> {
self.registry.get(name)
}
pub fn list_branches(&self) -> Vec<&Branch> {
self.registry.list_sorted()
}
pub fn branch_exists(&self, name: &str) -> bool {
self.registry.contains(name)
}
pub fn create_branch(&mut self, name: &str) -> Result<Branch, BranchError> {
let mut ops = BranchOps::new(&mut self.provider, &mut self.registry);
ops.create_branch(name)?;
Ok(self.registry.get(name).unwrap().clone())
}
pub fn create_branch_from(&mut self, name: &str, parent: &str) -> Result<Branch, BranchError> {
let mut ops = BranchOps::new(&mut self.provider, &mut self.registry);
ops.create_branch_from(name, parent)?;
Ok(self.registry.get(name).unwrap().clone())
}
pub fn checkout(&mut self, name: &str) -> Result<Branch, BranchError> {
let mut ops = BranchOps::new(&mut self.provider, &mut self.registry);
ops.checkout(name)?;
Ok(self.registry.get(name).unwrap().clone())
}
pub fn checkout_new(&mut self, name: &str) -> Result<Branch, BranchError> {
let mut ops = BranchOps::new(&mut self.provider, &mut self.registry);
ops.checkout_new(name)?;
Ok(self.registry.get(name).unwrap().clone())
}
pub fn delete_branch(&mut self, name: &str, force: bool) -> Result<Branch, BranchError> {
let mut ops = BranchOps::new(&mut self.provider, &mut self.registry);
ops.delete_branch(name, force)
}
pub fn rename_branch(&mut self, old_name: &str, new_name: &str) -> Result<(), BranchError> {
self.registry.rename(old_name, new_name)
}
pub fn branch_status(&mut self, name: &str) -> Result<BranchStatus, BranchError> {
let ops = BranchOps::new(&mut self.provider, &mut self.registry);
ops.status(name)
}
pub fn commit(
&mut self,
message: &str,
changes: Vec<FileChange>,
) -> Result<Commit, BranchError> {
self.commit_with_author(message, &self.default_author.clone(), changes)
}
pub fn commit_with_author(
&mut self,
message: &str,
author: &str,
changes: Vec<FileChange>,
) -> Result<Commit, BranchError> {
let branch = self
.registry
.current_branch()
.ok_or_else(|| BranchError::Internal("no current branch".into()))?;
let current_name = branch.name.clone();
let txg = branch.head_txg;
let timestamp = self.provider.current_timestamp();
let parent = self.commits.get_head_hash(¤t_name);
let mut builder = CommitBuilder::new(txg)
.message(message)
.author(author)
.timestamp(timestamp)
.changes(changes);
if let Some(p) = parent {
builder = builder.parent(p);
}
let commit = builder.build();
CommitValidator::validate(&commit)?;
self.commits
.add_commit_to_branch(commit.clone(), ¤t_name);
Ok(commit)
}
pub fn get_commit(&self, hash: &[u8; 32]) -> Option<&Commit> {
self.commits.get(hash)
}
pub fn get_commit_by_short(&self, short: &str) -> Option<&Commit> {
self.commits.get_by_short_hash(short)
}
pub fn get_head(&self, branch: &str) -> Option<&Commit> {
self.commits.get_head(branch)
}
pub fn log(&self, options: LogOptions) -> Vec<super::log::LogEntry> {
let viewer = LogViewer::new(&self.commits);
viewer.log(self.registry.current(), options).collect()
}
pub fn log_branch(&self, branch: &str, options: LogOptions) -> Vec<super::log::LogEntry> {
let viewer = LogViewer::new(&self.commits);
viewer.log(branch, options).collect()
}
pub fn file_history(&self, path: &str) -> Vec<super::log::LogEntry> {
let viewer = LogViewer::new(&self.commits);
viewer.file_history(self.registry.current(), path)
}
pub fn stats(&self, branch: &str) -> BranchStats {
let viewer = LogViewer::new(&self.commits);
viewer.stats(branch)
}
pub fn shortlog(&self, branch: &str) -> Vec<(String, usize)> {
let viewer = LogViewer::new(&self.commits);
viewer.shortlog(branch)
}
pub fn merge(&mut self, source_branch: &str) -> Result<MergeResult, BranchError> {
self.merge_with_strategy(source_branch, MergeStrategy::Normal)
}
pub fn merge_with_strategy(
&mut self,
source_branch: &str,
strategy: MergeStrategy,
) -> Result<MergeResult, BranchError> {
let target = self
.registry
.current_branch()
.ok_or_else(|| BranchError::Internal("no current branch".into()))?;
let source = self
.registry
.get(source_branch)
.ok_or_else(|| BranchError::BranchNotFound(source_branch.to_string()))?;
let target_name = target.name.clone();
let target_txg = target.head_txg;
let source_txg = source.head_txg;
let base_txg = self
.registry
.merge_base_txg(&target_name, source_branch)
.ok_or(BranchError::NoCommonAncestor)?;
let merger = ThreeWayMerge::new(&self.provider, strategy);
let analysis = merger.merge(base_txg, target_txg, source_txg)?;
if !analysis.is_clean() && strategy == MergeStrategy::Normal {
return Ok(MergeResult {
merged_files: 0,
conflicts: analysis.conflicts,
result_txg: None,
merge_commit: None,
});
}
let timestamp = self.provider.current_timestamp();
let parent = self.commits.get_head_hash(&target_name);
let new_txg = target_txg + 1;
let message = alloc::format!("Merge branch '{}' into {}", source_branch, target_name);
let mut builder = CommitBuilder::new(new_txg)
.message(message)
.author(&self.default_author)
.timestamp(timestamp)
.changes(analysis.changes.clone());
if let Some(p) = parent {
builder = builder.parent(p);
}
let commit = builder.build();
self.registry.update_head(&target_name, new_txg)?;
self.commits
.add_commit_to_branch(commit.clone(), &target_name);
Ok(MergeResult {
merged_files: analysis.change_count(),
conflicts: analysis.conflicts,
result_txg: Some(new_txg),
merge_commit: Some(commit),
})
}
pub fn merge_preview(&self, source_branch: &str) -> Result<MergeAnalysis, BranchError> {
let target = self
.registry
.current_branch()
.ok_or_else(|| BranchError::Internal("no current branch".into()))?;
let source = self
.registry
.get(source_branch)
.ok_or_else(|| BranchError::BranchNotFound(source_branch.to_string()))?;
let base_txg = self
.registry
.merge_base_txg(&target.name, source_branch)
.ok_or(BranchError::NoCommonAncestor)?;
let merger = ThreeWayMerge::new(&self.provider, MergeStrategy::Normal);
merger.merge(base_txg, target.head_txg, source.head_txg)
}
pub fn cherry_pick(&mut self, commit_hash: &[u8; 32]) -> Result<CherryPickResult, BranchError> {
self.cherry_pick_with_options(commit_hash, &CherryPickOptions::default())
}
pub fn cherry_pick_with_options(
&mut self,
commit_hash: &[u8; 32],
options: &CherryPickOptions,
) -> Result<CherryPickResult, BranchError> {
let branch = self
.registry
.current_branch()
.ok_or_else(|| BranchError::Internal("no current branch".into()))?;
let current_txg = branch.head_txg;
let new_txg = current_txg + 1;
let branch_name = branch.name.clone();
let timestamp = self.provider.current_timestamp();
let picker = CherryPicker::new(&self.provider, &self.commits);
let result = picker.cherry_pick(
commit_hash,
current_txg,
new_txg,
&self.default_author,
timestamp,
options,
)?;
if result.is_success() {
if let Some(ref commit) = result.new_commit {
self.registry.update_head(&branch_name, new_txg)?;
self.commits
.add_commit_to_branch(commit.clone(), &branch_name);
}
}
Ok(result)
}
pub fn cherry_pick_range(
&mut self,
commit_hashes: &[[u8; 32]],
options: &CherryPickOptions,
) -> Result<Vec<CherryPickResult>, BranchError> {
let branch = self
.registry
.current_branch()
.ok_or_else(|| BranchError::Internal("no current branch".into()))?;
let start_txg = branch.head_txg;
let branch_name = branch.name.clone();
let timestamp = self.provider.current_timestamp();
let picker = CherryPicker::new(&self.provider, &self.commits);
let results = picker.cherry_pick_range(
commit_hashes,
start_txg,
&self.default_author,
timestamp,
options,
)?;
let mut max_txg = start_txg;
for result in &results {
if result.is_success() {
if let Some(ref commit) = result.new_commit {
max_txg = commit.txg;
self.commits
.add_commit_to_branch(commit.clone(), &branch_name);
}
}
}
if max_txg > start_txg {
self.registry.update_head(&branch_name, max_txg)?;
}
Ok(results)
}
pub fn sync(&mut self) -> Result<u64, BranchError> {
let mut ops = BranchOps::new(&mut self.provider, &mut self.registry);
ops.sync_current()
}
pub fn sync_branch(&mut self, name: &str) -> Result<u64, BranchError> {
let mut ops = BranchOps::new(&mut self.provider, &mut self.registry);
ops.sync_branch(name)
}
}
#[cfg(test)]
mod tests {
use super::super::merge::FileInfo;
use super::*;
use alloc::collections::BTreeMap;
use alloc::vec;
struct MockProvider {
next_guid: u64,
active_guid: Option<u64>,
datasets: Vec<u64>,
txg_map: BTreeMap<u64, u64>,
file_states: BTreeMap<u64, BTreeMap<String, FileInfo>>,
}
impl MockProvider {
fn new() -> Self {
let mut provider = Self {
next_guid: 2,
active_guid: Some(1),
datasets: vec![1],
txg_map: BTreeMap::new(),
file_states: BTreeMap::new(),
};
provider.txg_map.insert(1, 0);
provider.file_states.insert(0, BTreeMap::new());
provider
}
fn add_file(&mut self, txg: u64, path: &str, checksum: [u64; 4], size: u64) {
let state = self.file_states.entry(txg).or_default();
state.insert(
path.to_string(),
FileInfo {
path: path.to_string(),
size,
checksum,
mtime: txg * 1000,
is_dir: false,
},
);
}
}
impl DatasetProvider for MockProvider {
fn clone_dataset(
&mut self,
_source_guid: u64,
_name: &str,
at_txg: u64,
) -> Result<u64, String> {
let guid = self.next_guid;
self.next_guid += 1;
self.datasets.push(guid);
self.txg_map.insert(guid, at_txg);
Ok(guid)
}
fn delete_dataset(&mut self, guid: u64) -> Result<(), String> {
self.datasets.retain(|&g| g != guid);
self.txg_map.remove(&guid);
Ok(())
}
fn activate_dataset(&mut self, guid: u64) -> Result<(), String> {
if self.datasets.contains(&guid) {
self.active_guid = Some(guid);
Ok(())
} else {
Err("dataset not found".into())
}
}
fn get_dataset_txg(&self, guid: u64) -> Result<u64, String> {
self.txg_map
.get(&guid)
.copied()
.ok_or_else(|| "dataset not found".into())
}
fn sync_dataset(&mut self, guid: u64) -> Result<u64, String> {
let txg = self
.txg_map
.get_mut(&guid)
.ok_or_else(|| "dataset not found".to_string())?;
*txg += 1;
Ok(*txg)
}
fn current_timestamp(&self) -> u64 {
1704067200
}
}
impl FileStateProvider for MockProvider {
fn list_files(&self, txg: u64) -> Result<Vec<FileInfo>, String> {
Ok(self
.file_states
.get(&txg)
.map(|s| s.values().cloned().collect())
.unwrap_or_default())
}
fn get_file(&self, path: &str, txg: u64) -> Result<Option<FileInfo>, String> {
Ok(self
.file_states
.get(&txg)
.and_then(|s| s.get(path).cloned()))
}
fn read_file(&self, _path: &str, _txg: u64) -> Result<Vec<u8>, String> {
Ok(alloc::vec![])
}
fn files_equal(
&self,
path1: &str,
txg1: u64,
path2: &str,
txg2: u64,
) -> Result<bool, String> {
let f1 = self.get_file(path1, txg1)?;
let f2 = self.get_file(path2, txg2)?;
match (f1, f2) {
(Some(a), Some(b)) => Ok(a.checksum == b.checksum),
(None, None) => Ok(true),
_ => Ok(false),
}
}
}
#[test]
fn test_branch_manager_new() {
let provider = MockProvider::new();
let manager = BranchManager::new(provider, "test@example.com");
assert_eq!(manager.current_branch_name(), "main");
assert!(manager.branch_exists("main"));
}
#[test]
fn test_create_and_checkout() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "test");
manager.create_branch("feature").unwrap();
assert!(manager.branch_exists("feature"));
manager.checkout("feature").unwrap();
assert_eq!(manager.current_branch_name(), "feature");
}
#[test]
fn test_commit() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "test");
let commit = manager
.commit(
"Initial commit",
alloc::vec![FileChange::created("/file.txt".into(), [1; 4], 100,)],
)
.unwrap();
assert_eq!(commit.message, "Initial commit");
assert_eq!(commit.author, "test");
let head = manager.get_head("main");
assert!(head.is_some());
assert_eq!(head.unwrap().hash, commit.hash);
}
#[test]
fn test_log() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "test");
manager.commit("First", alloc::vec![]).unwrap();
manager.commit("Second", alloc::vec![]).unwrap();
manager.commit("Third", alloc::vec![]).unwrap();
let log = manager.log(LogOptions::default());
assert_eq!(log.len(), 3);
assert_eq!(log[0].message, "Third");
assert_eq!(log[2].message, "First");
}
#[test]
fn test_list_branches() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "test");
manager.create_branch("alpha").unwrap();
manager.create_branch("beta").unwrap();
let branches = manager.list_branches();
assert_eq!(branches.len(), 3);
}
#[test]
fn test_delete_branch() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "test");
manager.create_branch("feature").unwrap();
manager.delete_branch("feature", true).unwrap();
assert!(!manager.branch_exists("feature"));
}
#[test]
fn test_rename_branch() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "test");
manager.create_branch("old-name").unwrap();
manager.rename_branch("old-name", "new-name").unwrap();
assert!(!manager.branch_exists("old-name"));
assert!(manager.branch_exists("new-name"));
}
#[test]
fn test_stats() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "alice");
manager
.commit(
"First",
alloc::vec![FileChange::created("/a.txt".into(), [1; 4], 100)],
)
.unwrap();
manager
.commit(
"Second",
alloc::vec![FileChange::created("/b.txt".into(), [2; 4], 200)],
)
.unwrap();
let stats = manager.stats("main");
assert_eq!(stats.commit_count, 2);
assert_eq!(stats.total_additions, 2);
assert_eq!(stats.author_count, 1);
}
#[test]
fn test_shortlog() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "alice");
manager.commit("By alice", alloc::vec![]).unwrap();
manager.set_author("bob");
manager.commit("By bob", alloc::vec![]).unwrap();
manager.commit("By bob again", alloc::vec![]).unwrap();
let shortlog = manager.shortlog("main");
assert_eq!(shortlog.len(), 2);
let bob = shortlog.iter().find(|(a, _)| a == "bob");
assert!(bob.is_some());
assert_eq!(bob.unwrap().1, 2);
}
#[test]
fn test_checkout_new() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "test");
manager.checkout_new("feature").unwrap();
assert_eq!(manager.current_branch_name(), "feature");
assert!(manager.branch_exists("feature"));
}
#[test]
fn test_sync() {
let provider = MockProvider::new();
let mut manager = BranchManager::new(provider, "test");
let txg = manager.sync().unwrap();
assert_eq!(txg, 1);
let branch = manager.current_branch().unwrap();
assert_eq!(branch.head_txg, 1);
}
}