Browse code

Add tab to draw a graphical representation of the commit history

Guillaume CAMUS authored on12/10/2014 16:09:14
Showing13 changed files
... ...
@@ -9,5 +9,6 @@ $app->mount('', new GitList\Controller\BlobController());
9 9
 $app->mount('', new GitList\Controller\CommitController());
10 10
 $app->mount('', new GitList\Controller\TreeController());
11 11
 $app->mount('', new GitList\Controller\NetworkController());
12
+$app->mount('', new GitList\Controller\TreeGraphController());
12 13
 
13 14
 return $app;
14 15
new file mode 100644
... ...
@@ -0,0 +1,157 @@
1
+<?php
2
+
3
+namespace GitList\Controller;
4
+
5
+use GitList\Git\Repository;
6
+use Gitter\Model\Commit\Commit;
7
+use Silex\Application;
8
+use Silex\ControllerProviderInterface;
9
+use Symfony\Component\HttpFoundation\Request;
10
+
11
+class TreeGraphController implements ControllerProviderInterface
12
+{
13
+    public function connect(Application $app)
14
+    {
15
+        $route = $app['controllers_factory'];
16
+
17
+//        $route->get('{repo}/graph/{commitishPath}/{page}.json',
18
+//            function ($repo, $commitishPath, $page) use ($app) {
19
+//                /** @var $repository Repository */
20
+//                $repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
21
+//
22
+//                if ($commitishPath === null) {
23
+//                    $commitishPath = $repository->getHead();
24
+//                }
25
+//
26
+//                $pager = $app['util.view']->getPager($page, $repository->getTotalCommits($commitishPath));
27
+//                $commits = $repository->getPaginatedCommits($commitishPath, $pager['current']);
28
+//
29
+//                $jsonFormattedCommits = array();
30
+//
31
+//                foreach ($commits as $commit) {
32
+//                    $detailsUrl = $app['url_generator']->generate(
33
+//                        'commit',
34
+//                        array(
35
+//                            'repo' => $repo,
36
+//                            'commit' => $commit->getHash()
37
+//                        )
38
+//                    );
39
+//
40
+//                    $jsonFormattedCommits[$commit->getHash()] = array(
41
+//                        'hash' => $commit->getHash(),
42
+//                        'parentsHash' => $commit->getParentsHash(),
43
+//                        'date' => $commit->getDate()->format('U'),
44
+//                        'message' => htmlentities($commit->getMessage()),
45
+//                        'details' => $detailsUrl,
46
+//                        'author' => array(
47
+//                            'name' => $commit->getAuthor()->getName(),
48
+//                            'email' => $commit->getAuthor()->getEmail(),
49
+//                            // due to the lack of a inbuilt javascript md5 mechanism, build the full avatar url on the php side
50
+//                            'image' => 'http://gravatar.com/avatar/' . md5(
51
+//                                strtolower($commit->getAuthor()->getEmail())
52
+//                            ) . '?s=40'
53
+//                        )
54
+//                    );
55
+//                }
56
+//
57
+//                $nextPageUrl = null;
58
+//
59
+//                if ($pager['last'] !== $pager['current']) {
60
+//                    $nextPageUrl = $app['url_generator']->generate(
61
+//                        'networkData',
62
+//                        array(
63
+//                            'repo' => $repo,
64
+//                            'commitishPath' => $commitishPath,
65
+//                            'page' => $pager['next']
66
+//                        )
67
+//                    );
68
+//                }
69
+//
70
+//				// when no commits are given, return an empty response - issue #369
71
+//				if( count($commits) === 0 ) {
72
+//					return $app->json( array(
73
+//						'repo' => $repo,
74
+//						'commitishPath' => $commitishPath,
75
+//						'nextPage' => null,
76
+//						'start' => null,
77
+//						'commits' => $jsonFormattedCommits
78
+//						), 200
79
+//					);
80
+//				}
81
+//
82
+//                return $app->json( array(
83
+//                    'repo' => $repo,
84
+//                    'commitishPath' => $commitishPath,
85
+//                    'nextPage' => $nextPageUrl,
86
+//                    'start' => $commits[0]->getHash(),
87
+//                    'commits' => $jsonFormattedCommits
88
+//                    ), 200
89
+//                );
90
+//            }
91
+//        )->assert('repo', $app['util.routing']->getRepositoryRegex())
92
+//        ->assert('commitishPath', $app['util.routing']->getCommitishPathRegex())
93
+//        ->value('commitishPath', null)
94
+//        ->convert('commitishPath', 'escaper.argument:escape')
95
+//        ->assert('page', '\d+')
96
+//        ->value('page', '0')
97
+//        ->bind('graphData');
98
+
99
+        $route->get(
100
+            '{repo}/treegraph/{commitishPath}',
101
+            function ($repo, $commitishPath) use ($app) {
102
+                /** @var \GitList\Git\Repository $repository */
103
+                $repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
104
+
105
+                $command = 'log --graph --date-order --all -C -M -n 100 --date=iso ' .
106
+                    '--pretty=format:"B[%d] C[%H] D[%ad] A[%an] E[%ae] H[%h] S[%s]"';
107
+                $rawRows = $repository->getClient()->run($repository, $command);
108
+                $rawRows = explode("\n", $rawRows);
109
+                $graphItems = array();
110
+
111
+                foreach ($rawRows as $row) {
112
+                    if (preg_match("/^(.+?)(\s(B\[(.*?)\])? C\[(.+?)\] D\[(.+?)\] A\[(.+?)\] E\[(.+?)\] H\[(.+?)\] S\[(.+?)\])?$/", $row, $output)) {
113
+                        if (!isset($output[4])) {
114
+                            $graphItems[] = array(
115
+                                "relation"=>$output[1]
116
+                            );
117
+                            continue;
118
+                        }
119
+                        $graphItems[] = array(
120
+                            "relation"=>$output[1],
121
+                            "branch"=>$output[4],
122
+                            "rev"=>$output[5],
123
+                            "date"=>$output[6],
124
+                            "author"=>$output[7],
125
+                            "author_email"=>$output[8],
126
+                            "short_rev"=>$output[9],
127
+                            "subject"=>preg_replace('/(^|\s)(#[[:xdigit:]]+)(\s|$)/', '$1<a href="$2">$2</a>$3', $output[10])
128
+                        );
129
+                    }
130
+                }
131
+
132
+                if ($commitishPath === null) {
133
+                    $commitishPath = $repository->getHead();
134
+                }
135
+
136
+                list($branch, $file) = $app['util.routing']->parseCommitishPathParam($commitishPath, $repo);
137
+                list($branch, $file) = $app['util.repository']->extractRef($repository, $branch, $file);
138
+
139
+                return $app['twig']->render(
140
+                    'treegraph.twig',
141
+                    array(
142
+                        'repo' => $repo,
143
+                        'branch' => $branch,
144
+                        'commitishPath' => $commitishPath,
145
+                        'graphItems' => $graphItems,
146
+                    )
147
+                );
148
+            }
149
+        )->assert('repo', $app['util.routing']->getRepositoryRegex())
150
+            ->assert('commitishPath', $app['util.routing']->getCommitishPathRegex())
151
+            ->value('commitishPath', null)
152
+            ->convert('commitishPath', 'escaper.argument:escape')
153
+            ->bind('treegraph');
154
+
155
+        return $route;
156
+    }
157
+}
0 158
new file mode 100644
... ...
@@ -0,0 +1,14 @@
1
+body {font:13.34px/1.4 helvetica,arial,freesans,clean,sans-serif;}
2
+em {font-style:normal;}
3
+
4
+#git-graph-container{ clear: both}
5
+#rev-container, #rel-container {float:left;}
6
+#git-graph-container li {list-style-type:none;height:28px;line-height:27px;overflow:hidden;}
7
+#git-graph-container li .node-relation {font-family:'Bitstream Vera Sans Mono', 'Courier', monospace;}
8
+#git-graph-container li .author {color:#666666;}
9
+#git-graph-container li .time {color:#999999;font-size:80%}
10
+#git-graph-container li a {color:#000000;}
11
+#git-graph-container li a em {color:#BB0000;border-bottom:1px dotted #BBBBBB;text-decoration:none;font-style:normal;}
12
+
13
+#rev-list {margin:0;padding:0 5px 0 0;}
14
+#graph-raw-list {margin:0px;}
0 15
\ No newline at end of file
1 16
new file mode 100644
... ...
@@ -0,0 +1,17 @@
1
+$(document).ready(function () {
2
+    var graphList = [];
3
+
4
+    if (!document.getElementById('graph-canvas')) {
5
+        return;
6
+    }
7
+
8
+    $("#graph-raw-list li span.node-relation").each(function () {
9
+        graphList.push($(this).text());
10
+    })
11
+
12
+    gitGraph(document.getElementById('graph-canvas'), graphList);
13
+
14
+    if ($("#rev-container")) {
15
+        $("#rev-container").css("width", $('#git-graph-container').width() - $('#graph-canvas').width());
16
+    }
17
+})
0 18
new file mode 100644
... ...
@@ -0,0 +1,399 @@
1
+/*
2
+ * Copyright (c) 2011, Terrence Lee <kill889@gmail.com>
3
+ * All rights reserved.
4
+ * 
5
+ * Redistribution and use in source and binary forms, with or without
6
+ * modification, are permitted provided that the following conditions are met:
7
+ *     * Redistributions of source code must retain the above copyright
8
+ *       notice, this list of conditions and the following disclaimer.
9
+ *     * Redistributions in binary form must reproduce the above copyright
10
+ *       notice, this list of conditions and the following disclaimer in the
11
+ *       documentation and/or other materials provided with the distribution.
12
+ *     * Neither the name of the <organization> nor the
13
+ *       names of its contributors may be used to endorse or promote products
14
+ *       derived from this software without specific prior written permission.
15
+ * 
16
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
20
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
+ */
27
+
28
+var gitGraph = function (canvas, rawGraphList, config) {
29
+	if (!canvas.getContext) {
30
+		return;
31
+	}
32
+	
33
+	if (typeof config === "undefined") {
34
+		config = {
35
+			unitSize: 28,
36
+			lineWidth: 3,
37
+			nodeRadius: 6
38
+		};
39
+	}
40
+	
41
+	var flows = [];
42
+	var graphList = [];
43
+	
44
+	var ctx = canvas.getContext("2d");
45
+	
46
+	var init = function () {
47
+		var maxWidth = 0;
48
+		var i;
49
+		var l = rawGraphList.length;
50
+		var row;
51
+		var midStr;
52
+		
53
+		for (i = 0; i < l; i++) {
54
+			midStr = rawGraphList[i].replace(/\s+/g, " ").replace(/^\s+|\s+$/g, "");
55
+			
56
+			maxWidth = Math.max(midStr.replace(/(\_|\s)/g, "").length, maxWidth);
57
+			
58
+			row = midStr.split("");
59
+			
60
+			graphList.unshift(row);
61
+		}
62
+		
63
+		canvas.width = maxWidth * config.unitSize;
64
+		canvas.height = graphList.length * config.unitSize;
65
+		
66
+		ctx.lineWidth = config.lineWidth;
67
+		ctx.lineJoin = "round";
68
+		ctx.lineCap = "round";
69
+	};
70
+	
71
+	var genRandomStr = function () {
72
+		var chars = "0123456789ABCDEF";
73
+		var stringLength = 6;
74
+		var randomString = '', rnum, i;
75
+		for (i = 0; i < stringLength; i++) {
76
+			rnum = Math.floor(Math.random() * chars.length);
77
+			randomString += chars.substring(rnum, rnum + 1);
78
+		}
79
+		
80
+		return randomString;
81
+	};
82
+	
83
+	var findFlow = function (id) {
84
+		var i = flows.length;
85
+		
86
+		while (i-- && flows[i].id !== id) {}
87
+		
88
+		return i;
89
+	};
90
+	
91
+	var findColomn = function (symbol, row) {
92
+		var i = row.length;
93
+		
94
+		while (i-- && row[i] !== symbol) {}
95
+		
96
+		return i;
97
+	};
98
+	
99
+	var findBranchOut = function (row) {
100
+		if (!row) {
101
+			return -1
102
+		}
103
+		
104
+		var i = row.length;
105
+		
106
+		while (i-- && 
107
+			!(row[i - 1] && row[i] === "/" && row[i - 1] === "|") &&
108
+			!(row[i - 2] && row[i] === "_" && row[i - 2] === "|")) {}
109
+		
110
+		return i;
111
+	}
112
+	
113
+	var genNewFlow = function () {
114
+		var newId;
115
+		
116
+		do {
117
+			newId = genRandomStr();
118
+		} while (findFlow(newId) !== -1);
119
+		
120
+		return {id:newId, color:"#" + newId};
121
+	};
122
+	
123
+	//draw method
124
+	var drawLineRight = function (x, y, color) {
125
+		ctx.strokeStyle = color;
126
+		ctx.beginPath();
127
+		ctx.moveTo(x, y + config.unitSize / 2);
128
+		ctx.lineTo(x + config.unitSize, y + config.unitSize / 2);
129
+		ctx.stroke();
130
+	};
131
+	
132
+	var drawLineUp = function (x, y, color) {
133
+		ctx.strokeStyle = color;
134
+		ctx.beginPath();
135
+		ctx.moveTo(x, y + config.unitSize / 2);
136
+		ctx.lineTo(x, y - config.unitSize / 2);
137
+		ctx.stroke();
138
+	};
139
+	
140
+	var drawNode = function (x, y, color) {
141
+		ctx.strokeStyle = color;
142
+		
143
+		drawLineUp(x, y, color);
144
+		
145
+		ctx.beginPath();
146
+		ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true);
147
+		ctx.fill();
148
+	};
149
+	
150
+	var drawLineIn = function (x, y, color) {
151
+		ctx.strokeStyle = color;
152
+		
153
+		ctx.beginPath();
154
+		ctx.moveTo(x + config.unitSize, y + config.unitSize / 2);
155
+		ctx.lineTo(x, y - config.unitSize / 2);
156
+		ctx.stroke();
157
+	};
158
+	
159
+	var drawLineOut = function (x, y, color) {
160
+		ctx.strokeStyle = color;
161
+		ctx.beginPath();
162
+		ctx.moveTo(x, y + config.unitSize / 2);
163
+		ctx.lineTo(x + config.unitSize, y - config.unitSize / 2);
164
+		ctx.stroke();
165
+	};
166
+	
167
+	var draw = function (graphList) {
168
+		var colomn, colomnIndex, prevColomn, condenseIndex;
169
+		var x, y;
170
+		var color;
171
+		var nodePos, outPos;
172
+		var tempFlow;
173
+		var prevRowLength = 0;
174
+		var flowSwapPos = -1;
175
+		var lastLinePos;
176
+		var i, k, l;
177
+		var condenseCurrentLength, condensePrevLength = 0, condenseNextLength = 0;
178
+		
179
+		var inlineIntersect = false;
180
+		
181
+		//initiate for first row
182
+		for (i = 0, l = graphList[0].length; i < l; i++) {
183
+			if (graphList[0][i] !== "_" && graphList[0][i] !== " ") {
184
+				flows.push(genNewFlow());
185
+			}
186
+		}
187
+		
188
+		y = canvas.height - config.unitSize * 0.5;
189
+		
190
+		//iterate
191
+		for (i = 0, l = graphList.length; i < l; i++) {
192
+			x = config.unitSize * 0.5;
193
+			
194
+			currentRow = graphList[i];
195
+			nextRow = graphList[i + 1];
196
+			prevRow = graphList[i - 1];
197
+			
198
+			flowSwapPos = -1;
199
+			
200
+			condenseCurrentLength = currentRow.filter(function (val) {
201
+				return (val !== " "  && val !== "_")
202
+			}).length;
203
+			
204
+			if (nextRow) {
205
+				condenseNextLength = nextRow.filter(function (val) {
206
+					return (val !== " "  && val !== "_")
207
+				}).length;
208
+			} else {
209
+				condenseNextLength = 0;
210
+			}
211
+			
212
+			//pre process begin
213
+			//use last row for analysing
214
+			if (prevRow) {
215
+				if (!inlineIntersect) {
216
+					//intersect might happen
217
+					for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) {
218
+						if (prevRow[colomnIndex + 1] && 
219
+							(prevRow[colomnIndex] === "/" && prevRow[colomnIndex + 1] === "|") || 
220
+							((prevRow[colomnIndex] === "_" && prevRow[colomnIndex + 1] === "|") &&
221
+							(prevRow[colomnIndex + 2] === "/"))) {
222
+							
223
+							flowSwapPos = colomnIndex;
224
+							
225
+							//swap two flow
226
+							tempFlow = {id:flows[flowSwapPos].id, color:flows[flowSwapPos].color};
227
+							
228
+							flows[flowSwapPos].id = flows[flowSwapPos + 1].id;
229
+							flows[flowSwapPos].color = flows[flowSwapPos + 1].color;
230
+							
231
+							flows[flowSwapPos + 1].id = tempFlow.id;
232
+							flows[flowSwapPos + 1].color = tempFlow.color;
233
+						}
234
+					}
235
+				}
236
+				
237
+				if (condensePrevLength < condenseCurrentLength &&
238
+					((nodePos = findColomn("*", currentRow)) !== -1 &&
239
+					(findColomn("_", currentRow) === -1))) {
240
+					
241
+					flows.splice(nodePos - 1, 0, genNewFlow());
242
+				}
243
+				
244
+				if (prevRowLength > currentRow.length &&
245
+					(nodePos = findColomn("*", prevRow)) !== -1) {
246
+					
247
+					if (findColomn("_", currentRow) === -1 &&
248
+						findColomn("/", currentRow) === -1 && 
249
+						findColomn("\\", currentRow) === -1) {
250
+						
251
+						flows.splice(nodePos + 1, 1);
252
+					}
253
+				}
254
+			} //done with the previous row
255
+			
256
+			prevRowLength = currentRow.length; //store for next round
257
+			colomnIndex = 0; //reset index
258
+			condenseIndex = 0;
259
+			condensePrevLength = 0;
260
+			while (colomnIndex < currentRow.length) {
261
+				colomn = currentRow[colomnIndex];
262
+				
263
+				if (colomn !== " " && colomn !== "_") {
264
+					++condensePrevLength;
265
+				}
266
+				
267
+				if (colomn === " " && 
268
+					currentRow[colomnIndex + 1] &&
269
+					currentRow[colomnIndex + 1] === "_" &&
270
+					currentRow[colomnIndex - 1] && 
271
+					currentRow[colomnIndex - 1] === "|") {
272
+					
273
+					currentRow.splice(colomnIndex, 1);
274
+					
275
+					currentRow[colomnIndex] = "/";
276
+					colomn = "/";
277
+				}
278
+				
279
+				//create new flow only when no intersetc happened
280
+				if (flowSwapPos === -1 &&
281
+					colomn === "/" &&
282
+					currentRow[colomnIndex - 1] && 
283
+					currentRow[colomnIndex - 1] === "|") {
284
+					
285
+					flows.splice(condenseIndex, 0, genNewFlow());
286
+				}
287
+				
288
+				//change \ and / to | when it's in the last position of the whole row
289
+				if (colomn === "/" || colomn === "\\") {
290
+					if (!(colomn === "/" && findBranchOut(nextRow) === -1)) {
291
+						if ((lastLinePos = Math.max(findColomn("|", currentRow), 
292
+													findColomn("*", currentRow))) !== -1 &&
293
+							(lastLinePos < colomnIndex - 1)) {
294
+							
295
+							while (currentRow[++lastLinePos] === " ") {}
296
+							
297
+							if (lastLinePos === colomnIndex) {
298
+								currentRow[colomnIndex] = "|";
299
+							}
300
+						}
301
+					}
302
+				}
303
+				
304
+				if (colomn === "*" &&
305
+					prevRow && 
306
+					prevRow[condenseIndex + 1] === "\\") {
307
+					flows.splice(condenseIndex + 1, 1);
308
+				}
309
+				
310
+				if (colomn !== " ") {
311
+					++condenseIndex;
312
+				}
313
+				
314
+				++colomnIndex;
315
+			}
316
+			
317
+			condenseCurrentLength = currentRow.filter(function (val) {
318
+				return (val !== " "  && val !== "_")
319
+			}).length;
320
+			
321
+			//do some clean up
322
+			if (flows.length > condenseCurrentLength) {
323
+				flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength);
324
+			}
325
+			
326
+			colomnIndex = 0;
327
+			
328
+			//a little inline analysis and draw process
329
+			while (colomnIndex < currentRow.length) {
330
+				colomn = currentRow[colomnIndex];
331
+				prevColomn = currentRow[colomnIndex - 1];
332
+				
333
+				if (currentRow[colomnIndex] === " ") {
334
+					currentRow.splice(colomnIndex, 1);
335
+					x += config.unitSize;
336
+					
337
+					continue;
338
+				}
339
+				
340
+				//inline interset
341
+				if ((colomn === "_" || colomn === "/") &&
342
+					currentRow[colomnIndex - 1] === "|" &&
343
+					currentRow[colomnIndex - 2] === "_") {
344
+					
345
+					inlineIntersect = true;
346
+					
347
+					tempFlow = flows.splice(colomnIndex - 2, 1)[0];
348
+					flows.splice(colomnIndex - 1, 0, tempFlow);
349
+					currentRow.splice(colomnIndex - 2, 1);
350
+					
351
+					colomnIndex = colomnIndex - 1;
352
+				} else {
353
+					inlineIntersect = false;
354
+				}
355
+				
356
+				color = flows[colomnIndex].color;
357
+				
358
+				switch (colomn) {
359
+					case "_" :
360
+						drawLineRight(x, y, color);
361
+						
362
+						x += config.unitSize;
363
+						break;
364
+						
365
+					case "*" :
366
+						drawNode(x, y, color);
367
+						break;
368
+						
369
+					case "|" :
370
+						drawLineUp(x, y, color);
371
+						break;
372
+						
373
+					case "/" :
374
+						if (prevColomn && 
375
+							(prevColomn === "/" || 
376
+							prevColomn === " ")) {
377
+							x -= config.unitSize;
378
+						}
379
+						
380
+						drawLineOut(x, y, color);
381
+						
382
+						x += config.unitSize;
383
+						break;
384
+						
385
+					case "\\" :
386
+						drawLineIn(x, y, color);
387
+						break;
388
+				}
389
+				
390
+				++colomnIndex;
391
+			}
392
+			
393
+			y -= config.unitSize;
394
+		}
395
+	};
396
+	
397
+	init();
398
+	draw(graphList);
399
+};
0 400
\ No newline at end of file
... ...
@@ -5,6 +5,7 @@
5 5
         <title>{% block title %}Welcome!{% endblock %}</title>
