#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommitKind {
Normal,
Merge,
CherryPick,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Commit {
pub id: String,
pub branch: String,
pub tag: Option<String>,
pub kind: CommitKind,
pub parent: Option<usize>,
pub merge_parent: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Branch {
pub name: String,
pub created_after_commit: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
Commit(usize),
BranchCreated(usize),
Checkout(String),
Merge(usize),
CherryPick(usize),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GitGraph {
pub branches: Vec<Branch>,
pub commits: Vec<Commit>,
pub events: Vec<Event>,
}
impl GitGraph {
pub fn lane_of(&self, branch_name: &str) -> Option<usize> {
self.branches.iter().position(|b| b.name == branch_name)
}
pub fn head_of(&self, branch_name: &str) -> Option<usize> {
self.commits
.iter()
.enumerate()
.rev()
.find(|(_, c)| c.branch == branch_name)
.map(|(i, _)| i)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_graph() -> GitGraph {
let mut g = GitGraph {
branches: vec![
Branch {
name: "main".to_string(),
created_after_commit: None,
},
Branch {
name: "develop".to_string(),
created_after_commit: Some(1),
},
],
..Default::default()
};
g.commits.push(Commit {
id: "c0".to_string(),
branch: "main".to_string(),
tag: None,
kind: CommitKind::Normal,
parent: None,
merge_parent: None,
});
g.commits.push(Commit {
id: "c1".to_string(),
branch: "main".to_string(),
tag: None,
kind: CommitKind::Normal,
parent: Some(0),
merge_parent: None,
});
g.commits.push(Commit {
id: "c2".to_string(),
branch: "develop".to_string(),
tag: None,
kind: CommitKind::Normal,
parent: Some(1),
merge_parent: None,
});
g.commits.push(Commit {
id: "c3".to_string(),
branch: "main".to_string(),
tag: Some("v1.0".to_string()),
kind: CommitKind::Merge,
parent: Some(1),
merge_parent: Some(2),
});
g
}
#[test]
fn lane_of_returns_correct_indices() {
let g = make_graph();
assert_eq!(g.lane_of("main"), Some(0));
assert_eq!(g.lane_of("develop"), Some(1));
assert_eq!(g.lane_of("nonexistent"), None);
}
#[test]
fn head_of_returns_latest_commit_on_branch() {
let g = make_graph();
assert_eq!(g.head_of("main"), Some(3));
assert_eq!(g.head_of("develop"), Some(2));
assert_eq!(g.head_of("feature"), None);
}
#[test]
fn merge_commit_has_both_parents() {
let g = make_graph();
let merge = &g.commits[3];
assert_eq!(merge.kind, CommitKind::Merge);
assert_eq!(merge.parent, Some(1));
assert_eq!(merge.merge_parent, Some(2));
assert_eq!(merge.tag.as_deref(), Some("v1.0"));
}
#[test]
fn default_graph_is_empty() {
let g = GitGraph::default();
assert!(g.branches.is_empty());
assert!(g.commits.is_empty());
assert!(g.events.is_empty());
assert_eq!(g.lane_of("main"), None);
assert_eq!(g.head_of("main"), None);
}
#[test]
fn commit_kind_variants_are_distinct() {
assert_ne!(CommitKind::Normal, CommitKind::Merge);
assert_ne!(CommitKind::Merge, CommitKind::CherryPick);
assert_ne!(CommitKind::Normal, CommitKind::CherryPick);
}
}