Skip to content

Commit b5ba5cd

Browse files
authored
Merge pull request #140 from plafrance/87-FR-SVG-output-to-include-text-commit-hashes-etc
Added details to SVG graph
2 parents 200b23a + 6e7fd1b commit b5ba5cd

1 file changed

Lines changed: 223 additions & 67 deletions

File tree

src/print/svg.rs

Lines changed: 223 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
//! Create graphs in SVG format (Scalable Vector Graphics).
22
3+
use crate::graph::CommitInfo;
34
use crate::graph::GitGraph;
45
use crate::settings::Settings;
56
use svg::node::element::path::Data;
6-
use svg::node::element::{Circle, Line, Path};
7+
use svg::node::element::{Circle, Group, Line, Path, Text, Title};
78
use svg::Document;
89

910
/// Creates a SVG visual representation of a graph.
1011
pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result<String, String> {
1112
let mut document = Document::new();
1213

1314
let max_idx = graph.commits.len();
14-
let mut max_column = 0;
15+
let mut widest_summary = 0.0;
16+
let mut widest_branch_names = 0.0;
1517

1618
if settings.debug {
1719
for branch in &graph.all_branches {
@@ -27,80 +29,136 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result<String, String
2729
}
2830
}
2931

32+
let max_column = find_max_column(graph);
33+
3034
for (idx, info) in graph.commits.iter().enumerate() {
35+
document = document.add(draw_commit(info, graph, idx));
36+
37+
let commit = graph.repository.find_commit(info.oid).unwrap();
38+
let commit_summary = commit.summary().unwrap_or("");
39+
40+
document = document.add(draw_summary(idx, max_column, commit_summary));
41+
3142
if let Some(trace) = info.branch_trace {
3243
let branch = &graph.all_branches[trace];
33-
let branch_color = &branch.visual.svg_color;
3444

35-
if branch.visual.column.unwrap() > max_column {
36-
max_column = branch.visual.column.unwrap();
45+
if let Some((branches, width)) =
46+
draw_branches(idx, branch.visual.column.unwrap(), info, graph)
47+
{
48+
document = document.add(branches);
49+
50+
widest_branch_names = f32::max(widest_branch_names, width);
3751
}
52+
}
53+
widest_summary = f32::max(widest_summary, text_bounding_box(commit_summary, 12.0).0);
54+
}
3855

39-
for p in 0..2 {
40-
let parent = info.parents[p];
41-
let Some(par_oid) = parent else {
42-
continue;
43-
};
44-
let Some(par_idx) = graph.indices.get(&par_oid) else {
45-
// Parent is outside scope of graph.indices
46-
// so draw a vertical line to the bottom
47-
let idx_bottom = max_idx;
48-
document = document.add(line(
49-
idx,
50-
branch.visual.column.unwrap(),
51-
idx_bottom,
52-
branch.visual.column.unwrap(),
53-
branch_color,
54-
));
55-
continue;
56-
};
57-
let par_info = &graph.commits[*par_idx];
58-
let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
56+
document = set_document_size(
57+
document.clone(),
58+
widest_branch_names,
59+
widest_summary,
60+
max_idx,
61+
max_column,
62+
);
5963

60-
let color = if info.is_merge {
61-
&par_branch.visual.svg_color
62-
} else {
63-
branch_color
64-
};
64+
let mut out: Vec<u8> = vec![];
65+
svg::write(&mut out, &document).map_err(|err| err.to_string())?;
66+
Ok(String::from_utf8(out).unwrap_or_else(|_| "Invalid UTF8 character.".to_string()))
67+
}
6568

69+
fn set_document_size(
70+
document: Document,
71+
widest_branch_names: f32,
72+
widest_summary: f32,
73+
max_idx: usize,
74+
max_column: usize,
75+
) -> Document {
76+
let (x_max, y_max) = commit_coord(max_idx + 1, max_column + 1);
77+
78+
document
79+
.set(
80+
"viewBox",
81+
(
82+
-widest_branch_names,
83+
0,
84+
x_max + widest_branch_names + widest_summary,
85+
y_max,
86+
),
87+
)
88+
.set("width", x_max + widest_branch_names + widest_summary + 15.0)
89+
.set("height", y_max)
90+
.set("style", "font-family:monospace;font-size:12px;")
91+
}
92+
93+
fn find_max_column(graph: &GitGraph) -> usize {
94+
graph
95+
.commits
96+
.iter()
97+
.filter_map(|info| {
98+
info.branch_trace
99+
.and_then(|trace| graph.all_branches[trace].visual.column)
100+
})
101+
.max()
102+
.unwrap_or(0)
103+
}
104+
105+
fn draw_commit(info: &CommitInfo, graph: &GitGraph, index: usize) -> Group {
106+
let mut group = Group::new();
107+
108+
if let Some(trace) = info.branch_trace {
109+
let branch = &graph.all_branches[trace];
110+
let branch_color = &branch.visual.svg_color;
111+
112+
for p in 0..2 {
113+
let parent = info.parents[p];
114+
let Some(par_oid) = parent else {
115+
continue;
116+
};
117+
let Some(par_idx) = graph.indices.get(&par_oid) else {
118+
// Parent is outside scope of graph.indices
119+
// so draw a vertical line to the bottom
120+
let idx_bottom = graph.commits.len();
121+
group = group.add(line(
122+
index,
123+
branch.visual.column.unwrap(),
124+
idx_bottom,
125+
branch.visual.column.unwrap(),
126+
branch_color,
127+
));
128+
continue;
129+
};
130+
let par_info = &graph.commits[*par_idx];
131+
let par_branch = &graph.all_branches[par_info.branch_trace.unwrap()];
132+
133+
group = group.add(path(
134+
index,
135+
branch.visual.column.unwrap(),
136+
*par_idx,
137+
par_branch.visual.column.unwrap(),
66138
if branch.visual.column == par_branch.visual.column {
67-
document = document.add(line(
68-
idx,
69-
branch.visual.column.unwrap(),
70-
*par_idx,
71-
par_branch.visual.column.unwrap(),
72-
color,
73-
));
139+
index
74140
} else {
75-
let split_index = super::get_deviate_index(graph, idx, *par_idx);
76-
document = document.add(path(
77-
idx,
78-
branch.visual.column.unwrap(),
79-
*par_idx,
80-
par_branch.visual.column.unwrap(),
81-
split_index,
82-
color,
83-
));
84-
}
85-
}
141+
super::get_deviate_index(graph, index, *par_idx)
142+
},
143+
if info.is_merge {
144+
&par_branch.visual.svg_color
145+
} else {
146+
branch_color
147+
},
148+
));
149+
}
86150

87-
document = document.add(commit_dot(
88-
idx,
151+
group = group.add(
152+
commit_dot(
153+
index,
89154
branch.visual.column.unwrap(),
90155
branch_color,
91156
!info.is_merge,
92-
));
93-
}
157+
)
158+
.add(Title::new(info.oid.to_string())),
159+
);
94160
}
95-
let (x_max, y_max) = commit_coord(max_idx + 1, max_column + 1);
96-
document = document
97-
.set("viewBox", (0, 0, x_max, y_max))
98-
.set("width", x_max)
99-
.set("height", y_max);
100-
101-
let mut out: Vec<u8> = vec![];
102-
svg::write(&mut out, &document).map_err(|err| err.to_string())?;
103-
Ok(String::from_utf8(out).unwrap_or_else(|_| "Invalid UTF8 character.".to_string()))
161+
group
104162
}
105163