6 6
         <link rel="stylesheet" type="text/css" href="{{ app.request.basepath }}/themes/{{ app.theme }}/css/style.css">
7 7
         <link rel="stylesheet" type="text/css" href="{{ app.request.basepath }}/themes/{{ app.theme }}/css/fontawesome.css">
8
+        <link rel="stylesheet" type="text/css" href="{{ app.request.basepath }}/themes/{{ app.theme }}/css/gitgraph.css">
8 9
         <link rel="shortcut icon" type="image/png" href="{{ app.request.basepath }}/themes/{{ app.theme }}/img/favicon.png" />
9 10
         <!--[if lt IE 9]>
10 11
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/html5.js"></script>
... ...
@@ -13,6 +14,7 @@
13 14
 
14 15
     <body>
15 16
         {% block body %}{% endblock %}
17
+        {% block javascripts %}
16 18
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/jquery.js"></script>
17 19
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/raphael.js"></script>
18 20
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/bootstrap.js"></script>
... ...
@@ -21,5 +23,8 @@
21 23
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/table.js"></script>
22 24
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/main.js"></script>
23 25
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/networkGraph.js"></script>
26
+        <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/gitgraph.js"></script>
27
+        <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/draw.js"></script>
28
+        {% endblock %}
24 29
     </body>
