11//! Create graphs in SVG format (Scalable Vector Graphics).
22
3+ use crate :: graph:: CommitInfo ;
34use crate :: graph:: GitGraph ;
45use crate :: settings:: Settings ;
56use svg:: node:: element:: path:: Data ;
6- use svg:: node:: element:: { Circle , Line , Path } ;
7+ use svg:: node:: element:: { Circle , Group , Line , Path , Text , Title } ;
78use svg:: Document ;
89
910/// Creates a SVG visual representation of a graph.
1011pub 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
106164fn 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+
117266fn 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