106164
fn commit_dot(index: usize, column: usize, color: &str, filled: bool) -> Circle {
@@ -114,6 +172,97 @@ fn commit_dot(index: usize, column: usize, color: &str, filled: bool) -> Circle
114172
.set("stroke-width", 1)
115173
}
116174

175+
fn draw_branches(
176+
index: usize,
177+
column: usize,
178+
info: &CommitInfo,
179+
graph: &GitGraph,
180+
) -> Option<(Group, f32)> {
181+
let (x, y) = commit_coord(index, column);
182+
183+
let mut branch_names = info
184+
.branches
185+
.iter()
186+
.map(|b| graph.all_branches[*b].name.clone())
187+
.collect::<Vec<String>>();
188+
189+
if graph.head.oid == info.oid {
190+
// Head is here
191+
match branch_names
192+
.iter()
193+
.position(|name| name == &graph.head.name)
194+
{
195+
Some(index) => {
196+
branch_names.insert(index + 1, "HEAD".to_string());
197+
}
198+
//Detached HEAD
199+
None => branch_names.push("HEAD".to_string()),
200+
}
201+
}
202+
203+
if !branch_names.is_empty() {
204+
let mut g = Group::new();
205+
let mut start: f32 = 5.0;
206+
207+
for branch_name in &branch_names {
208+
let gap = 9.0
209+
+ if branch_name == "HEAD" && graph.head.is_branch {
210+
0.0
211+
} else {
212+
8.0
213+
};
214+
g = g.add(draw_branch(start - gap, 2.5, branch_name));
215+
216+
start = start - text_bounding_box(branch_name, 12.0).0 - gap;
217+
}
218+
219+
g = g.set("transform", format!("translate({x}, {y})"));
220+
221+
Some((g.clone(), -(start + x)))
222+
} else {
223+
None
224+
}
225+
}
226+
227+
fn draw_branch(x: f32, y: f32, branch_name: &String) -> Group {
228+
let width = text_bounding_box(branch_name, 12.0).0;
229+
230+
Group::new()
231+
.add(Text::new(branch_name).set("x", x - width).set("y", y + 1.0))
232+
.add(
233+
Path::new()
234+
.set(
235+
"d",
236+
Data::new()
237+
//Tip
238+
.move_to((x + 2.0, y + 4.0))
239+
.line_by((6.0, -7.0))
240+
.line_by((-6.0, -7.0))
241+
//Body
242+
.horizontal_line_by(-width - 11.0)
243+
//Rear
244+
.line_by((6.0, 7.0))
245+
.line_by((-6.0, 7.0))
246+
.close(),
247+
)
248+
.set("stroke", "#00000000")
249+
.set("fill", "#00000030"),
250+
)
251+
}
252+
253+
fn draw_summary(index: usize, max_column: usize, hash: &str) -> Text {
254+
let (x, y) = commit_coord(index, max_column);
255+
Text::new(hash)
256+
.set("x", x + 15.0)
257+
.set("y", y + 2.0)
258+
.set("style", "font-family:monospace;font-size:12px")
259+
}
260+
261+
fn text_bounding_box(text: &str, size: f32) -> (f32, f32) {
262+
// Let's assume the font has a 60% width
263+
(text.len() as f32 * size * 0.6, size)
264+
}
265+
117266
fn line(index1: usize, column1: usize, index2: usize, column2: usize, color: &str) -> Line {
118267
let (x1, y1) = commit_coord(index1, column1);
119268
let (x2, y2) = commit_coord(index2, column2);
@@ -155,12 +304,19 @@ fn path(
155304

156305
let m = (0.5 * (c1.0 + c2.0), 0.5 * (c1.1 + c2.1));
157306

158-
let data = Data::new()
159-
.move_to(c0)
160-
.line_to(c1)
161-
.quadratic_curve_to((c1.0, m.1, m.0, m.1))
162-
.quadratic_curve_to((c2.0, m.1, c2.0, c2.1))
163-
.line_to(c3);
307+
let data = if column2 > column1 {
308+
Data::new()
309+
.move_to(c0)
310+
.line_to(c1)
311+
.line_to((c2.0, m.1))
312+
.line_to(c3)
313+
} else {
314+
Data::new()
315+
.move_to(c0)
316+
.line_to((c1.0, m.1))
317+
.line_to(c2)
318+
.line_to(c3)
319+
};
164320

165321
Path::new()
166322
.set("d", data)

0 commit comments

Comments
 (0)