25 30
 </html>
26 31
new file mode 100644
... ...
@@ -0,0 +1,48 @@
1
+{% extends 'layout_page.twig' %}
2
+
3
+{% set page = 'treegraph' %}
4
+
5
+{% block title %}GitList{% endblock %}
6
+
7
+{% block content %}
8
+    {% include 'breadcrumb.twig' with {breadcrumbs: [{dir: 'Graph', path:''}]} %}
9
+    <div class="network-view">
10
+        <div class="network-header">
11
+            <div class="meta">Graph of {{ repo }} </div>
12
+        </div>
13
+
14
+        <div id="git-graph-container">
15
+            <div id="rel-container">
16
+                <canvas id="graph-canvas" width="100px">
17
+                    <ul id="graph-raw-list">
18
+                        {% for item in graphItems %}
19
+                            <li><span class="node-relation">{{ item.relation }}</span></li>
20
+                        {% endfor %}
21
+                    </ul>
22
+                </canvas>
23
+            </div>
24
+            <div style="float:left;" id="rev-container">
25
+                <ul id="rev-list">
26
+                    {% for item in graphItems %}
27
+                        <li>
28
+                            {% if item.rev is defined %}
29
+                                <a id="{{ item.short_rev }}" class="btn btn-default btn-sm" href="{{ path('commit', {repo: repo, commit: item.rev}) }}"> {{ item.short_rev }} </a>
30
+                                <strong> {{ item.branch }} </strong>
31
+                                <em>{{ item.subject }}</em> by
32
+                                <span class="author">{{ item.author }} &lt;{{ item.author_email }}&gt;</span>
33
+                                <span class="time">{{ item.date }}</span>;
34
+                            {% else %}
35
+                                <span/>
36
+                            {% endif %}
37
+                        </li>
38
+                    {% endfor %}
39
+                </ul>
40
+            </div>
41
+            <div style="clear:both"><!-- --></div>
42
+        </div>
43
+    </div>
44
+
45
+
46
+
47
+    <hr/>
48
+{% endblock %}
0 49
new file mode 100644
... ...
@@ -0,0 +1,14 @@
1
+body {font:13.34px/1.4 helvetica,arial,freesans,clean,sans-serif;}
2
+em {font-style:normal;}
3
+
4
+#git-graph-container{ clear: both}
5
+#rev-container, #rel-container {float:left;}
6
+#git-graph-container li {list-style-type:none;height:28px;line-height:27px;overflow:hidden;}
7
+#git-graph-container li .node-relation {font-family:'Bitstream Vera Sans Mono', 'Courier', monospace;}
8
+#git-graph-container li .author {color:#666666;}
9
+#git-graph-container li .time {color:#999999;font-size:80%}
10
+#git-graph-container li a {color:#000000;}
11
+#git-graph-container li a em {color:#BB0000;border-bottom:1px dotted #BBBBBB;text-decoration:none;font-style:normal;}
12
+
13
+#rev-list {margin:0;padding:0 5px 0 0;}
14
+#graph-raw-list {margin:0px;}
0 15
\ No newline at end of file
1 16
new file mode 100644
... ...
@@ -0,0 +1,17 @@
1
+$(document).ready(function () {
2
+    var graphList = [];
3
+
4
+    if (!document.getElementById('graph-canvas')) {
5
+        return;
6
+    }
7
+
8
+    $("#graph-raw-list li span.node-relation").each(function () {
9
+        graphList.push($(this).text());
10
+    })
11
+
12
+    gitGraph(document.getElementById('graph-canvas'), graphList);
13
+
14
+    if ($("#rev-container")) {
15
+        $("#rev-container").css("width", $('#git-graph-container').width() - $('#graph-canvas').width());
16
+    }
17
+})
0 18
new file mode 100644
... ...
@@ -0,0 +1,399 @@
1
+/*
2
+ * Copyright (c) 2011, Terrence Lee <kill889@gmail.com>
3
+ * All rights reserved.
4
+ * 
5
+ * Redistribution and use in source and binary forms, with or without
6
+ * modification, are permitted provided that the following conditions are met:
7
+ *     * Redistributions of source code must retain the above copyright
8
+ *       notice, this list of conditions and the following disclaimer.
9
+ *     * Redistributions in binary form must reproduce the above copyright
10
+ *       notice, this list of conditions and the following disclaimer in the
11
+ *       documentation and/or other materials provided with the distribution.
12
+ *     * Neither the name of the <organization> nor the
13
+ *       names of its contributors may be used to endorse or promote products
14
+ *       derived from this software without specific prior written permission.
15
+ * 
16
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
20
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
+ */
27
+
28
+var gitGraph = function (canvas, rawGraphList, config) {
29
+	if (!canvas.getContext) {
30
+		return;
31
+	}
32
+	
33
+	if (typeof config === "undefined") {
34
+		config = {
35
+			unitSize: 28,
36
+			lineWidth: 3,
37
+			nodeRadius: 6
38
+		};
39
+	}
40
+	
41
+	var flows = [];
42
+	var graphList = [];
43
+	
44
+	var ctx = canvas.getContext("2d");
45
+	
46
+	var init = function () {
47
+		var maxWidth = 0;
48
+		var i;
49
+		var l = rawGraphList.length;
50
+		var row;
51
+		var midStr;
52
+		
53
+		for (i = 0; i < l; i++) {
54
+			midStr = rawGraphList[i].replace(/\s+/g, " ").replace(/^\s+|\s+$/g, "");
55
+			
56
+			maxWidth = Math.max(midStr.replace(/(\_|\s)/g, "").length, maxWidth);
57
+			
58
+			row = midStr.split("");
59
+			
60
+			graphList.unshift(row);
61
+		}
62
+		
63
+		canvas.width = maxWidth * config.unitSize;
64
+		canvas.height = graphList.length * config.unitSize;
65
+		
66
+		ctx.lineWidth = config.lineWidth;
67
+		ctx.lineJoin = "round";
68
+		ctx.lineCap = "round";
69
+	};
70
+	
71
+	var genRandomStr = function () {
72
+		var chars = "0123456789ABCDEF";
73
+		var stringLength = 6;
74
+		var randomString = '', rnum, i;
75
+		for (i = 0; i < stringLength; i++) {
76
+			rnum = Math.floor(Math.random() * chars.length);
77
+			randomString += chars.substring(rnum, rnum + 1);
78
+		}
79
+		
80
+		return randomString;
81
+	};
82
+	
83
+	var findFlow = function (id) {
84
+		var i = flows.length;
85
+		
86
+		while (i-- && flows[i].id !== id) {}
87
+		
88
+		return i;
89
+	};
90
+	
91
+	var findColomn = function (symbol, row) {
92
+		var i = row.length;
93
+		
94
+		while (i-- && row[i] !== symbol) {}
95
+		
96
+		return i;
97
+	};
98
+	
99
+	var findBranchOut = function (row) {
100
+		if (!row) {
101
+			return -1
102
+		}
103
+		
104
+		var i = row.length;
105
+		
106
+		while (i-- && 
107
+			!(row[i - 1] && row[i] === "/" && row[i - 1] === "|") &&
108
+			!(row[i - 2] && row[i] === "_" && row[i - 2] === "|")) {}
109
+		
110
+		return i;
111
+	}
112
+	
113
+	var genNewFlow = function () {
114
+		var newId;
115
+		
116
+		do {
117
+			newId = genRandomStr();
118
+		} while (findFlow(newId) !== -1);
119
+		
120
+		return {id:newId, color:"#" + newId};
121
+	};
122
+	
123
+	//draw method
124
+	var drawLineRight = function (x, y, color) {
125
+		ctx.strokeStyle = color;
126
+		ctx.beginPath();
127
+		ctx.moveTo(x, y + config.unitSize / 2);
128
+		ctx.lineTo(x + config.unitSize, y + config.unitSize / 2);
129
+		ctx.stroke();
130
+	};
131
+	
132
+	var drawLineUp = function (x, y, color) {
133
+		ctx.strokeStyle = color;
134
+		ctx.beginPath();
135
+		ctx.moveTo(x, y + config.unitSize / 2);
136
+		ctx.lineTo(x, y - config.unitSize / 2);
137
+		ctx.stroke();
138
+	};
139
+	
140
+	var drawNode = function (x, y, color) {
141
+		ctx.strokeStyle = color;
142
+		
143
+		drawLineUp(x, y, color);
144
+		
145
+		ctx.beginPath();
146
+		ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true);
147
+		ctx.fill();
148
+	};
149
+	
150
+	var drawLineIn = function (x, y, color) {
151
+		ctx.strokeStyle = color;
152
+		
153
+		ctx.beginPath();
154
+		ctx.moveTo(x + config.unitSize, y + config.unitSize / 2);
155
+		ctx.lineTo(x, y - config.unitSize / 2);
156
+		ctx.stroke();
157
+	};
158
+	
159
+	var drawLineOut = function (x, y, color) {
160
+		ctx.strokeStyle = color;
161
+		ctx.beginPath();
162
+		ctx.moveTo(x, y + config.unitSize / 2);
163
+		ctx.lineTo(x + config.unitSize, y - config.unitSize / 2);
164
+		ctx.stroke();
165
+	};
166
+	
167
+	var draw = function (graphList) {
168
+		var colomn, colomnIndex, prevColomn, condenseIndex;
169
+		var x, y;
170
+		var color;
171
+		var nodePos, outPos;
172
+		var tempFlow;
173
+		var prevRowLength = 0;
174
+		var flowSwapPos = -1;
175
+		var lastLinePos;
176
+		var i, k, l;
177
+		var condenseCurrentLength, condensePrevLength = 0, condenseNextLength = 0;
178
+		
179
+		var inlineIntersect = false;
180
+		
181
+		//initiate for first row
182
+		for (i = 0, l = graphList[0].length; i < l; i++) {
183
+			if (graphList[0][i] !== "_" && graphList[0][i] !== " ") {
184
+				flows.push(genNewFlow());
185
+			}
186
+		}
187
+		
188
+		y = canvas.height - config.unitSize * 0.5;
189
+		
190
+		//iterate
191
+		for (i = 0, l = graphList.length; i < l; i++) {
192
+			x = config.unitSize * 0.5;
193
+			
194
+			currentRow = graphList[i];
195
+			nextRow = graphList[i + 1];
196
+			prevRow = graphList[i - 1];
197
+			
198
+			flowSwapPos = -1;
199
+			
200
+			condenseCurrentLength = currentRow.filter(function (val) {
201
+				return (val !== " "  && val !== "_")
202
+			}).length;
203
+			
204
+			if (nextRow) {
205
+				condenseNextLength = nextRow.filter(function (val) {
206
+					return (val !== " "  && val !== "_")
207
+				}).length;
208
+			} else {
209
+				condenseNextLength = 0;
210
+			}
211
+			
212
+			//pre process begin
213
+			//use last row for analysing
214
+			if (prevRow) {
215
+				if (!inlineIntersect) {
216
+					//intersect might happen
217
+					for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) {
218
+						if (prevRow[colomnIndex + 1] && 
219
+							(prevRow[colomnIndex] === "/" && prevRow[colomnIndex + 1] === "|") || 
220
+							((prevRow[colomnIndex] === "_" && prevRow[colomnIndex + 1] === "|") &&
221
+							(prevRow[colomnIndex + 2] === "/"))) {
222
+							
223
+							flowSwapPos = colomnIndex;
224
+							
225
+							//swap two flow
226
+							tempFlow = {id:flows[flowSwapPos].id, color:flows[flowSwapPos].color};
227
+							
228
+							flows[flowSwapPos].id = flows[flowSwapPos + 1].id;
229
+							flows[flowSwapPos].color = flows[flowSwapPos + 1].color;
230
+							
231
+							flows[flowSwapPos + 1].id = tempFlow.id;
232
+							flows[flowSwapPos + 1].color = tempFlow.color;
233
+						}
234
+					}
235
+				}
236
+				
237
+				if (condensePrevLength < condenseCurrentLength &&
238
+					((nodePos = findColomn("*", currentRow)) !== -1 &&
239
+					(findColomn("_", currentRow) === -1))) {
240
+					
241
+					flows.splice(nodePos - 1, 0, genNewFlow());
242
+				}
243
+				
244
+				if (prevRowLength > currentRow.length &&
245
+					(nodePos = findColomn("*", prevRow)) !== -1) {
246
+					
247
+					if (findColomn("_", currentRow) === -1 &&
248
+						findColomn("/", currentRow) === -1 && 
249
+						findColomn("\\", currentRow) === -1) {
250
+						
251
+						flows.splice(nodePos + 1, 1);
252
+					}
253
+				}
254
+			} //done with the previous row
255
+			
256
+			prevRowLength = currentRow.length; //store for next round
257
+			colomnIndex = 0; //reset index
258
+			condenseIndex = 0;
259
+			condensePrevLength = 0;
260
+			while (colomnIndex < currentRow.length) {
261
+				colomn = currentRow[colomnIndex];
262
+				
263
+				if (colomn !== " " && colomn !== "_") {
264
+					++condensePrevLength;
265
+				}
266
+				
267
+				if (colomn === " " && 
268
+					currentRow[colomnIndex + 1] &&
269
+					currentRow[colomnIndex + 1] === "_" &&
270
+					currentRow[colomnIndex - 1] && 
271
+					currentRow[colomnIndex - 1] === "|") {
272
+					
273
+					currentRow.splice(colomnIndex, 1);
274
+					
275
+					currentRow[colomnIndex] = "/";
276
+					colomn = "/";
277
+				}
278
+				
279
+				//create new flow only when no intersetc happened
280
+				if (flowSwapPos === -1 &&
281
+					colomn === "/" &&
282
+					currentRow[colomnIndex - 1] && 
283
+					currentRow[colomnIndex - 1] === "|") {
284
+					
285
+					flows.splice(condenseIndex, 0, genNewFlow());
286
+				}
287
+				
288
+				//change \ and / to | when it's in the last position of the whole row
289
+				if (colomn === "/" || colomn === "\\") {
290
+					if (!(colomn === "/" && findBranchOut(nextRow) === -1)) {
291
+						if ((lastLinePos = Math.max(findColomn("|", currentRow), 
292
+													findColomn("*", currentRow))) !== -1 &&
293
+							(lastLinePos < colomnIndex - 1)) {
294
+							
295
+							while (currentRow[++lastLinePos] === " ") {}
296
+							
297
+							if (lastLinePos === colomnIndex) {
298
+								currentRow[colomnIndex] = "|";
299
+							}
300
+						}
301
+					}
302
+				}
303
+				
304
+				if (colomn === "*" &&
305
+					prevRow && 
306
+					prevRow[condenseIndex + 1] === "\\") {
307
+					flows.splice(condenseIndex + 1, 1);
308
+				}
309
+				
310
+				if (colomn !== " ") {
311
+					++condenseIndex;
312
+				}
313
+				
314
+				++colomnIndex;
315
+			}
316
+			
317
+			condenseCurrentLength = currentRow.filter(function (val) {
318
+				return (val !== " "  && val !== "_")
319
+			}).length;
320
+			
321
+			//do some clean up
322
+			if (flows.length > condenseCurrentLength) {
323
+				flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength);
324
+			}
325
+			
326
+			colomnIndex = 0;
327
+			
328
+			//a little inline analysis and draw process
329
+			while (colomnIndex < currentRow.length) {
330
+				colomn = currentRow[colomnIndex];
331
+				prevColomn = currentRow[colomnIndex - 1];
332
+				
333
+				if (currentRow[colomnIndex] === " ") {
334
+					currentRow.splice(colomnIndex, 1);
335
+					x += config.unitSize;
336
+					
337
+					continue;
338
+				}
339
+				
340
+				//inline interset
341
+				if ((colomn === "_" || colomn === "/") &&
342
+					currentRow[colomnIndex - 1] === "|" &&
343
+					currentRow[colomnIndex - 2] === "_") {
344
+					
345
+					inlineIntersect = true;
346
+					
347
+					tempFlow = flows.splice(colomnIndex - 2, 1)[0];
348
+					flows.splice(colomnIndex - 1, 0, tempFlow);
349
+					currentRow.splice(colomnIndex - 2, 1);
350
+					
351
+					colomnIndex = colomnIndex - 1;
352
+				} else {
353
+					inlineIntersect = false;
354
+				}
355
+				
356
+				color = flows[colomnIndex].color;
357
+				
358
+				switch (colomn) {
359
+					case "_" :
360
+						drawLineRight(x, y, color);
361
+						
362
+						x += config.unitSize;
363
+						break;
364
+						
365
+					case "*" :
366
+						drawNode(x, y, color);
367
+						break;
368
+						
369
+					case "|" :
370
+						drawLineUp(x, y, color);
371
+						break;
372
+						
373
+					case "/" :
374
+						if (prevColomn && 
375
+							(prevColomn === "/" || 
376
+							prevColomn === " ")) {
377
+							x -= config.unitSize;
378
+						}
379
+						
380
+						drawLineOut(x, y, color);
381
+						
382
+						x += config.unitSize;
383
+						break;
384
+						
385
+					case "\\" :
386
+						drawLineIn(x, y, color);
387
+						break;
388
+				}
389
+				
390
+				++colomnIndex;
391
+			}
392
+			
393
+			y -= config.unitSize;
394
+		}
395
+	};
396
+	
397
+	init();
398
+	draw(graphList);
399
+};
0 400
\ No newline at end of file
... ...
@@ -4,6 +4,7 @@
4 4
         <meta charset="UTF-8" />
