From da9f32d5274df1c503903c3209274ef2122601c9 Mon Sep 17 00:00:00 2001 From: Guillaume CAMUS Date: Wed, 10 Dec 2014 17:09:14 +0100 Subject: [PATCH 1/2] Add tab to draw a graphical representation of the commit history --- boot.php | 1 + .../Controller/TreeGraphController.php | 157 +++++++ themes/bootstrap3/css/gitgraph.css | 14 + themes/bootstrap3/js/draw.js | 17 + themes/bootstrap3/js/gitgraph.js | 399 ++++++++++++++++++ themes/bootstrap3/twig/layout.twig | 5 + themes/bootstrap3/twig/treegraph.twig | 48 +++ themes/default/css/gitgraph.css | 14 + themes/default/js/draw.js | 17 + themes/default/js/gitgraph.js | 399 ++++++++++++++++++ themes/default/twig/layout.twig | 5 + themes/default/twig/menu.twig | 1 + themes/default/twig/treegraph.twig | 48 +++ 13 files changed, 1125 insertions(+) create mode 100644 src/GitList/Controller/TreeGraphController.php create mode 100644 themes/bootstrap3/css/gitgraph.css create mode 100644 themes/bootstrap3/js/draw.js create mode 100644 themes/bootstrap3/js/gitgraph.js create mode 100644 themes/bootstrap3/twig/treegraph.twig create mode 100644 themes/default/css/gitgraph.css create mode 100644 themes/default/js/draw.js create mode 100644 themes/default/js/gitgraph.js create mode 100644 themes/default/twig/treegraph.twig diff --git a/boot.php b/boot.php index 6ab0915..9b15b9c 100644 --- a/boot.php +++ b/boot.php @@ -9,5 +9,6 @@ $app->mount('', new GitList\Controller\BlobController()); $app->mount('', new GitList\Controller\CommitController()); $app->mount('', new GitList\Controller\TreeController()); $app->mount('', new GitList\Controller\NetworkController()); +$app->mount('', new GitList\Controller\TreeGraphController()); return $app; diff --git a/src/GitList/Controller/TreeGraphController.php b/src/GitList/Controller/TreeGraphController.php new file mode 100644 index 0000000..894b801 --- /dev/null +++ b/src/GitList/Controller/TreeGraphController.php @@ -0,0 +1,157 @@ +get('{repo}/graph/{commitishPath}/{page}.json', +// function ($repo, $commitishPath, $page) use ($app) { +// /** @var $repository Repository */ +// $repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo); +// +// if ($commitishPath === null) { +// $commitishPath = $repository->getHead(); +// } +// +// $pager = $app['util.view']->getPager($page, $repository->getTotalCommits($commitishPath)); +// $commits = $repository->getPaginatedCommits($commitishPath, $pager['current']); +// +// $jsonFormattedCommits = array(); +// +// foreach ($commits as $commit) { +// $detailsUrl = $app['url_generator']->generate( +// 'commit', +// array( +// 'repo' => $repo, +// 'commit' => $commit->getHash() +// ) +// ); +// +// $jsonFormattedCommits[$commit->getHash()] = array( +// 'hash' => $commit->getHash(), +// 'parentsHash' => $commit->getParentsHash(), +// 'date' => $commit->getDate()->format('U'), +// 'message' => htmlentities($commit->getMessage()), +// 'details' => $detailsUrl, +// 'author' => array( +// 'name' => $commit->getAuthor()->getName(), +// 'email' => $commit->getAuthor()->getEmail(), +// // due to the lack of a inbuilt javascript md5 mechanism, build the full avatar url on the php side +// 'image' => 'http://gravatar.com/avatar/' . md5( +// strtolower($commit->getAuthor()->getEmail()) +// ) . '?s=40' +// ) +// ); +// } +// +// $nextPageUrl = null; +// +// if ($pager['last'] !== $pager['current']) { +// $nextPageUrl = $app['url_generator']->generate( +// 'networkData', +// array( +// 'repo' => $repo, +// 'commitishPath' => $commitishPath, +// 'page' => $pager['next'] +// ) +// ); +// } +// +// // when no commits are given, return an empty response - issue #369 +// if( count($commits) === 0 ) { +// return $app->json( array( +// 'repo' => $repo, +// 'commitishPath' => $commitishPath, +// 'nextPage' => null, +// 'start' => null, +// 'commits' => $jsonFormattedCommits +// ), 200 +// ); +// } +// +// return $app->json( array( +// 'repo' => $repo, +// 'commitishPath' => $commitishPath, +// 'nextPage' => $nextPageUrl, +// 'start' => $commits[0]->getHash(), +// 'commits' => $jsonFormattedCommits +// ), 200 +// ); +// } +// )->assert('repo', $app['util.routing']->getRepositoryRegex()) +// ->assert('commitishPath', $app['util.routing']->getCommitishPathRegex()) +// ->value('commitishPath', null) +// ->convert('commitishPath', 'escaper.argument:escape') +// ->assert('page', '\d+') +// ->value('page', '0') +// ->bind('graphData'); + + $route->get( + '{repo}/treegraph/{commitishPath}', + function ($repo, $commitishPath) use ($app) { + /** @var \GitList\Git\Repository $repository */ + $repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo); + + $command = 'log --graph --date-order --all -C -M -n 100 --date=iso ' . + '--pretty=format:"B[%d] C[%H] D[%ad] A[%an] E[%ae] H[%h] S[%s]"'; + $rawRows = $repository->getClient()->run($repository, $command); + $rawRows = explode("\n", $rawRows); + $graphItems = array(); + + foreach ($rawRows as $row) { + if (preg_match("/^(.+?)(\s(B\[(.*?)\])? C\[(.+?)\] D\[(.+?)\] A\[(.+?)\] E\[(.+?)\] H\[(.+?)\] S\[(.+?)\])?$/", $row, $output)) { + if (!isset($output[4])) { + $graphItems[] = array( + "relation"=>$output[1] + ); + continue; + } + $graphItems[] = array( + "relation"=>$output[1], + "branch"=>$output[4], + "rev"=>$output[5], + "date"=>$output[6], + "author"=>$output[7], + "author_email"=>$output[8], + "short_rev"=>$output[9], + "subject"=>preg_replace('/(^|\s)(#[[:xdigit:]]+)(\s|$)/', '$1$2$3', $output[10]) + ); + } + } + + if ($commitishPath === null) { + $commitishPath = $repository->getHead(); + } + + list($branch, $file) = $app['util.routing']->parseCommitishPathParam($commitishPath, $repo); + list($branch, $file) = $app['util.repository']->extractRef($repository, $branch, $file); + + return $app['twig']->render( + 'treegraph.twig', + array( + 'repo' => $repo, + 'branch' => $branch, + 'commitishPath' => $commitishPath, + 'graphItems' => $graphItems, + ) + ); + } + )->assert('repo', $app['util.routing']->getRepositoryRegex()) + ->assert('commitishPath', $app['util.routing']->getCommitishPathRegex()) + ->value('commitishPath', null) + ->convert('commitishPath', 'escaper.argument:escape') + ->bind('treegraph'); + + return $route; + } +} diff --git a/themes/bootstrap3/css/gitgraph.css b/themes/bootstrap3/css/gitgraph.css new file mode 100644 index 0000000..43347fa --- /dev/null +++ b/themes/bootstrap3/css/gitgraph.css @@ -0,0 +1,14 @@ +body {font:13.34px/1.4 helvetica,arial,freesans,clean,sans-serif;} +em {font-style:normal;} + +#git-graph-container{ clear: both} +#rev-container, #rel-container {float:left;} +#git-graph-container li {list-style-type:none;height:28px;line-height:27px;overflow:hidden;} +#git-graph-container li .node-relation {font-family:'Bitstream Vera Sans Mono', 'Courier', monospace;} +#git-graph-container li .author {color:#666666;} +#git-graph-container li .time {color:#999999;font-size:80%} +#git-graph-container li a {color:#000000;} +#git-graph-container li a em {color:#BB0000;border-bottom:1px dotted #BBBBBB;text-decoration:none;font-style:normal;} + +#rev-list {margin:0;padding:0 5px 0 0;} +#graph-raw-list {margin:0px;} \ No newline at end of file diff --git a/themes/bootstrap3/js/draw.js b/themes/bootstrap3/js/draw.js new file mode 100644 index 0000000..58ee7f9 --- /dev/null +++ b/themes/bootstrap3/js/draw.js @@ -0,0 +1,17 @@ +$(document).ready(function () { + var graphList = []; + + if (!document.getElementById('graph-canvas')) { + return; + } + + $("#graph-raw-list li span.node-relation").each(function () { + graphList.push($(this).text()); + }) + + gitGraph(document.getElementById('graph-canvas'), graphList); + + if ($("#rev-container")) { + $("#rev-container").css("width", $('#git-graph-container').width() - $('#graph-canvas').width()); + } +}) diff --git a/themes/bootstrap3/js/gitgraph.js b/themes/bootstrap3/js/gitgraph.js new file mode 100644 index 0000000..84cfbfc --- /dev/null +++ b/themes/bootstrap3/js/gitgraph.js @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2011, Terrence Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +var gitGraph = function (canvas, rawGraphList, config) { + if (!canvas.getContext) { + return; + } + + if (typeof config === "undefined") { + config = { + unitSize: 28, + lineWidth: 3, + nodeRadius: 6 + }; + } + + var flows = []; + var graphList = []; + + var ctx = canvas.getContext("2d"); + + var init = function () { + var maxWidth = 0; + var i; + var l = rawGraphList.length; + var row; + var midStr; + + for (i = 0; i < l; i++) { + midStr = rawGraphList[i].replace(/\s+/g, " ").replace(/^\s+|\s+$/g, ""); + + maxWidth = Math.max(midStr.replace(/(\_|\s)/g, "").length, maxWidth); + + row = midStr.split(""); + + graphList.unshift(row); + } + + canvas.width = maxWidth * config.unitSize; + canvas.height = graphList.length * config.unitSize; + + ctx.lineWidth = config.lineWidth; + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + }; + + var genRandomStr = function () { + var chars = "0123456789ABCDEF"; + var stringLength = 6; + var randomString = '', rnum, i; + for (i = 0; i < stringLength; i++) { + rnum = Math.floor(Math.random() * chars.length); + randomString += chars.substring(rnum, rnum + 1); + } + + return randomString; + }; + + var findFlow = function (id) { + var i = flows.length; + + while (i-- && flows[i].id !== id) {} + + return i; + }; + + var findColomn = function (symbol, row) { + var i = row.length; + + while (i-- && row[i] !== symbol) {} + + return i; + }; + + var findBranchOut = function (row) { + if (!row) { + return -1 + } + + var i = row.length; + + while (i-- && + !(row[i - 1] && row[i] === "/" && row[i - 1] === "|") && + !(row[i - 2] && row[i] === "_" && row[i - 2] === "|")) {} + + return i; + } + + var genNewFlow = function () { + var newId; + + do { + newId = genRandomStr(); + } while (findFlow(newId) !== -1); + + return {id:newId, color:"#" + newId}; + }; + + //draw method + var drawLineRight = function (x, y, color) { + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(x, y + config.unitSize / 2); + ctx.lineTo(x + config.unitSize, y + config.unitSize / 2); + ctx.stroke(); + }; + + var drawLineUp = function (x, y, color) { + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(x, y + config.unitSize / 2); + ctx.lineTo(x, y - config.unitSize / 2); + ctx.stroke(); + }; + + var drawNode = function (x, y, color) { + ctx.strokeStyle = color; + + drawLineUp(x, y, color); + + ctx.beginPath(); + ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true); + ctx.fill(); + }; + + var drawLineIn = function (x, y, color) { + ctx.strokeStyle = color; + + ctx.beginPath(); + ctx.moveTo(x + config.unitSize, y + config.unitSize / 2); + ctx.lineTo(x, y - config.unitSize / 2); + ctx.stroke(); + }; + + var drawLineOut = function (x, y, color) { + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(x, y + config.unitSize / 2); + ctx.lineTo(x + config.unitSize, y - config.unitSize / 2); + ctx.stroke(); + }; + + var draw = function (graphList) { + var colomn, colomnIndex, prevColomn, condenseIndex; + var x, y; + var color; + var nodePos, outPos; + var tempFlow; + var prevRowLength = 0; + var flowSwapPos = -1; + var lastLinePos; + var i, k, l; + var condenseCurrentLength, condensePrevLength = 0, condenseNextLength = 0; + + var inlineIntersect = false; + + //initiate for first row + for (i = 0, l = graphList[0].length; i < l; i++) { + if (graphList[0][i] !== "_" && graphList[0][i] !== " ") { + flows.push(genNewFlow()); + } + } + + y = canvas.height - config.unitSize * 0.5; + + //iterate + for (i = 0, l = graphList.length; i < l; i++) { + x = config.unitSize * 0.5; + + currentRow = graphList[i]; + nextRow = graphList[i + 1]; + prevRow = graphList[i - 1]; + + flowSwapPos = -1; + + condenseCurrentLength = currentRow.filter(function (val) { + return (val !== " " && val !== "_") + }).length; + + if (nextRow) { + condenseNextLength = nextRow.filter(function (val) { + return (val !== " " && val !== "_") + }).length; + } else { + condenseNextLength = 0; + } + + //pre process begin + //use last row for analysing + if (prevRow) { + if (!inlineIntersect) { + //intersect might happen + for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) { + if (prevRow[colomnIndex + 1] && + (prevRow[colomnIndex] === "/" && prevRow[colomnIndex + 1] === "|") || + ((prevRow[colomnIndex] === "_" && prevRow[colomnIndex + 1] === "|") && + (prevRow[colomnIndex + 2] === "/"))) { + + flowSwapPos = colomnIndex; + + //swap two flow + tempFlow = {id:flows[flowSwapPos].id, color:flows[flowSwapPos].color}; + + flows[flowSwapPos].id = flows[flowSwapPos + 1].id; + flows[flowSwapPos].color = flows[flowSwapPos + 1].color; + + flows[flowSwapPos + 1].id = tempFlow.id; + flows[flowSwapPos + 1].color = tempFlow.color; + } + } + } + + if (condensePrevLength < condenseCurrentLength && + ((nodePos = findColomn("*", currentRow)) !== -1 && + (findColomn("_", currentRow) === -1))) { + + flows.splice(nodePos - 1, 0, genNewFlow()); + } + + if (prevRowLength > currentRow.length && + (nodePos = findColomn("*", prevRow)) !== -1) { + + if (findColomn("_", currentRow) === -1 && + findColomn("/", currentRow) === -1 && + findColomn("\\", currentRow) === -1) { + + flows.splice(nodePos + 1, 1); + } + } + } //done with the previous row + + prevRowLength = currentRow.length; //store for next round + colomnIndex = 0; //reset index + condenseIndex = 0; + condensePrevLength = 0; + while (colomnIndex < currentRow.length) { + colomn = currentRow[colomnIndex]; + + if (colomn !== " " && colomn !== "_") { + ++condensePrevLength; + } + + if (colomn === " " && + currentRow[colomnIndex + 1] && + currentRow[colomnIndex + 1] === "_" && + currentRow[colomnIndex - 1] && + currentRow[colomnIndex - 1] === "|") { + + currentRow.splice(colomnIndex, 1); + + currentRow[colomnIndex] = "/"; + colomn = "/"; + } + + //create new flow only when no intersetc happened + if (flowSwapPos === -1 && + colomn === "/" && + currentRow[colomnIndex - 1] && + currentRow[colomnIndex - 1] === "|") { + + flows.splice(condenseIndex, 0, genNewFlow()); + } + + //change \ and / to | when it's in the last position of the whole row + if (colomn === "/" || colomn === "\\") { + if (!(colomn === "/" && findBranchOut(nextRow) === -1)) { + if ((lastLinePos = Math.max(findColomn("|", currentRow), + findColomn("*", currentRow))) !== -1 && + (lastLinePos < colomnIndex - 1)) { + + while (currentRow[++lastLinePos] === " ") {} + + if (lastLinePos === colomnIndex) { + currentRow[colomnIndex] = "|"; + } + } + } + } + + if (colomn === "*" && + prevRow && + prevRow[condenseIndex + 1] === "\\") { + flows.splice(condenseIndex + 1, 1); + } + + if (colomn !== " ") { + ++condenseIndex; + } + + ++colomnIndex; + } + + condenseCurrentLength = currentRow.filter(function (val) { + return (val !== " " && val !== "_") + }).length; + + //do some clean up + if (flows.length > condenseCurrentLength) { + flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength); + } + + colomnIndex = 0; + + //a little inline analysis and draw process + while (colomnIndex < currentRow.length) { + colomn = currentRow[colomnIndex]; + prevColomn = currentRow[colomnIndex - 1]; + + if (currentRow[colomnIndex] === " ") { + currentRow.splice(colomnIndex, 1); + x += config.unitSize; + + continue; + } + + //inline interset + if ((colomn === "_" || colomn === "/") && + currentRow[colomnIndex - 1] === "|" && + currentRow[colomnIndex - 2] === "_") { + + inlineIntersect = true; + + tempFlow = flows.splice(colomnIndex - 2, 1)[0]; + flows.splice(colomnIndex - 1, 0, tempFlow); + currentRow.splice(colomnIndex - 2, 1); + + colomnIndex = colomnIndex - 1; + } else { + inlineIntersect = false; + } + + color = flows[colomnIndex].color; + + switch (colomn) { + case "_" : + drawLineRight(x, y, color); + + x += config.unitSize; + break; + + case "*" : + drawNode(x, y, color); + break; + + case "|" : + drawLineUp(x, y, color); + break; + + case "/" : + if (prevColomn && + (prevColomn === "/" || + prevColomn === " ")) { + x -= config.unitSize; + } + + drawLineOut(x, y, color); + + x += config.unitSize; + break; + + case "\\" : + drawLineIn(x, y, color); + break; + } + + ++colomnIndex; + } + + y -= config.unitSize; + } + }; + + init(); + draw(graphList); +}; \ No newline at end of file diff --git a/themes/bootstrap3/twig/layout.twig b/themes/bootstrap3/twig/layout.twig index 62f5c4d..cafc09e 100644 --- a/themes/bootstrap3/twig/layout.twig +++ b/themes/bootstrap3/twig/layout.twig @@ -5,6 +5,7 @@ {% block title %}Welcome!{% endblock %} + + + + + + +
+{% endblock %} diff --git a/themes/default/css/gitgraph.css b/themes/default/css/gitgraph.css new file mode 100644 index 0000000..43347fa --- /dev/null +++ b/themes/default/css/gitgraph.css @@ -0,0 +1,14 @@ +body {font:13.34px/1.4 helvetica,arial,freesans,clean,sans-serif;} +em {font-style:normal;} + +#git-graph-container{ clear: both} +#rev-container, #rel-container {float:left;} +#git-graph-container li {list-style-type:none;height:28px;line-height:27px;overflow:hidden;} +#git-graph-container li .node-relation {font-family:'Bitstream Vera Sans Mono', 'Courier', monospace;} +#git-graph-container li .author {color:#666666;} +#git-graph-container li .time {color:#999999;font-size:80%} +#git-graph-container li a {color:#000000;} +#git-graph-container li a em {color:#BB0000;border-bottom:1px dotted #BBBBBB;text-decoration:none;font-style:normal;} + +#rev-list {margin:0;padding:0 5px 0 0;} +#graph-raw-list {margin:0px;} \ No newline at end of file diff --git a/themes/default/js/draw.js b/themes/default/js/draw.js new file mode 100644 index 0000000..58ee7f9 --- /dev/null +++ b/themes/default/js/draw.js @@ -0,0 +1,17 @@ +$(document).ready(function () { + var graphList = []; + + if (!document.getElementById('graph-canvas')) { + return; + } + + $("#graph-raw-list li span.node-relation").each(function () { + graphList.push($(this).text()); + }) + + gitGraph(document.getElementById('graph-canvas'), graphList); + + if ($("#rev-container")) { + $("#rev-container").css("width", $('#git-graph-container').width() - $('#graph-canvas').width()); + } +}) diff --git a/themes/default/js/gitgraph.js b/themes/default/js/gitgraph.js new file mode 100644 index 0000000..84cfbfc --- /dev/null +++ b/themes/default/js/gitgraph.js @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2011, Terrence Lee + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +var gitGraph = function (canvas, rawGraphList, config) { + if (!canvas.getContext) { + return; + } + + if (typeof config === "undefined") { + config = { + unitSize: 28, + lineWidth: 3, + nodeRadius: 6 + }; + } + + var flows = []; + var graphList = []; + + var ctx = canvas.getContext("2d"); + + var init = function () { + var maxWidth = 0; + var i; + var l = rawGraphList.length; + var row; + var midStr; + + for (i = 0; i < l; i++) { + midStr = rawGraphList[i].replace(/\s+/g, " ").replace(/^\s+|\s+$/g, ""); + + maxWidth = Math.max(midStr.replace(/(\_|\s)/g, "").length, maxWidth); + + row = midStr.split(""); + + graphList.unshift(row); + } + + canvas.width = maxWidth * config.unitSize; + canvas.height = graphList.length * config.unitSize; + + ctx.lineWidth = config.lineWidth; + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + }; + + var genRandomStr = function () { + var chars = "0123456789ABCDEF"; + var stringLength = 6; + var randomString = '', rnum, i; + for (i = 0; i < stringLength; i++) { + rnum = Math.floor(Math.random() * chars.length); + randomString += chars.substring(rnum, rnum + 1); + } + + return randomString; + }; + + var findFlow = function (id) { + var i = flows.length; + + while (i-- && flows[i].id !== id) {} + + return i; + }; + + var findColomn = function (symbol, row) { + var i = row.length; + + while (i-- && row[i] !== symbol) {} + + return i; + }; + + var findBranchOut = function (row) { + if (!row) { + return -1 + } + + var i = row.length; + + while (i-- && + !(row[i - 1] && row[i] === "/" && row[i - 1] === "|") && + !(row[i - 2] && row[i] === "_" && row[i - 2] === "|")) {} + + return i; + } + + var genNewFlow = function () { + var newId; + + do { + newId = genRandomStr(); + } while (findFlow(newId) !== -1); + + return {id:newId, color:"#" + newId}; + }; + + //draw method + var drawLineRight = function (x, y, color) { + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(x, y + config.unitSize / 2); + ctx.lineTo(x + config.unitSize, y + config.unitSize / 2); + ctx.stroke(); + }; + + var drawLineUp = function (x, y, color) { + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(x, y + config.unitSize / 2); + ctx.lineTo(x, y - config.unitSize / 2); + ctx.stroke(); + }; + + var drawNode = function (x, y, color) { + ctx.strokeStyle = color; + + drawLineUp(x, y, color); + + ctx.beginPath(); + ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true); + ctx.fill(); + }; + + var drawLineIn = function (x, y, color) { + ctx.strokeStyle = color; + + ctx.beginPath(); + ctx.moveTo(x + config.unitSize, y + config.unitSize / 2); + ctx.lineTo(x, y - config.unitSize / 2); + ctx.stroke(); + }; + + var drawLineOut = function (x, y, color) { + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(x, y + config.unitSize / 2); + ctx.lineTo(x + config.unitSize, y - config.unitSize / 2); + ctx.stroke(); + }; + + var draw = function (graphList) { + var colomn, colomnIndex, prevColomn, condenseIndex; + var x, y; + var color; + var nodePos, outPos; + var tempFlow; + var prevRowLength = 0; + var flowSwapPos = -1; + var lastLinePos; + var i, k, l; + var condenseCurrentLength, condensePrevLength = 0, condenseNextLength = 0; + + var inlineIntersect = false; + + //initiate for first row + for (i = 0, l = graphList[0].length; i < l; i++) { + if (graphList[0][i] !== "_" && graphList[0][i] !== " ") { + flows.push(genNewFlow()); + } + } + + y = canvas.height - config.unitSize * 0.5; + + //iterate + for (i = 0, l = graphList.length; i < l; i++) { + x = config.unitSize * 0.5; + + currentRow = graphList[i]; + nextRow = graphList[i + 1]; + prevRow = graphList[i - 1]; + + flowSwapPos = -1; + + condenseCurrentLength = currentRow.filter(function (val) { + return (val !== " " && val !== "_") + }).length; + + if (nextRow) { + condenseNextLength = nextRow.filter(function (val) { + return (val !== " " && val !== "_") + }).length; + } else { + condenseNextLength = 0; + } + + //pre process begin + //use last row for analysing + if (prevRow) { + if (!inlineIntersect) { + //intersect might happen + for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) { + if (prevRow[colomnIndex + 1] && + (prevRow[colomnIndex] === "/" && prevRow[colomnIndex + 1] === "|") || + ((prevRow[colomnIndex] === "_" && prevRow[colomnIndex + 1] === "|") && + (prevRow[colomnIndex + 2] === "/"))) { + + flowSwapPos = colomnIndex; + + //swap two flow + tempFlow = {id:flows[flowSwapPos].id, color:flows[flowSwapPos].color}; + + flows[flowSwapPos].id = flows[flowSwapPos + 1].id; + flows[flowSwapPos].color = flows[flowSwapPos + 1].color; + + flows[flowSwapPos + 1].id = tempFlow.id; + flows[flowSwapPos + 1].color = tempFlow.color; + } + } + } + + if (condensePrevLength < condenseCurrentLength && + ((nodePos = findColomn("*", currentRow)) !== -1 && + (findColomn("_", currentRow) === -1))) { + + flows.splice(nodePos - 1, 0, genNewFlow()); + } + + if (prevRowLength > currentRow.length && + (nodePos = findColomn("*", prevRow)) !== -1) { + + if (findColomn("_", currentRow) === -1 && + findColomn("/", currentRow) === -1 && + findColomn("\\", currentRow) === -1) { + + flows.splice(nodePos + 1, 1); + } + } + } //done with the previous row + + prevRowLength = currentRow.length; //store for next round + colomnIndex = 0; //reset index + condenseIndex = 0; + condensePrevLength = 0; + while (colomnIndex < currentRow.length) { + colomn = currentRow[colomnIndex]; + + if (colomn !== " " && colomn !== "_") { + ++condensePrevLength; + } + + if (colomn === " " && + currentRow[colomnIndex + 1] && + currentRow[colomnIndex + 1] === "_" && + currentRow[colomnIndex - 1] && + currentRow[colomnIndex - 1] === "|") { + + currentRow.splice(colomnIndex, 1); + + currentRow[colomnIndex] = "/"; + colomn = "/"; + } + + //create new flow only when no intersetc happened + if (flowSwapPos === -1 && + colomn === "/" && + currentRow[colomnIndex - 1] && + currentRow[colomnIndex - 1] === "|") { + + flows.splice(condenseIndex, 0, genNewFlow()); + } + + //change \ and / to | when it's in the last position of the whole row + if (colomn === "/" || colomn === "\\") { + if (!(colomn === "/" && findBranchOut(nextRow) === -1)) { + if ((lastLinePos = Math.max(findColomn("|", currentRow), + findColomn("*", currentRow))) !== -1 && + (lastLinePos < colomnIndex - 1)) { + + while (currentRow[++lastLinePos] === " ") {} + + if (lastLinePos === colomnIndex) { + currentRow[colomnIndex] = "|"; + } + } + } + } + + if (colomn === "*" && + prevRow && + prevRow[condenseIndex + 1] === "\\") { + flows.splice(condenseIndex + 1, 1); + } + + if (colomn !== " ") { + ++condenseIndex; + } + + ++colomnIndex; + } + + condenseCurrentLength = currentRow.filter(function (val) { + return (val !== " " && val !== "_") + }).length; + + //do some clean up + if (flows.length > condenseCurrentLength) { + flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength); + } + + colomnIndex = 0; + + //a little inline analysis and draw process + while (colomnIndex < currentRow.length) { + colomn = currentRow[colomnIndex]; + prevColomn = currentRow[colomnIndex - 1]; + + if (currentRow[colomnIndex] === " ") { + currentRow.splice(colomnIndex, 1); + x += config.unitSize; + + continue; + } + + //inline interset + if ((colomn === "_" || colomn === "/") && + currentRow[colomnIndex - 1] === "|" && + currentRow[colomnIndex - 2] === "_") { + + inlineIntersect = true; + + tempFlow = flows.splice(colomnIndex - 2, 1)[0]; + flows.splice(colomnIndex - 1, 0, tempFlow); + currentRow.splice(colomnIndex - 2, 1); + + colomnIndex = colomnIndex - 1; + } else { + inlineIntersect = false; + } + + color = flows[colomnIndex].color; + + switch (colomn) { + case "_" : + drawLineRight(x, y, color); + + x += config.unitSize; + break; + + case "*" : + drawNode(x, y, color); + break; + + case "|" : + drawLineUp(x, y, color); + break; + + case "/" : + if (prevColomn && + (prevColomn === "/" || + prevColomn === " ")) { + x -= config.unitSize; + } + + drawLineOut(x, y, color); + + x += config.unitSize; + break; + + case "\\" : + drawLineIn(x, y, color); + break; + } + + ++colomnIndex; + } + + y -= config.unitSize; + } + }; + + init(); + draw(graphList); +}; \ No newline at end of file diff --git a/themes/default/twig/layout.twig b/themes/default/twig/layout.twig index a2296c8..4f9b44e 100644 --- a/themes/default/twig/layout.twig +++ b/themes/default/twig/layout.twig @@ -4,6 +4,7 @@ {{ app.title }}{% if app.title %} - {% endif %}{% block title %}Welcome!{% endblock %} + + + + + + +
+{% endblock %} From f04e1d4e48d54ec7741ef6c37ce07844940abc85 Mon Sep 17 00:00:00 2001 From: Guillaume CAMUS Date: Wed, 10 Dec 2014 17:16:01 +0100 Subject: [PATCH 2/2] Clean code --- .../Controller/TreeGraphController.php | 84 ------------------- 1 file changed, 84 deletions(-) diff --git a/src/GitList/Controller/TreeGraphController.php b/src/GitList/Controller/TreeGraphController.php index 894b801..ecf0c06 100644 --- a/src/GitList/Controller/TreeGraphController.php +++ b/src/GitList/Controller/TreeGraphController.php @@ -2,8 +2,6 @@ namespace GitList\Controller; -use GitList\Git\Repository; -use Gitter\Model\Commit\Commit; use Silex\Application; use Silex\ControllerProviderInterface; use Symfony\Component\HttpFoundation\Request; @@ -14,88 +12,6 @@ class TreeGraphController implements ControllerProviderInterface { $route = $app['controllers_factory']; -// $route->get('{repo}/graph/{commitishPath}/{page}.json', -// function ($repo, $commitishPath, $page) use ($app) { -// /** @var $repository Repository */ -// $repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo); -// -// if ($commitishPath === null) { -// $commitishPath = $repository->getHead(); -// } -// -// $pager = $app['util.view']->getPager($page, $repository->getTotalCommits($commitishPath)); -// $commits = $repository->getPaginatedCommits($commitishPath, $pager['current']); -// -// $jsonFormattedCommits = array(); -// -// foreach ($commits as $commit) { -// $detailsUrl = $app['url_generator']->generate( -// 'commit', -// array( -// 'repo' => $repo, -// 'commit' => $commit->getHash() -// ) -// ); -// -// $jsonFormattedCommits[$commit->getHash()] = array( -// 'hash' => $commit->getHash(), -// 'parentsHash' => $commit->getParentsHash(), -// 'date' => $commit->getDate()->format('U'), -// 'message' => htmlentities($commit->getMessage()), -// 'details' => $detailsUrl, -// 'author' => array( -// 'name' => $commit->getAuthor()->getName(), -// 'email' => $commit->getAuthor()->getEmail(), -// // due to the lack of a inbuilt javascript md5 mechanism, build the full avatar url on the php side -// 'image' => 'http://gravatar.com/avatar/' . md5( -// strtolower($commit->getAuthor()->getEmail()) -// ) . '?s=40' -// ) -// ); -// } -// -// $nextPageUrl = null; -// -// if ($pager['last'] !== $pager['current']) { -// $nextPageUrl = $app['url_generator']->generate( -// 'networkData', -// array( -// 'repo' => $repo, -// 'commitishPath' => $commitishPath, -// 'page' => $pager['next'] -// ) -// ); -// } -// -// // when no commits are given, return an empty response - issue #369 -// if( count($commits) === 0 ) { -// return $app->json( array( -// 'repo' => $repo, -// 'commitishPath' => $commitishPath, -// 'nextPage' => null, -// 'start' => null, -// 'commits' => $jsonFormattedCommits -// ), 200 -// ); -// } -// -// return $app->json( array( -// 'repo' => $repo, -// 'commitishPath' => $commitishPath, -// 'nextPage' => $nextPageUrl, -// 'start' => $commits[0]->getHash(), -// 'commits' => $jsonFormattedCommits -// ), 200 -// ); -// } -// )->assert('repo', $app['util.routing']->getRepositoryRegex()) -// ->assert('commitishPath', $app['util.routing']->getCommitishPathRegex()) -// ->value('commitishPath', null) -// ->convert('commitishPath', 'escaper.argument:escape') -// ->assert('page', '\d+') -// ->value('page', '0') -// ->bind('graphData'); - $route->get( '{repo}/treegraph/{commitishPath}', function ($repo, $commitishPath) use ($app) {