From 341f5a5286b8c124875507bcb6e692985bc64dac Mon Sep 17 00:00:00 2001 From: Patrick Lafrance Date: Sun, 8 Mar 2026 11:43:13 -0400 Subject: [PATCH 1/3] Added details to SVG graph --- src/print/svg.rs | 182 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 161 insertions(+), 21 deletions(-) diff --git a/src/print/svg.rs b/src/print/svg.rs index f6ef745..0a72684 100644 --- a/src/print/svg.rs +++ b/src/print/svg.rs @@ -1,9 +1,10 @@ //! Create graphs in SVG format (Scalable Vector Graphics). +use crate::graph::CommitInfo; use crate::graph::GitGraph; use crate::settings::Settings; use svg::node::element::path::Data; -use svg::node::element::{Circle, Line, Path}; +use svg::node::element::{Circle, Group, Line, Path, Text, Title}; use svg::Document; /// Creates a SVG visual representation of a graph. @@ -11,7 +12,8 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result Result max_column { - max_column = branch.visual.column.unwrap(); - } - for p in 0..2 { let parent = info.parents[p]; let Some(par_oid) = parent else { @@ -84,19 +92,53 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result { + document = document.add(branches); + + widest_branch_names = f32::max(widest_branch_names, width); + } + None => {} + } + + widest_summary = f32::max(widest_summary, text_bounding_box(&commit_str, 12.0).0); } } + let (x_max, y_max) = commit_coord(max_idx + 1, max_column + 1); + document = document - .set("viewBox", (0, 0, x_max, y_max)) - .set("width", x_max) - .set("height", y_max); + .set( + "viewBox", + ( + -widest_branch_names, + 0, + x_max + widest_branch_names + widest_summary, + y_max, + ), + ) + .set("width", x_max + widest_branch_names + widest_summary + 15.0) + .set("height", y_max) + .set("style", "font-family:monospace;font-size:12px;"); let mut out: Vec = vec![]; svg::write(&mut out, &document).map_err(|err| err.to_string())?; @@ -114,6 +156,97 @@ fn commit_dot(index: usize, column: usize, color: &str, filled: bool) -> Circle .set("stroke-width", 1) } +fn draw_branches( + index: usize, + column: usize, + info: &CommitInfo, + graph: &GitGraph, +) -> Option<(Group, f32)> { + let (x, y) = commit_coord(index, column); + + let mut branch_names = info + .branches + .iter() + .map(|b| graph.all_branches[*b].name.clone()) + .collect::>(); + + if graph.head.oid == info.oid { + // Head is here + match branch_names + .iter() + .position(|name| name == &graph.head.name) + { + Some(index) => { + branch_names.insert(index + 1, "HEAD".to_string()); + } + //Detached HEAD + None => branch_names.push("HEAD".to_string()), + } + } + + if branch_names.len() > 0 { + let mut g = Group::new(); + let mut start: f32 = 5.0; + + for branch_name in &branch_names { + let gap = 9.0 + + if branch_name == "HEAD" && graph.head.is_branch { + 0.0 + } else { + 8.0 + }; + g = g.add(draw_branch(start - gap, 2.5, branch_name)); + + start = start - text_bounding_box(&branch_name, 12.0).0 - gap; + } + + g = g.set("transform", format!("translate({x}, {y})")); + + Some((g.clone(), -(start + x))) + } else { + None + } +} + +fn draw_branch(x: f32, y: f32, branch_name: &String) -> Group { + let width = text_bounding_box(&branch_name, 12.0).0; + + Group::new() + .add(Text::new(branch_name).set("x", x - width).set("y", y + 1.0)) + .add( + Path::new() + .set( + "d", + Data::new() + //Tip + .move_to((x + 2.0, y + 4.0)) + .line_by((6.0, -7.0)) + .line_by((-6.0, -7.0)) + //Body + .horizontal_line_by(-width - 11.0) + //Rear + .line_by((6.0, 7.0)) + .line_by((-6.0, 7.0)) + .close(), + ) + .set("stroke", "#00000000") + .set("fill", "#00000030"), + ) +} + +fn draw_summary(index: usize, max_column: usize, hash: &str) -> Text { + let (x, y) = commit_coord(index, max_column); + Text::new(hash) + .set("x", x + 15.0) + .set("y", y + 2.0) + .set("style", "font-family:monospace;font-size:12px") +} + +fn text_bounding_box(text: &str, size: f32) -> (f32, f32) { + // Let's assume the font has a 60% width + (text.len() as f32 * size * 0.6, size) +} + fn line(index1: usize, column1: usize, index2: usize, column2: usize, color: &str) -> Line { let (x1, y1) = commit_coord(index1, column1); let (x2, y2) = commit_coord(index2, column2); @@ -155,12 +288,19 @@ fn path( let m = (0.5 * (c1.0 + c2.0), 0.5 * (c1.1 + c2.1)); - let data = Data::new() - .move_to(c0) - .line_to(c1) - .quadratic_curve_to((c1.0, m.1, m.0, m.1)) - .quadratic_curve_to((c2.0, m.1, c2.0, c2.1)) - .line_to(c3); + let data = if column2 > column1 { + Data::new() + .move_to(c0) + .line_to(c1) + .line_to((c2.0, m.1)) + .line_to(c3) + } else { + Data::new() + .move_to(c0) + .line_to((c1.0, m.1)) + .line_to(c2) + .line_to(c3) + }; Path::new() .set("d", data) From 7c3115ea9605986fc7e0211c437ae5790069b049 Mon Sep 17 00:00:00 2001 From: Patrick Lafrance Date: Wed, 8 Apr 2026 20:56:11 -0400 Subject: [PATCH 2/3] Fixed clippy errors --- src/print/svg.rs | 176 +++++++++++++++++++++++++---------------------- 1 file changed, 94 insertions(+), 82 deletions(-) diff --git a/src/print/svg.rs b/src/print/svg.rs index 0a72684..70166c1 100644 --- a/src/print/svg.rs +++ b/src/print/svg.rs @@ -40,93 +40,50 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result { - document = document.add(branches); + if let Some((branches, width)) = + draw_branches(idx, branch.visual.column.unwrap(), info, graph) + { + document = document.add(branches); - widest_branch_names = f32::max(widest_branch_names, width); - } - None => {} + widest_branch_names = f32::max(widest_branch_names, width); } - - widest_summary = f32::max(widest_summary, text_bounding_box(&commit_str, 12.0).0); } + widest_summary = f32::max(widest_summary, text_bounding_box(commit_str, 12.0).0); } let (x_max, y_max) = commit_coord(max_idx + 1, max_column + 1); + document = set_document_size( + document.clone(), + widest_branch_names, + widest_summary, + x_max, + y_max, + ); - document = document + let mut out: Vec = vec![]; + svg::write(&mut out, &document).map_err(|err| err.to_string())?; + Ok(String::from_utf8(out).unwrap_or_else(|_| "Invalid UTF8 character.".to_string())) +} + +fn set_document_size( + document: Document, + widest_branch_names: f32, + widest_summary: f32, + x_max: f32, + y_max: f32, +) -> Document { + document .set( "viewBox", ( @@ -138,11 +95,66 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result = vec![]; - svg::write(&mut out, &document).map_err(|err| err.to_string())?; - Ok(String::from_utf8(out).unwrap_or_else(|_| "Invalid UTF8 character.".to_string())) +fn draw_commit(info: &CommitInfo, graph: &GitGraph, index: usize) -> Group { + let mut group = Group::new(); + + if let Some(trace) = info.branch_trace { + let branch = &graph.all_branches[trace]; + let branch_color = &branch.visual.svg_color; + + for p in 0..2 { + let parent = info.parents[p]; + let Some(par_oid) = parent else { + continue; + }; + let Some(par_idx) = graph.indices.get(&par_oid) else { + // Parent is outside scope of graph.indices + // so draw a vertical line to the bottom + let idx_bottom = graph.commits.len(); + group = group.add(line( + index, + branch.visual.column.unwrap(), + idx_bottom, + branch.visual.column.unwrap(), + branch_color, + )); + continue; + }; + let par_info = &graph.commits[*par_idx]; + let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()]; + + group = group.add(path( + index, + branch.visual.column.unwrap(), + *par_idx, + par_branch.visual.column.unwrap(), + if branch.visual.column == par_branch.visual.column { + index + } else { + super::get_deviate_index(graph, index, *par_idx) + }, + if info.is_merge { + &par_branch.visual.svg_color + } else { + branch_color + }, + )); + } + + group = group.add( + commit_dot( + index, + branch.visual.column.unwrap(), + branch_color, + !info.is_merge, + ) + .add(Title::new(info.oid.to_string())), + ); + } + group } fn commit_dot(index: usize, column: usize, color: &str, filled: bool) -> Circle { @@ -184,7 +196,7 @@ fn draw_branches( } } - if branch_names.len() > 0 { + if !branch_names.is_empty() { let mut g = Group::new(); let mut start: f32 = 5.0; @@ -197,7 +209,7 @@ fn draw_branches( }; g = g.add(draw_branch(start - gap, 2.5, branch_name)); - start = start - text_bounding_box(&branch_name, 12.0).0 - gap; + start = start - text_bounding_box(branch_name, 12.0).0 - gap; } g = g.set("transform", format!("translate({x}, {y})")); @@ -209,7 +221,7 @@ fn draw_branches( } fn draw_branch(x: f32, y: f32, branch_name: &String) -> Group { - let width = text_bounding_box(&branch_name, 12.0).0; + let width = text_bounding_box(branch_name, 12.0).0; Group::new() .add(Text::new(branch_name).set("x", x - width).set("y", y + 1.0)) From 6e7fd1b26f8f89124a585b6f13d4306ce6af1502 Mon Sep 17 00:00:00 2001 From: Patrick Lafrance Date: Wed, 8 Apr 2026 21:28:53 -0400 Subject: [PATCH 3/3] Refactoring --- src/print/svg.rs | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/print/svg.rs b/src/print/svg.rs index 70166c1..83719b3 100644 --- a/src/print/svg.rs +++ b/src/print/svg.rs @@ -29,24 +29,15 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result Result = vec![]; @@ -80,9 +70,11 @@ fn set_document_size( document: Document, widest_branch_names: f32, widest_summary: f32, - x_max: f32, - y_max: f32, + max_idx: usize, + max_column: usize, ) -> Document { + let (x_max, y_max) = commit_coord(max_idx + 1, max_column + 1); + document .set( "viewBox", @@ -98,6 +90,18 @@ fn set_document_size( .set("style", "font-family:monospace;font-size:12px;") } +fn find_max_column(graph: &GitGraph) -> usize { + graph + .commits + .iter() + .filter_map(|info| { + info.branch_trace + .and_then(|trace| graph.all_branches[trace].visual.column) + }) + .max() + .unwrap_or(0) +} + fn draw_commit(info: &CommitInfo, graph: &GitGraph, index: usize) -> Group { let mut group = Group::new(); @@ -154,7 +158,7 @@ fn draw_commit(info: &CommitInfo, graph: &GitGraph, index: usize) -> Group { .add(Title::new(info.oid.to_string())), ); } - group + group } fn commit_dot(index: usize, column: usize, color: &str, filled: bool) -> Circle {