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,15 +29,21 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result<String, String
2729 }
2830 }
2931
32+ let max_column = graph
33+ . commits
34+ . iter ( )
35+ . filter_map ( |info| {
36+ info. branch_trace
37+ . and_then ( |trace| graph. all_branches [ trace] . visual . column )
38+ } )
39+ . max ( )
40+ . unwrap_or ( 0 ) ;
41+
3042 for ( idx, info) in graph. commits . iter ( ) . enumerate ( ) {
3143 if let Some ( trace) = info. branch_trace {
3244 let branch = & graph. all_branches [ trace] ;
3345 let branch_color = & branch. visual . svg_color ;
3446
35- if branch. visual . column . unwrap ( ) > max_column {
36- max_column = branch. visual . column . unwrap ( ) ;
37- }
38-
3947 for p in 0 ..2 {
4048 let parent = info. parents [ p] ;
4149 let Some ( par_oid) = parent else {
@@ -84,19 +92,53 @@ pub fn print_svg(graph: &GitGraph, settings: &Settings) -> Result<String, String
8492 }
8593 }
8694
87- document = document. add ( commit_dot (
88- idx,
89- branch. visual . column . unwrap ( ) ,
90- branch_color,
91- !info. is_merge ,
92- ) ) ;
95+ document = document. add (
96+ commit_dot (
97+ idx,
98+ branch. visual . column . unwrap ( ) ,
99+ branch_color,
100+ !info. is_merge ,
101+ )
102+ . add ( Title :: new ( & info. oid . to_string ( ) ) ) ,
103+ ) ;
104+
105+ let commit = graph
106+ . repository
107+ . find_commit ( info. oid )
108+ . map_err ( |err| err. message ( ) . to_string ( ) ) ?;
109+
110+ let commit_str = commit. summary ( ) . unwrap_or ( "" ) ;
111+
112+ document = document. add ( draw_summary ( idx, max_column, & commit_str) ) ;
113+
114+ match draw_branches ( idx, branch. visual . column . unwrap ( ) , info, graph) {
115+ Some ( ( branches, width) ) => {
116+ document = document. add ( branches) ;
117+
118+ widest_branch_names = f32:: max ( widest_branch_names, width) ;
119+ }
120+ None => { }
121+ }
122+
123+ widest_summary = f32:: max ( widest_summary, text_bounding_box ( & commit_str, 12.0 ) . 0 ) ;
93124 }
94125 }
126+
95127 let ( x_max, y_max) = commit_coord ( max_idx + 1 , max_column + 1 ) ;
128+
96129 document = document
97- . set ( "viewBox" , ( 0 , 0 , x_max, y_max) )
98- . set ( "width" , x_max)
99- . set ( "height" , y_max) ;
130+ . set (
131+ "viewBox" ,
132+ (
133+ -widest_branch_names,
134+ 0 ,
135+ x_max + widest_branch_names + widest_summary,
136+ y_max,
137+ ) ,
138+ )
139+ . set ( "width" , x_max + widest_branch_names + widest_summary + 15.0 )
140+ . set ( "height" , y_max)
141+ . set ( "style" , "font-family:monospace;font-size:12px;" ) ;
100142
101143 let mut out: Vec < u8 > = vec ! [ ] ;
102144 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
114156 . set ( "stroke-width" , 1 )
115157}
116158
159+ fn draw_branches (
160+ index : usize ,
161+ column : usize ,
162+ info : & CommitInfo ,
163+ graph : & GitGraph ,
164+ ) -> Option < ( Group , f32 ) > {
165+ let ( x, y) = commit_coord ( index, column) ;
166+
167+ let mut branch_names = info
168+ . branches
169+ . iter ( )
170+ . map ( |b| graph. all_branches [ * b] . name . clone ( ) )
171+ . collect :: < Vec < String > > ( ) ;
172+
173+ if graph. head . oid == info. oid {
174+ // Head is here
175+ match branch_names
176+ . iter ( )
177+ . position ( |name| name == & graph. head . name )
178+ {
179+ Some ( index) => {
180+ branch_names. insert ( index + 1 , "HEAD" . to_string ( ) ) ;
181+ }
182+ //Detached HEAD
183+ None => branch_names. push ( "HEAD" . to_string ( ) ) ,
184+ }
185+ }
186+
187+ if branch_names. len ( ) > 0 {
188+ let mut g = Group :: new ( ) ;
189+ let mut start: f32 = 5.0 ;
190+
191+ for branch_name in & branch_names {
192+ let gap = 9.0
193+ + if branch_name == "HEAD" && graph. head . is_branch {
194+ 0.0
195+ } else {
196+ 8.0
197+ } ;
198+ g = g. add ( draw_branch ( start - gap, 2.5 , branch_name) ) ;
199+
200+ start = start - text_bounding_box ( & branch_name, 12.0 ) . 0 - gap;
201+ }
202+
203+ g = g. set ( "transform" , format ! ( "translate({x}, {y})" ) ) ;
204+
205+ Some ( ( g. clone ( ) , -( start + x) ) )
206+ } else {
207+ None
208+ }
209+ }
210+
211+ fn draw_branch ( x : f32 , y : f32 , branch_name : & String ) -> Group {
212+ let width = text_bounding_box ( & branch_name, 12.0 ) . 0 ;
213+
214+ Group :: new ( )
215+ . add ( Text :: new ( branch_name) . set ( "x" , x - width) . set ( "y" , y + 1.0 ) )
216+ . add (
217+ Path :: new ( )
218+ . set (
219+ "d" ,
220+ Data :: new ( )
221+ //Tip
222+ . move_to ( ( x + 2.0 , y + 4.0 ) )
223+ . line_by ( ( 6.0 , -7.0 ) )
224+ . line_by ( ( -6.0 , -7.0 ) )
225+ //Body
226+ . horizontal_line_by ( -width - 11.0 )
227+ //Rear
228+ . line_by ( ( 6.0 , 7.0 ) )
229+ . line_by ( ( -6.0 , 7.0 ) )
230+ . close ( ) ,
231+ )
232+ . set ( "stroke" , "#00000000" )
233+ . set ( "fill" , "#00000030" ) ,
234+ )
235+ }
236+
237+ fn draw_summary ( index : usize , max_column : usize , hash : & str ) -> Text {
238+ let ( x, y) = commit_coord ( index, max_column) ;
239+ Text :: new ( hash)
240+ . set ( "x" , x + 15.0 )
241+ . set ( "y" , y + 2.0 )
242+ . set ( "style" , "font-family:monospace;font-size:12px" )
243+ }
244+
245+ fn text_bounding_box ( text : & str , size : f32 ) -> ( f32 , f32 ) {
246+ // Let's assume the font has a 60% width
247+ ( text. len ( ) as f32 * size * 0.6 , size)
248+ }
249+
117250fn line ( index1 : usize , column1 : usize , index2 : usize , column2 : usize , color : & str ) -> Line {
118251 let ( x1, y1) = commit_coord ( index1, column1) ;
119252 let ( x2, y2) = commit_coord ( index2, column2) ;
@@ -155,12 +288,19 @@ fn path(
155288
156289 let m = ( 0.5 * ( c1. 0 + c2. 0 ) , 0.5 * ( c1. 1 + c2. 1 ) ) ;
157290
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) ;
291+ let data = if column2 > column1 {
292+ Data :: new ( )
293+ . move_to ( c0)
294+ . line_to ( c1)
295+ . line_to ( ( c2. 0 , m. 1 ) )
296+ . line_to ( c3)
297+ } else {
298+ Data :: new ( )
299+ . move_to ( c0)
300+ . line_to ( ( c1. 0 , m. 1 ) )
301+ . line_to ( c2)
302+ . line_to ( c3)
303+ } ;
164304
165305 Path :: new ( )
166306 . set ( "d" , data)
0 commit comments