5 5
         <title>{{ app.title }}{% if app.title %} - {% endif %}{% block title %}Welcome!{% endblock %}</title>
6 6
         <link rel="stylesheet" type="text/css" href="{{ app.request.basepath }}/themes/{{ app.theme }}/css/style.css">
7
+        <link rel="stylesheet" type="text/css" href="{{ app.request.basepath }}/themes/{{ app.theme }}/css/gitgraph.css">
7 8
         <link rel="shortcut icon" type="image/png" href="{{ app.request.basepath }}/themes/{{ app.theme }}/img/favicon.png" />
8 9
         <!--[if lt IE 9]>
9 10
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/html5.js"></script>
... ...
@@ -12,6 +13,7 @@
12 13
 
13 14
     <body>
14 15
         {% block body %}{% endblock %}
16
+        {% block javascripts %}
15 17
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/jquery.js"></script>
16 18
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/raphael.js"></script>
17 19
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/bootstrap.js"></script>
... ...
@@ -21,5 +23,8 @@
21 23
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/list.min.js"></script>
22 24
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/main.js"></script>
23 25
         <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/networkGraph.js"></script>
26
+        <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/gitgraph.js"></script>
27
+        <script src="{{ app.request.basepath }}/themes/{{ app.theme }}/js/draw.js"></script>
28
+        {% endblock %}
24 29
     </body>
