use super::types::{BranchStatus, BranchTopology, TopologyBranch};
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct LayoutRow {
pub branch_index: usize,
pub column: usize,
pub node_line: String,
pub info_line: String,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TopologyLayout {
pub rows: Vec<LayoutRow>,
pub max_column: usize,
}
impl TopologyLayout {
pub fn from_topology(topology: &BranchTopology) -> Self {
let mut rows = Vec::new();
let mut max_column = 0;
for (idx, branch) in topology.branches.iter().enumerate() {
let column = if branch.name == topology.main_branch {
0
} else {
1 };
max_column = max_column.max(column);
let node_line = create_node_line(branch, column, &topology.main_branch);
let info_line = create_info_line(branch, topology);
rows.push(LayoutRow {
branch_index: idx,
column,
node_line,
info_line,
});
}
Self { rows, max_column }
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
}
fn create_node_line(branch: &TopologyBranch, column: usize, main_branch: &str) -> String {
let mut line = String::new();
if branch.name == main_branch {
line.push('●');
} else {
line.push('│');
for _ in 0..column {
line.push(' ');
}
line.push('╲');
line.push('●');
}
line
}
fn create_info_line(branch: &TopologyBranch, topology: &BranchTopology) -> String {
let mut parts = Vec::new();
parts.push(branch.name.clone());
let label = branch.status.label();
if !label.is_empty() {
parts.push(format!("[{}]", label));
}
if let Some(ref relation) = branch.relation {
if branch.name != topology.main_branch {
let summary = relation.summary();
if summary != "up to date" {
parts.push(format!("({})", summary));
}
}
}
if branch.status == BranchStatus::Stale {
let days = chrono::Local::now()
.signed_duration_since(branch.last_activity)
.num_days();
parts.push(format!("{}d inactive", days));
}
parts.join(" ")
}
pub fn render_topology_ascii(topology: &BranchTopology, width: usize) -> Vec<String> {
let mut lines = Vec::new();
lines.push(format!("Branch Topology ({})", topology.main_branch));
lines.push("─".repeat(width.min(60)));
let main_line = create_main_branch_line(topology, width);
lines.push(main_line);
for branch in &topology.branches {
if branch.name == topology.main_branch {
continue;
}
let branch_line = create_branch_line(branch, topology, width);
lines.push(branch_line);
}
lines.push("─".repeat(width.min(60)));
lines.push(format!(
"{} branches ({} stale, {} merged)",
topology.branch_count(),
topology.stale_count(),
topology.merged_count()
));
lines
}
fn create_main_branch_line(topology: &BranchTopology, width: usize) -> String {
let main_branch = topology
.branches
.iter()
.find(|b| b.name == topology.main_branch);
let label = match main_branch {
Some(b) if b.status == BranchStatus::Active => format!("{} [HEAD]", topology.main_branch),
_ => topology.main_branch.clone(),
};
let node = "●";
let line_char = "─";
let head_marker = "HEAD";
let content_width = width.saturating_sub(10);
let node_count = content_width / 2;
let mut line = format!("{} ", label);
for i in 0..node_count {
if i == node_count - 1 {
line.push_str(node);
line.push_str("── ");
line.push_str(head_marker);
} else {
line.push_str(line_char);
line.push_str(line_char);
}
}
line
}
fn create_branch_line(branch: &TopologyBranch, topology: &BranchTopology, _width: usize) -> String {
let status_prefix = match branch.status {
BranchStatus::Stale => "[stale] ",
BranchStatus::Merged => "[merged] ",
BranchStatus::Active => "[HEAD] ",
_ => "",
};
let relation_suffix = match &branch.relation {
Some(r) if !r.is_merged => format!(" ({})", r.summary()),
_ => String::new(),
};
let stale_suffix = if branch.status == BranchStatus::Stale {
let days = chrono::Local::now()
.signed_duration_since(branch.last_activity)
.num_days();
format!(" ({}d ago)", days)
} else {
String::new()
};
let indent = if branch.name == topology.main_branch {
""
} else {
" ╲"
};
format!(
"{}{}{}●{}{}",
indent, status_prefix, branch.name, relation_suffix, stale_suffix
)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Local;
fn create_test_topology() -> BranchTopology {
let mut topology = BranchTopology::new("main".to_string());
topology.add_branch(
TopologyBranch::new("main".to_string(), "abc1234".to_string(), Local::now())
.with_status(BranchStatus::Active),
);
topology.add_branch(TopologyBranch::new(
"feature".to_string(),
"def5678".to_string(),
Local::now(),
));
topology
}
#[test]
fn test_topology_layout_from_topology() {
let topology = create_test_topology();
let layout = TopologyLayout::from_topology(&topology);
assert_eq!(layout.row_count(), 2);
}
#[test]
fn test_render_topology_ascii_returns_lines() {
let topology = create_test_topology();
let lines = render_topology_ascii(&topology, 60);
assert!(!lines.is_empty());
assert!(lines[0].contains("Branch Topology"));
}
#[test]
fn test_create_branch_line_shows_status() {
let topology = create_test_topology();
let stale_branch = TopologyBranch::new(
"old-feature".to_string(),
"ghi9012".to_string(),
Local::now() - chrono::Duration::days(60),
)
.with_status(BranchStatus::Stale);
let line = create_branch_line(&stale_branch, &topology, 60);
assert!(line.contains("[stale]"));
}
#[test]
fn test_create_info_line_shows_ahead_behind() {
let topology = create_test_topology();
let mut relation =
super::super::types::BranchRelation::new("main".to_string(), "feature".to_string());
relation.ahead_count = 3;
relation.behind_count = 1;
let branch =
TopologyBranch::new("feature".to_string(), "def5678".to_string(), Local::now())
.with_relation(relation);
let info = create_info_line(&branch, &topology);
assert!(info.contains("3 ahead"));
assert!(info.contains("1 behind"));
}
}