mirror of
https://github.com/klaussilveira/gitlist.git
synced 2025-11-17 19:20:56 +01:00
1
boot.php
1
boot.php
@@ -8,5 +8,6 @@ $app->mount('', new GitList\Controller\MainController());
|
|||||||
$app->mount('', new GitList\Controller\BlobController());
|
$app->mount('', new GitList\Controller\BlobController());
|
||||||
$app->mount('', new GitList\Controller\CommitController());
|
$app->mount('', new GitList\Controller\CommitController());
|
||||||
$app->mount('', new GitList\Controller\TreeController());
|
$app->mount('', new GitList\Controller\TreeController());
|
||||||
|
$app->mount('', new GitList\Controller\NetworkController());
|
||||||
|
|
||||||
return $app;
|
return $app;
|
||||||
|
|||||||
113
src/GitList/Controller/NetworkController.php
Normal file
113
src/GitList/Controller/NetworkController.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace GitList\Controller;
|
||||||
|
|
||||||
|
use GitList\Git\Repository;
|
||||||
|
use Gitter\Model\Commit\Commit;
|
||||||
|
use Silex\Application;
|
||||||
|
use Silex\ControllerProviderInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
class NetworkController implements ControllerProviderInterface
|
||||||
|
{
|
||||||
|
public function connect(Application $app)
|
||||||
|
{
|
||||||
|
$route = $app['controllers_factory'];
|
||||||
|
|
||||||
|
$route->get('{repo}/network/{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']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
->assert('page', '\d+')
|
||||||
|
->value('page', '0')
|
||||||
|
->bind('networkData');
|
||||||
|
|
||||||
|
$route->get(
|
||||||
|
'{repo}/network/{commitishPath}',
|
||||||
|
function ($repo, $commitishPath) use ($app) {
|
||||||
|
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||||
|
|
||||||
|
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(
|
||||||
|
'network.twig',
|
||||||
|
array(
|
||||||
|
'repo' => $repo,
|
||||||
|
'branch' => $branch,
|
||||||
|
'commitishPath' => $commitishPath,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||||
|
->assert('commitishPath', $app['util.routing']->getCommitishPathRegex())
|
||||||
|
->value('commitishPath', null)
|
||||||
|
->bind('network');
|
||||||
|
|
||||||
|
return $route;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -212,7 +212,7 @@ class Repository extends BaseRepository
|
|||||||
$pager = "--skip=$page --max-count=15";
|
$pager = "--skip=$page --max-count=15";
|
||||||
$command =
|
$command =
|
||||||
"log $pager --pretty=format:\"<item><hash>%H</hash>"
|
"log $pager --pretty=format:\"<item><hash>%H</hash>"
|
||||||
. "<short_hash>%h</short_hash><tree>%T</tree><parent>%P</parent>"
|
. "<short_hash>%h</short_hash><tree>%T</tree><parents>%P</parents>"
|
||||||
. "<author>%an</author><author_email>%ae</author_email>"
|
. "<author>%an</author><author_email>%ae</author_email>"
|
||||||
. "<date>%at</date><commiter>%cn</commiter>"
|
. "<date>%at</date><commiter>%cn</commiter>"
|
||||||
. "<commiter_email>%ce</commiter_email>"
|
. "<commiter_email>%ce</commiter_email>"
|
||||||
@@ -243,7 +243,7 @@ class Repository extends BaseRepository
|
|||||||
$query = escapeshellarg($query);
|
$query = escapeshellarg($query);
|
||||||
$command =
|
$command =
|
||||||
"log --grep={$query} --pretty=format:\"<item><hash>%H</hash>"
|
"log --grep={$query} --pretty=format:\"<item><hash>%H</hash>"
|
||||||
. "<short_hash>%h</short_hash><tree>%T</tree><parent>%P</parent>"
|
. "<short_hash>%h</short_hash><tree>%T</tree><parents>%P</parents>"
|
||||||
. "<author>%an</author><author_email>%ae</author_email>"
|
. "<author>%an</author><author_email>%ae</author_email>"
|
||||||
. "<date>%at</date><commiter>%cn</commiter>"
|
. "<date>%at</date><commiter>%cn</commiter>"
|
||||||
. "<commiter_email>%ce</commiter_email>"
|
. "<commiter_email>%ce</commiter_email>"
|
||||||
|
|||||||
@@ -13,9 +13,11 @@
|
|||||||
<body>
|
<body>
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
<script src="{{ app.request.basepath }}/web/js/jquery.js"></script>
|
<script src="{{ app.request.basepath }}/web/js/jquery.js"></script>
|
||||||
|
<script src="{{ app.request.basepath }}/web/js/raphael.js"></script>
|
||||||
<script src="{{ app.request.basepath }}/web/js/bootstrap.js"></script>
|
<script src="{{ app.request.basepath }}/web/js/bootstrap.js"></script>
|
||||||
<script src="{{ app.request.basepath }}/web/js/codemirror.js"></script>
|
<script src="{{ app.request.basepath }}/web/js/codemirror.js"></script>
|
||||||
<script src="{{ app.request.basepath }}/web/js/showdown.js"></script>
|
<script src="{{ app.request.basepath }}/web/js/showdown.js"></script>
|
||||||
<script src="{{ app.request.basepath }}/web/js/main.js"></script>
|
<script src="{{ app.request.basepath }}/web/js/main.js"></script>
|
||||||
|
<script src="{{ app.request.basepath }}/web/js/networkGraph.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<li{% if page == 'files' %} class="active"{% endif %}><a href="{{ path('branch', {repo: repo, branch: branch}) }}">Files</a></li>
|
<li{% if page == 'files' %} class="active"{% endif %}><a href="{{ path('branch', {repo: repo, branch: branch}) }}">Files</a></li>
|
||||||
<li{% if page in ['commits', 'searchcommits'] %} class="active"{% endif %}><a href="{{ path('commits', {repo: repo, commitishPath: branch}) }}">Commits</a></li>
|
<li{% if page in ['commits', 'searchcommits'] %} class="active"{% endif %}><a href="{{ path('commits', {repo: repo, commitishPath: branch}) }}">Commits</a></li>
|
||||||
<li{% if page == 'stats' %} class="active"{% endif %}><a href="{{ path('stats', {repo: repo, branch: branch}) }}">Stats</a></li>
|
<li{% if page == 'stats' %} class="active"{% endif %}><a href="{{ path('stats', {repo: repo, branch: branch}) }}">Stats</a></li>
|
||||||
|
<li{% if page == 'network' %} class="active"{% endif %}><a href="{{ path('network', {repo: repo, branch: branch}) }}">Network</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
23
views/network.twig
Normal file
23
views/network.twig
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'layout_page.twig' %}
|
||||||
|
|
||||||
|
{% set page = 'network' %}
|
||||||
|
|
||||||
|
{% block title %}GitList{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'breadcrumb.twig' with {breadcrumbs: [{dir: 'Network', path:''}]} %}
|
||||||
|
<div class="network-view">
|
||||||
|
<div class="network-header">
|
||||||
|
<div class="meta">Network Graph of {{ repo }} / {{ commitishPath }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="network-graph" data-source="{{ path('networkData', {repo: repo, commitishPath: commitishPath}) }}">
|
||||||
|
{#<div class="network-graph" data-source="/dummynetwork.json">#}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
{% endblock %}
|
||||||
File diff suppressed because one or more lines are too long
581
web/js/networkGraph.js
Normal file
581
web/js/networkGraph.js
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
/**
|
||||||
|
* Network Graph JS
|
||||||
|
* This File is a part of the GitList Project at http://gitlist.org
|
||||||
|
*
|
||||||
|
* @license http://www.opensource.org/licenses/bsd-license.php
|
||||||
|
* @author Lukas Domnick <lukx@lukx.de> http://github.com/lukx
|
||||||
|
*/
|
||||||
|
|
||||||
|
( function( $ ){
|
||||||
|
// global config
|
||||||
|
var cfg = {
|
||||||
|
laneColors: ['#ff0000', '#0000FF', '#00FFFF', '#00FF00', '#FFFF00', '#ff00ff'],
|
||||||
|
laneHeight: 20,
|
||||||
|
columnWidth: 42,
|
||||||
|
dotRadius: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define the jQuery Plugins
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DragScrollr is a custom made x/y-Drag Scroll Plugin for Gitlist
|
||||||
|
*
|
||||||
|
* TODO: Make this touch-scrollable
|
||||||
|
*/
|
||||||
|
$.fn.dragScrollr = function() {
|
||||||
|
var lastX,
|
||||||
|
lastY,
|
||||||
|
hotZone = 200,
|
||||||
|
container = this.first(),
|
||||||
|
domElement = container[0]; // so basically container without the jQuery stuff
|
||||||
|
|
||||||
|
function handleMouseDown( evt ) {
|
||||||
|
container.on('mousemove', handleMouseMove);
|
||||||
|
container.on('mouseup', handleMouseUp);
|
||||||
|
container.on('mouseleave', handleMouseUp);
|
||||||
|
lastX = evt.pageX;
|
||||||
|
lastY = evt.pageY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
// save the last scroll position to figure out whether the scroll event has entered the hot zone
|
||||||
|
var lastScrollLeft = domElement.scrollLeft;
|
||||||
|
domElement.scrollLeft = domElement.scrollLeft + lastX - evt.pageX;
|
||||||
|
domElement.scrollTop = domElement.scrollTop + lastY - evt.pageY;
|
||||||
|
|
||||||
|
if( lastScrollLeft > hotZone && domElement.scrollLeft <= hotZone ) {
|
||||||
|
container.trigger('enterHotZone');
|
||||||
|
}
|
||||||
|
|
||||||
|
// when we move into the hot zone
|
||||||
|
|
||||||
|
lastX = evt.pageX;
|
||||||
|
lastY = evt.pageY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(evt) {
|
||||||
|
container.off('mousemove', handleMouseMove)
|
||||||
|
.off('mouseup', handleMouseUp)
|
||||||
|
.off('mouseleave', handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now bind the initial event
|
||||||
|
container.on('mousedown', handleMouseDown);
|
||||||
|
|
||||||
|
// return this instead of container, because of the .first() we applied - remember?
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
function graphLaneManager() {
|
||||||
|
var that = {},
|
||||||
|
occupiedLanes = [];
|
||||||
|
|
||||||
|
// "private" methods
|
||||||
|
function findLaneNumberFor( commit ) {
|
||||||
|
|
||||||
|
if( commit.lane ) {
|
||||||
|
// oh? we've already got a lane?
|
||||||
|
return commit.lane.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find out which lane may draw our dot on. Start with a free one
|
||||||
|
var laneNumber = findFreeLane();
|
||||||
|
|
||||||
|
// if the child is a merge, we need to figure out which lane we may render this commit on.
|
||||||
|
// Rules are simple: A "parent" by the same author as the merge may render on the same line as the child
|
||||||
|
// others take the next free lane.
|
||||||
|
// furthermore, commits in a linear line of events may stay on the same lane, too
|
||||||
|
if( commit.children.length > 0) {
|
||||||
|
if( !commit.children[0].isMerge // linear ...
|
||||||
|
|| ( commit.children[0].isMerge && commit.children[0].author.email === commit.author.email ) // same author
|
||||||
|
) {
|
||||||
|
laneNumber = commit.children[0].lane.number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return laneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFreeLane() {
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
while( true ) {
|
||||||
|
// if an array index is not yet defined or set to false, the lane with that number is free.
|
||||||
|
if( !occupiedLanes[i] ) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
i ++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
that.occupy = function( lane ) {
|
||||||
|
// make sure we work with lane numbers here
|
||||||
|
if( typeof lane === 'object' ) {
|
||||||
|
lane = lane.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
occupiedLanes[lane] = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
that.free = function( lane ) {
|
||||||
|
// make sure we work with lane numbers here
|
||||||
|
if( typeof lane === 'object' ) {
|
||||||
|
lane = lane.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
occupiedLanes[lane] = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
that.getLaneForCommit = function( commit ) {
|
||||||
|
// does this commit have a lane already?
|
||||||
|
if( commit.lane ) return commit.lane;
|
||||||
|
|
||||||
|
var laneNumber = findLaneNumberFor( commit );
|
||||||
|
return that.getLane( laneNumber );
|
||||||
|
};
|
||||||
|
|
||||||
|
that.getLane = function(laneNumber) {
|
||||||
|
return {
|
||||||
|
'number': laneNumber,
|
||||||
|
'centerY': ( laneNumber * cfg.laneHeight ) + (cfg.laneHeight/2),
|
||||||
|
'color': cfg.laneColors[ laneNumber % cfg.laneColors.length ]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitDetailOverlay( ) {
|
||||||
|
var that = {},
|
||||||
|
el = $('<div class="network-commit-overlay"></div>'),
|
||||||
|
imageDisplay = $('<img/>').appendTo(el),
|
||||||
|
messageDisplay = $('<h4></h4>').appendTo(el),
|
||||||
|
metaDisplay = $('<p></p>').appendTo(el),
|
||||||
|
authorDisplay = $('<a rel="author"></a>').appendTo(metaDisplay),
|
||||||
|
dateDisplay = $('<span></span>').appendTo(metaDisplay),
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
|
el.hide();
|
||||||
|
/**
|
||||||
|
* Pads an input number with one leading '0' if needed, and assure it's a string
|
||||||
|
*
|
||||||
|
* @param input Number
|
||||||
|
* @returns String
|
||||||
|
*/
|
||||||
|
function twoDigits( input ) {
|
||||||
|
if( input < 10 ) {
|
||||||
|
return '0' + input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '' + input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a JS Native Date Object to a string, maintaining the same format given in the commit_list view
|
||||||
|
* 'd/m/Y \\a\\t H:i:s'
|
||||||
|
*
|
||||||
|
* @param date Date
|
||||||
|
* @returns String
|
||||||
|
*/
|
||||||
|
function getDateString( date ) {
|
||||||
|
return twoDigits( date.getDate() ) + '/'
|
||||||
|
+ twoDigits( date.getMonth() ) + '/'
|
||||||
|
+ date.getFullYear() + ' at '
|
||||||
|
+ twoDigits(date.getHours()) + ':'
|
||||||
|
+ twoDigits(date.getMinutes()) + ':'
|
||||||
|
+ twoDigits(date.getSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update the author view
|
||||||
|
*
|
||||||
|
* @param author
|
||||||
|
*/
|
||||||
|
function setAuthor( author ) {
|
||||||
|
authorDisplay.html(author.name)
|
||||||
|
.attr('href', 'mailto:' + author.email );
|
||||||
|
|
||||||
|
imageDisplay.attr('src', author.image );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the commit that is being displayed in this detail overlay instance
|
||||||
|
*
|
||||||
|
* @param commit
|
||||||
|
* @return that
|
||||||
|
*/
|
||||||
|
that.setCommit = function( commit ) {
|
||||||
|
setAuthor( commit.author );
|
||||||
|
dateDisplay.html( ' authored on ' + getDateString( commit.date ) );
|
||||||
|
messageDisplay.html( commit.message );
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
// expose some jquery functions
|
||||||
|
|
||||||
|
that.show = function() {
|
||||||
|
el.show();
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
that.hide = function() {
|
||||||
|
el.hide();
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
that.appendTo = function(where) {
|
||||||
|
el.appendTo(where);
|
||||||
|
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
that.positionTo = function( x, y ) {
|
||||||
|
el.css('left', x + 'px');
|
||||||
|
el.css('top', y + 'px');
|
||||||
|
};
|
||||||
|
|
||||||
|
that.outerWidth = function( ) {
|
||||||
|
return el.outerWidth.apply(el, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return that;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitDataRetriever( startPage, callback ) {
|
||||||
|
var that = {},
|
||||||
|
nextPage = startPage,
|
||||||
|
isLoading = false,
|
||||||
|
indicatorElements;
|
||||||
|
|
||||||
|
that.updateIndicators = function() {
|
||||||
|
if( isLoading ) {
|
||||||
|
$(indicatorElements).addClass('loading-commits');
|
||||||
|
} else {
|
||||||
|
$(indicatorElements).removeClass('loading-commits');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
that.bindIndicator = function( el ) {
|
||||||
|
if( !indicatorElements ) {
|
||||||
|
indicatorElements = $(el);
|
||||||
|
} else {
|
||||||
|
indicatorElements = indicatorElements.add(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
that.unbindIndicator = function( el ) {
|
||||||
|
indicatorElements.not( el );
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleNetworkDataLoaded(data) {
|
||||||
|
isLoading = false;
|
||||||
|
that.updateIndicators();
|
||||||
|
nextPage = data.nextPage;
|
||||||
|
|
||||||
|
if( !data.commits || data.commits.length === 0 ) {
|
||||||
|
callback( null );
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(data.commits);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNetworkDataError() {
|
||||||
|
throw "Network Data Error while retrieving Commits";
|
||||||
|
}
|
||||||
|
|
||||||
|
that.retrieve = function() {
|
||||||
|
|
||||||
|
if( !nextPage ) {
|
||||||
|
callback( null );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
that.updateIndicators();
|
||||||
|
$.ajax({
|
||||||
|
dataType: "json",
|
||||||
|
url: nextPage,
|
||||||
|
success: handleNetworkDataLoaded,
|
||||||
|
error: handleNetworkDataError
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
that.hasMore = function () {
|
||||||
|
return ( !!nextPage );
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// the $(document).ready starting point
|
||||||
|
$( function() {
|
||||||
|
|
||||||
|
// initialise network graph only when there is one network graph container on the page
|
||||||
|
if( $('div.network-graph').length !== 1 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var
|
||||||
|
// the element into which we will render our graph
|
||||||
|
commitsGraph = $('div.network-graph').first(),
|
||||||
|
laneManager = graphLaneManager(),
|
||||||
|
dataRetriever = commitDataRetriever( commitsGraph.data('source'), handleCommitsRetrieved ),
|
||||||
|
paper = Raphael( commitsGraph[0], commitsGraph.width(), commitsGraph.height()),
|
||||||
|
usedColumns = 0,
|
||||||
|
detailOverlay = commitDetailOverlay();
|
||||||
|
|
||||||
|
dataRetriever.bindIndicator( commitsGraph.parent('.network-view') );
|
||||||
|
detailOverlay.appendTo( commitsGraph );
|
||||||
|
|
||||||
|
|
||||||
|
function handleEnterHotZone() {
|
||||||
|
dataRetriever.retrieve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommitsRetrieved( commits ) {
|
||||||
|
|
||||||
|
// no commits or empty commits array? Well, we can't draw a graph of that
|
||||||
|
if( commits === null ) {
|
||||||
|
handleNoAvailableData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareCommits( commits );
|
||||||
|
renderCommits( commits );
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNoAvailableData() {
|
||||||
|
console.log('No Data available');
|
||||||
|
}
|
||||||
|
|
||||||
|
var awaitedParents = {};
|
||||||
|
|
||||||
|
function prepareCommits( commits ) {
|
||||||
|
$.each( commits, function ( index, commit) {
|
||||||
|
prepareCommit( commit );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareCommit( commit ) {
|
||||||
|
// make "date" an actual JS Date object
|
||||||
|
commit.date = new Date(commit.date*1000);
|
||||||
|
|
||||||
|
// the parents will be filled once they have become prepared
|
||||||
|
commit.parents = [];
|
||||||
|
|
||||||
|
// we will want to store this commit's children
|
||||||
|
commit.children = getChildrenFor( commit );
|
||||||
|
|
||||||
|
commit.isFork = ( commit.children.length > 1 );
|
||||||
|
commit.isMerge = ( commit.parentsHash.length > 1 );
|
||||||
|
|
||||||
|
// after a fork, the occupied lanes must be cleaned up. The children used some lanes we no longer occupy
|
||||||
|
if ( commit.isFork === true ) {
|
||||||
|
$.each( commit.children, function( key, thisChild ) {
|
||||||
|
// free this lane
|
||||||
|
laneManager.occupy( thisChild.lane );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
commit.lane = laneManager.getLaneForCommit( commit );
|
||||||
|
|
||||||
|
// now the lane we chose must be marked occupied again.
|
||||||
|
laneManager.occupy( commit.lane );
|
||||||
|
|
||||||
|
registerAwaitedParentsFor( commit );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new childCommit to the dictionary of awaited parents
|
||||||
|
*
|
||||||
|
* @param commit who is waiting?
|
||||||
|
*/
|
||||||
|
function registerAwaitedParentsFor( commit ) {
|
||||||
|
// This commit's parents are not yet known in our little world, as we are rendering following the time line.
|
||||||
|
// Therefore we are registering this commit as "waiting" for each of the parent hashes
|
||||||
|
$.each( commit.parentsHash, function( key, thisParentHash ) {
|
||||||
|
// If awaitedParents does not already have a key for thisParent's hash, initialise as array
|
||||||
|
if( !awaitedParents.hasOwnProperty(thisParentHash) ) {
|
||||||
|
awaitedParents[thisParentHash] = [ commit ];
|
||||||
|
} else {
|
||||||
|
awaitedParents[ thisParentHash ].push( commit );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChildrenFor( commit ) {
|
||||||
|
var children = [];
|
||||||
|
|
||||||
|
if( awaitedParents.hasOwnProperty( commit.hash )) {
|
||||||
|
// there are child commits waiting
|
||||||
|
children = awaitedParents[ commit.hash ];
|
||||||
|
|
||||||
|
// let the children know their parent objects
|
||||||
|
$.each( children, function(key, thisChild ) {
|
||||||
|
thisChild.parents.push( commit );
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove this item from parentsBeingWaitedFor
|
||||||
|
delete awaitedParents[ commit.hash ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastRenderedDate = new Date(0);
|
||||||
|
function renderCommits( commits ) {
|
||||||
|
|
||||||
|
var neededWidth = ((usedColumns + Object.keys(commits).length) * cfg.columnWidth);
|
||||||
|
|
||||||
|
if ( neededWidth > paper.width ) {
|
||||||
|
extendPaper( neededWidth, paper.height );
|
||||||
|
} else if( dataRetriever.hasMore() ) {
|
||||||
|
// this is the case when we have not loaded enough commits to fill the paper yet. Get some more then...
|
||||||
|
dataRetriever.retrieve();
|
||||||
|
}
|
||||||
|
|
||||||
|
$.each( commits, function ( index, commit) {
|
||||||
|
if( lastRenderedDate.getYear() !== commit.date.getYear()
|
||||||
|
|| lastRenderedDate.getMonth() !== commit.date.getMonth()
|
||||||
|
|| lastRenderedDate.getDate() !== commit.date.getDate() ) {
|
||||||
|
// TODO: If desired, one could add a time scale on top, maybe.
|
||||||
|
}
|
||||||
|
renderCommit(commit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCommit( commit ) {
|
||||||
|
// find the column this dot is drawn on
|
||||||
|
usedColumns++;
|
||||||
|
commit.column = usedColumns;
|
||||||
|
|
||||||
|
commit.dot = paper.circle( getXPositionForColumnNumber(commit.column), commit.lane.centerY, cfg.dotRadius );
|
||||||
|
commit.dot.attr({
|
||||||
|
fill: commit.lane.color,
|
||||||
|
stroke: 'none',
|
||||||
|
cursor: 'pointer'
|
||||||
|
})
|
||||||
|
.data('commit', commit)
|
||||||
|
.mouseover( handleCommitMouseover )
|
||||||
|
.mouseout( handleCommitMouseout )
|
||||||
|
.click( handleCommitClick );
|
||||||
|
|
||||||
|
// maybe we have not enough space for the lane yet
|
||||||
|
if( commit.lane.centerY + cfg.laneHeight > paper.height ) {
|
||||||
|
extendPaper( paper.width, commit.lane.centerY + cfg.laneHeight )
|
||||||
|
}
|
||||||
|
|
||||||
|
$.each( commit.children, function ( idx, thisChild ) {
|
||||||
|
|
||||||
|
// if there is one child only, stay on the commit's lane as long as possible when connecting the dots.
|
||||||
|
// but if there is more than one child, switch to the child's lane ASAP.
|
||||||
|
// this is to display merges and forks where they happen (ie. at a commit node/ a dot), rather than
|
||||||
|
// connecting from a line.
|
||||||
|
// So: commit.isFork decides whether or not we must switch lanes early
|
||||||
|
|
||||||
|
connectDots( commit, thisChild, commit.isFork );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param firstCommit
|
||||||
|
* @param secondCommit
|
||||||
|
* @param switchLanesEarly (boolean): Move the line to the secondCommit's lane ASAP? Defaults to false
|
||||||
|
*/
|
||||||
|
function connectDots( firstCommit, secondCommit, switchLanesEarly ) {
|
||||||
|
// default value for switchLanesEarly
|
||||||
|
switchLanesEarly = switchLanesEarly || false;
|
||||||
|
|
||||||
|
var lineLane = switchLanesEarly ? secondCommit.lane : firstCommit.lane;
|
||||||
|
|
||||||
|
// the connection has 4 stops, resulting in the following 3 segments:
|
||||||
|
// - from the x/y center of firstCommit.dot to the rightmost end (x) of the commit's column, with y=lineLane
|
||||||
|
// - from the rightmost end of firstCommit's column, to the leftmost end of secondCommit's column
|
||||||
|
// - from the leftmost end of secondCommit's column (y=lineLane) to the x/y center of secondCommit
|
||||||
|
|
||||||
|
paper.path(
|
||||||
|
getSvgLineString(
|
||||||
|
[firstCommit.dot.attr('cx'), firstCommit.dot.attr('cy')],
|
||||||
|
[firstCommit.dot.attr('cx') + (cfg.columnWidth/2), lineLane.centerY],
|
||||||
|
[secondCommit.dot.attr('cx') - (cfg.columnWidth/2), lineLane.centerY],
|
||||||
|
[secondCommit.dot.attr('cx'), secondCommit.dot.attr('cy')]
|
||||||
|
)
|
||||||
|
).attr({ "stroke": lineLane.color, "stroke-width": 2 }).toBack();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set together a path string from any amount of arguments
|
||||||
|
// each argument is an array of [x, y] within the paper's coordinate system
|
||||||
|
function getSvgLineString( ) {
|
||||||
|
if (arguments.length < 2) return;
|
||||||
|
|
||||||
|
var svgString = 'M' + arguments[0][0] + ' ' + arguments[0][1];
|
||||||
|
|
||||||
|
for (var i = 1, j = arguments.length; i < j; i++){
|
||||||
|
svgString += 'L' + arguments[i][0] + ' ' + arguments[i][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return svgString;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommitMouseover(evt) {
|
||||||
|
detailOverlay.setCommit( this.data('commit'))
|
||||||
|
.show();
|
||||||
|
|
||||||
|
var xPos = evt.pageX - commitsGraph.offset().left + commitsGraph.scrollLeft() - (detailOverlay.outerWidth()/2);
|
||||||
|
// check that x doesn't run out the viewport
|
||||||
|
xPos = Math.max( xPos, commitsGraph.scrollLeft() + 10);
|
||||||
|
xPos = Math.min( xPos, commitsGraph.scrollLeft() + commitsGraph.width() - detailOverlay.outerWidth() - 10);
|
||||||
|
|
||||||
|
detailOverlay.positionTo( xPos,
|
||||||
|
evt.pageY - commitsGraph.offset().top + commitsGraph.scrollTop() + 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommitMouseout(evt) {
|
||||||
|
detailOverlay.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommitClick( evt ) {
|
||||||
|
window.open( this.data('commit').details );
|
||||||
|
}
|
||||||
|
|
||||||
|
function getXPositionForColumnNumber( columnNumber ) {
|
||||||
|
// we want the column's center point
|
||||||
|
return ( paper.width - ( columnNumber * cfg.columnWidth ) + (cfg.columnWidth / 2 ));
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendPaper( newWidth, newHeight ) {
|
||||||
|
var deltaX = newWidth - paper.width;
|
||||||
|
|
||||||
|
paper.setSize( newWidth, newHeight );
|
||||||
|
// fixup parent's scroll position
|
||||||
|
paper.canvas.parentNode.scrollLeft = paper.canvas.parentNode.scrollLeft + deltaX;
|
||||||
|
|
||||||
|
// now fixup the x positions of existing circles and lines
|
||||||
|
paper.forEach( function( el ) {
|
||||||
|
|
||||||
|
if( el.type === "circle" ) {
|
||||||
|
el.attr('cx', el.attr('cx') + deltaX);
|
||||||
|
} else if ( el.type === "path") {
|
||||||
|
var newXTranslation = el.data('currentXTranslation') || 0;
|
||||||
|
newXTranslation += deltaX;
|
||||||
|
el.transform( 't' + newXTranslation + ' 0' );
|
||||||
|
el.data('currentXTranslation', newXTranslation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
commitsGraph.dragScrollr();
|
||||||
|
commitsGraph.on('enterHotZone', handleEnterHotZone);
|
||||||
|
// load initial data
|
||||||
|
dataRetriever.retrieve( );
|
||||||
|
});
|
||||||
|
}( jQuery ));
|
||||||
10
web/js/raphael.js
Normal file
10
web/js/raphael.js
Normal file
File diff suppressed because one or more lines are too long
1
web/less/bootstrap.less
vendored
1
web/less/bootstrap.less
vendored
@@ -26,6 +26,7 @@
|
|||||||
@import "forms.less";
|
@import "forms.less";
|
||||||
@import "tables.less";
|
@import "tables.less";
|
||||||
@import "files.less";
|
@import "files.less";
|
||||||
|
@import "network.less";
|
||||||
|
|
||||||
// Components: common
|
// Components: common
|
||||||
@import "sprites.less";
|
@import "sprites.less";
|
||||||
|
|||||||
94
web/less/network.less
Normal file
94
web/less/network.less
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
.network-view {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: @baseLineHeight;
|
||||||
|
border: 1px solid @treeHeaderBorder;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.loading-commits {
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 100px;
|
||||||
|
width: 200px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
margin-left: -100px;
|
||||||
|
margin-top: -50px;
|
||||||
|
|
||||||
|
color: #555;
|
||||||
|
background: rgba(255,255,255,.8) url("@{ajaxLoaderPath}") no-repeat center;
|
||||||
|
z-index: 2000;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 2px 2px 2px #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-header .meta:after {
|
||||||
|
content: " - Loading";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-header {
|
||||||
|
padding: 8px;
|
||||||
|
line-height: @baseLineHeight;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: bottom;
|
||||||
|
#gradient > .vertical(@treeHeaderHighlight, @treeHeader);
|
||||||
|
border-bottom: 1px solid lighten(@treeHeaderBorder, 5%);
|
||||||
|
font-weight: bold;
|
||||||
|
color: @gray;
|
||||||
|
text-shadow: 1px 1px 1px rgba(255, 255, 255, 1);
|
||||||
|
height: 28px;
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
float: left;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.network-graph {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: move;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.network-commit-overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 400px;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0 0 @baseLineHeight;
|
||||||
|
list-style: none;
|
||||||
|
#gradient > .vertical(@white, #f5f5f5);
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
|
||||||
|
.border-radius(3px);
|
||||||
|
.box-shadow(~"0 1px 0 rgba(255,255,255,0.4), 0 0 10px rgba(0,0,0,0.1)");
|
||||||
|
|
||||||
|
img {
|
||||||
|
float: left;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>h4,
|
||||||
|
&>p {
|
||||||
|
padding-left: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,6 +131,7 @@
|
|||||||
@iconSpritePath: "../img/glyphicons-halflings.png";
|
@iconSpritePath: "../img/glyphicons-halflings.png";
|
||||||
@iconWhiteSpritePath: "../img/glyphicons-halflings-white.png";
|
@iconWhiteSpritePath: "../img/glyphicons-halflings-white.png";
|
||||||
@rssIconPath: "../img/feed.png";
|
@rssIconPath: "../img/feed.png";
|
||||||
|
@ajaxLoaderPath: "../img/ajax-loader.gif";
|
||||||
|
|
||||||
// Input placeholder text color
|
// Input placeholder text color
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user