25 30
 </html>
... ...
@@ -3,4 +3,5 @@
3 3
     <li{% if page in ['commits', 'searchcommits'] %} class="active"{% endif %}><a href="{{ path('commits', {repo: repo, commitishPath: branch}) }}">Commits</a></li>
4 4
     <li{% if page == 'stats' %} class="active"{% endif %}><a href="{{ path('stats', {repo: repo, branch: branch}) }}">Stats</a></li>
5 5
   	<li{% if page == 'network' %} class="active"{% endif %}><a href="{{ path('network', {repo: repo, branch: branch}) }}">Network</a></li>
6
+  	<li{% if page == 'treegraph' %} class="active"{% endif %}><a href="{{ path('treegraph', {repo: repo, branch: branch}) }}">Graph</a></li>
6 7
 </ul>
7 8
new file mode 100644
... ...
@@ -0,0 +1,48 @@
1
+{% extends 'layout_page.twig' %}
2
+
3
+{% set page = 'treegraph' %}
4
+
5
+{% block title %}GitList{% endblock %}
6
+
7
+{% block content %}
8
+    {% include 'breadcrumb.twig' with {breadcrumbs: [{dir: 'Graph', path:''}]} %}
9
+    <div class="network-view">
10
+        <div class="network-header">
11
+            <div class="meta">Graph of {{ repo }} </div>
12
+        </div>
13
+
14
+        <div id="git-graph-container">
15
+            <div id="rel-container">
16
+                <canvas id="graph-canvas" width="100px">
17
+                    <ul id="graph-raw-list">
18
+                        {% for item in graphItems %}
19
+                            <li><span class="node-relation">{{ item.relation }}</span></li>
20
+                        {% endfor %}
21
+                    </ul>
22
+                </canvas>
23
+            </div>
24
+            <div style="float:left;" id="rev-container">
25
+                <ul id="rev-list">
26
+                    {% for item in graphItems %}
27
+                        <li>
28
+                            {% if item.rev is defined %}
29
+                                <a id="{{ item.short_rev }}" class="btn btn-small" href="{{ path('commit', {repo: repo, commit: item.rev}) }}"> {{ item.short_rev }} </a>
30
+                                <strong> {{ item.branch }} </strong>
31
+                                <em>{{ item.subject }}</em> by
32
+                                <span class="author">{{ item.author }} &lt;{{ item.author_email }}&gt;</span>
33
+                                <span class="time">{{ item.date }}</span>;
34
+                            {% else %}
35
+                                <span/>
36
+                            {% endif %}
37
+                        </li>
38
+                    {% endfor %}
39
+                </ul>
40
+            </div>
41
+            <div style="clear:both"><!-- --></div>
42
+        </div>
43
+    </div>
44
+
45
+
46
+
47
+    <hr/>
48
+{% endblock %}