mirror of
https://github.com/redmine/redmine.git
synced 2026-03-24 13:21:04 +01:00
Patch by Mizuki ISHIKAWA (user:ishikawa999). git-svn-id: https://svn.redmine.org/redmine/trunk@24469 e93f8b46-1217-0410-a6f0-8f06a7374b81
226 lines
7.7 KiB
JavaScript
226 lines
7.7 KiB
JavaScript
/**
|
|
* Redmine - project management software
|
|
* Copyright (C) 2006- Jean-Philippe Lang
|
|
* This code is released under the GNU General Public License.
|
|
*/
|
|
|
|
var revisionGraph = null;
|
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
const XLINK_NS = 'http://www.w3.org/1999/xlink';
|
|
|
|
function createSvgElement(name) {
|
|
return document.createElementNS(SVG_NS, name);
|
|
}
|
|
|
|
function buildRevisionGraph(holder) {
|
|
const svg = createSvgElement('svg');
|
|
const pathsLayer = createSvgElement('g');
|
|
const nodesLayer = createSvgElement('g');
|
|
const overlaysLayer = createSvgElement('g');
|
|
|
|
svg.setAttribute('aria-hidden', 'true');
|
|
svg.style.display = 'block';
|
|
svg.style.overflow = 'visible';
|
|
|
|
svg.appendChild(pathsLayer);
|
|
svg.appendChild(nodesLayer);
|
|
svg.appendChild(overlaysLayer);
|
|
|
|
while (holder.firstChild) {
|
|
holder.removeChild(holder.firstChild);
|
|
}
|
|
holder.appendChild(svg);
|
|
|
|
return {
|
|
holder: holder,
|
|
svg: svg,
|
|
pathsLayer: pathsLayer,
|
|
nodesLayer: nodesLayer,
|
|
overlaysLayer: overlaysLayer
|
|
};
|
|
}
|
|
|
|
function clearRevisionGraph(graph) {
|
|
[graph.pathsLayer, graph.nodesLayer, graph.overlaysLayer].forEach(layer => layer.replaceChildren());
|
|
}
|
|
|
|
function setRevisionGraphSize(graph, width, height) {
|
|
const graphWidth = Math.max(1, Math.ceil(width));
|
|
const graphHeight = Math.max(1, Math.ceil(height));
|
|
|
|
graph.svg.setAttribute('width', graphWidth);
|
|
graph.svg.setAttribute('height', graphHeight);
|
|
graph.svg.setAttribute('viewBox', '0 0 ' + graphWidth + ' ' + graphHeight);
|
|
}
|
|
|
|
function drawPath(graph, pathData, attrs) {
|
|
const path = createSvgElement('path');
|
|
const d = pathData.map(function(item) { return String(item); }).join(' ');
|
|
|
|
path.setAttribute('d', d);
|
|
Object.keys(attrs).forEach(function(name) {
|
|
path.setAttribute(name, String(attrs[name]));
|
|
});
|
|
graph.pathsLayer.appendChild(path);
|
|
}
|
|
|
|
function drawCircle(layer, x, y, r, attrs) {
|
|
const circle = createSvgElement('circle');
|
|
|
|
circle.setAttribute('cx', String(x));
|
|
circle.setAttribute('cy', String(y));
|
|
circle.setAttribute('r', String(r));
|
|
Object.keys(attrs).forEach(function(name) {
|
|
circle.setAttribute(name, String(attrs[name]));
|
|
});
|
|
layer.appendChild(circle);
|
|
|
|
return circle;
|
|
}
|
|
|
|
// Generates a distinct, consistent HSL color for each index.
|
|
function colorBySpace(index) {
|
|
const hue = (index * 27) % 360;
|
|
const band = Math.floor(index / 14);
|
|
const saturation = Math.max(40, 72 - (band % 3) * 12);
|
|
const lightness = Math.min(56, 42 + (band % 2) * 6);
|
|
|
|
return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
|
|
}
|
|
|
|
function drawRevisionGraph(holder, commits_hash, graph_space) {
|
|
var XSTEP = 20,
|
|
CIRCLE_INROW_OFFSET = 10;
|
|
var commits_by_scmid = commits_hash,
|
|
commits = $.map(commits_by_scmid, function(val,i){return val;});
|
|
var max_rdmid = commits.length - 1;
|
|
var commit_table_rows = $('table.changesets tr.changeset');
|
|
if (!revisionGraph || revisionGraph.holder !== holder) {
|
|
revisionGraph = buildRevisionGraph(holder);
|
|
}
|
|
const graph = revisionGraph;
|
|
clearRevisionGraph(graph);
|
|
|
|
if (commit_table_rows.length === 0) {
|
|
setRevisionGraphSize(graph, 1, 1);
|
|
return;
|
|
}
|
|
|
|
// init dimensions
|
|
var graph_x_offset = commit_table_rows.first().find('td').first().position().left - $(holder).position().left,
|
|
graph_y_offset = $(holder).position().top,
|
|
graph_right_side = graph_x_offset + (graph_space + 1) * XSTEP,
|
|
graph_bottom = commit_table_rows.last().position().top + commit_table_rows.last().height() - graph_y_offset;
|
|
|
|
|
|
var yForRow = function (index, commit) {
|
|
var row = commit_table_rows.eq(index);
|
|
|
|
switch (row.find("td:first").css("vertical-align")) {
|
|
case "middle":
|
|
return row.position().top + (row.height() / 2) - graph_y_offset;
|
|
default:
|
|
return row.position().top - graph_y_offset + CIRCLE_INROW_OFFSET;
|
|
}
|
|
};
|
|
|
|
setRevisionGraphSize(graph, graph_right_side, graph_bottom);
|
|
|
|
// init colors
|
|
var colors = [];
|
|
for (let k = 0; k <= graph_space; k++) {
|
|
colors.push(colorBySpace(k));
|
|
}
|
|
|
|
var parent_commit;
|
|
var x, y, parent_x, parent_y;
|
|
var path, title;
|
|
var revision_dot_overlay;
|
|
$.each(commits, function(index, commit) {
|
|
if (!commit.hasOwnProperty("space"))
|
|
commit.space = 0;
|
|
|
|
y = yForRow(max_rdmid - commit.rdmid);
|
|
x = graph_x_offset + XSTEP / 2 + XSTEP * commit.space;
|
|
drawCircle(graph.nodesLayer, x, y, 3, {
|
|
fill: colors[commit.space],
|
|
stroke: 'none'
|
|
});
|
|
|
|
// check for parents in the same column
|
|
let noVerticalParents = true;
|
|
$.each(commit.parent_scmids, function (index, parentScmid) {
|
|
parent_commit = commits_by_scmid[parentScmid];
|
|
if (parent_commit) {
|
|
if (!parent_commit.hasOwnProperty("space"))
|
|
parent_commit.space = 0;
|
|
|
|
// has parent in the same column on this page
|
|
if (parent_commit.space === commit.space)
|
|
noVerticalParents = false;
|
|
} else {
|
|
// has parent in the same column on the other page
|
|
noVerticalParents = false;
|
|
}
|
|
});
|
|
|
|
// paths to parents
|
|
$.each(commit.parent_scmids, function(index, parent_scmid) {
|
|
parent_commit = commits_by_scmid[parent_scmid];
|
|
if (parent_commit) {
|
|
parent_y = yForRow(max_rdmid - parent_commit.rdmid);
|
|
parent_x = graph_x_offset + XSTEP / 2 + XSTEP * parent_commit.space;
|
|
const controlPointDelta = (parent_y - y) / 8;
|
|
|
|
if (parent_commit.space === commit.space) {
|
|
// vertical path
|
|
path = [
|
|
'M', x, y,
|
|
'V', parent_y];
|
|
} else if (noVerticalParents) {
|
|
// branch start (Bezier curve)
|
|
path = [
|
|
'M', x, y,
|
|
'C', x, y + controlPointDelta, x, parent_y - controlPointDelta, parent_x, parent_y];
|
|
} else if (!parent_commit.hasOwnProperty('vertical_children')) {
|
|
// branch end (Bezier curve)
|
|
path = [
|
|
'M', x, y,
|
|
'C', parent_x, y + controlPointDelta, parent_x, parent_y, parent_x, parent_y];
|
|
} else {
|
|
// path to a commit in a different branch (Bezier curve)
|
|
path = [
|
|
'M', x, y,
|
|
'C', parent_x, y, x, parent_y, parent_x, parent_y];
|
|
}
|
|
} else {
|
|
// vertical path ending at the bottom of the revisionGraph
|
|
path = [
|
|
'M', x, y,
|
|
'V', graph_bottom];
|
|
}
|
|
drawPath(graph, path, {stroke: colors[commit.space], 'stroke-width': 1.5, fill: 'none'});
|
|
});
|
|
|
|
let overlayLayer = graph.overlaysLayer;
|
|
if (commit.href) {
|
|
overlayLayer = createSvgElement('a');
|
|
overlayLayer.setAttribute('href', commit.href);
|
|
overlayLayer.setAttributeNS(XLINK_NS, 'href', commit.href);
|
|
graph.overlaysLayer.appendChild(overlayLayer);
|
|
}
|
|
|
|
revision_dot_overlay = drawCircle(overlayLayer, x, y, 10, {
|
|
fill: '#000',
|
|
opacity: 0,
|
|
cursor: commit.href ? 'pointer' : 'default'
|
|
});
|
|
|
|
if(commit.refs != null && commit.refs.length > 0) {
|
|
title = createSvgElement('title');
|
|
title.appendChild(document.createTextNode(commit.refs));
|
|
revision_dot_overlay.appendChild(title);
|
|
}
|
|
});
|
|
};
|