aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js
diff options
context:
space:
mode:
authorzeripath <art27@cantab.net>2020-08-06 09:04:08 +0100
committerGitHub <noreply@github.com>2020-08-06 09:04:08 +0100
commit2c1ae6c82d0b3fa62dda7e6a30fb91e27aba6e04 (patch)
treebe14ac1376125be2482e6ca7de3eedc276203304 /web_src/js
parentf1a42f5d5ee0279ddec7973a1ba9236c70bd5b5e (diff)
downloadgitea-2c1ae6c82d0b3fa62dda7e6a30fb91e27aba6e04.tar.gz
gitea-2c1ae6c82d0b3fa62dda7e6a30fb91e27aba6e04.zip
Render the git graph on the server (#12333)
Rendering the git graph on the server means that we can properly track flows and switch from the Canvas implementation to a SVG implementation. * This implementation provides a 16 limited color selection * The uniqued color numbers are also provided * And there is also a monochrome version *In addition is a hover highlight that allows users to highlight commits on the same flow. Closes #12209 Signed-off-by: Andrew Thornton art27@cantab.net Co-authored-by: silverwind <me@silverwind.io>
Diffstat (limited to 'web_src/js')
-rw-r--r--web_src/js/features/gitgraph.js641
1 files changed, 77 insertions, 564 deletions
diff --git a/web_src/js/features/gitgraph.js b/web_src/js/features/gitgraph.js
index 3e6b27436d..655cfb77c2 100644
--- a/web_src/js/features/gitgraph.js
+++ b/web_src/js/features/gitgraph.js
@@ -1,568 +1,81 @@
-// Although inspired by the https://github.com/bluef/gitgraph.js/blob/master/gitgraph.js
-// this has been completely rewritten with almost no remaining code
-
-// GitGraphCanvas is a canvas for drawing gitgraphs on to
-class GitGraphCanvas {
- constructor(canvas, widthUnits, heightUnits, config) {
- this.ctx = canvas.getContext('2d');
-
- const width = widthUnits * config.unitSize;
- this.height = heightUnits * config.unitSize;
-
- const ratio = window.devicePixelRatio || 1;
-
- canvas.width = width * ratio;
- canvas.height = this.height * ratio;
-
- canvas.style.width = `${width}px`;
- canvas.style.height = `${this.height}px`;
-
- this.ctx.lineWidth = config.lineWidth;
- this.ctx.lineJoin = 'round';
- this.ctx.lineCap = 'round';
-
- this.ctx.scale(ratio, ratio);
- this.config = config;
- }
- drawLine(moveX, moveY, lineX, lineY, color) {
- this.ctx.strokeStyle = color;
- this.ctx.beginPath();
- this.ctx.moveTo(moveX, moveY);
- this.ctx.lineTo(lineX, lineY);
- this.ctx.stroke();
- }
- drawLineRight(x, y, color) {
- this.drawLine(
- x - 0.5 * this.config.unitSize,
- y + this.config.unitSize / 2,
- x + 0.5 * this.config.unitSize,
- y + this.config.unitSize / 2,
- color
- );
- }
- drawLineUp(x, y, color) {
- this.drawLine(
- x,
- y + this.config.unitSize / 2,
- x,
- y - this.config.unitSize / 2,
- color
- );
- }
- drawNode(x, y, color) {
- this.ctx.strokeStyle = color;
-
- this.drawLineUp(x, y, color);
-
- this.ctx.beginPath();
- this.ctx.arc(x, y, this.config.nodeRadius, 0, Math.PI * 2, true);
- this.ctx.fillStyle = color;
- this.ctx.fill();
- }
- drawLineIn(x, y, color) {
- this.drawLine(
- x + 0.5 * this.config.unitSize,
- y + this.config.unitSize / 2,
- x - 0.5 * this.config.unitSize,
- y - this.config.unitSize / 2,
- color
- );
- }
- drawLineOut(x, y, color) {
- this.drawLine(
- x - 0.5 * this.config.unitSize,
- y + this.config.unitSize / 2,
- x + 0.5 * this.config.unitSize,
- y - this.config.unitSize / 2,
- color
- );
- }
- drawSymbol(symbol, columnNumber, rowNumber, color) {
- const y = this.height - this.config.unitSize * (rowNumber + 0.5);
- const x = this.config.unitSize * 0.5 * (columnNumber + 1);
- switch (symbol) {
- case '-':
- if (columnNumber % 2 === 1) {
- this.drawLineRight(x, y, color);
- }
- break;
- case '_':
- this.drawLineRight(x, y, color);
- break;
- case '*':
- this.drawNode(x, y, color);
- break;
- case '|':
- this.drawLineUp(x, y, color);
- break;
- case '/':
- this.drawLineOut(x, y, color);
- break;
- case '\\':
- this.drawLineIn(x, y, color);
- break;
- case '.':
- case ' ':
- break;
- default:
- console.error('Unknown symbol', symbol, color);
- }
- }
-}
-
-class GitGraph {
- constructor(canvas, rawRows, config) {
- this.rows = [];
- let maxWidth = 0;
-
- for (let i = 0; i < rawRows.length; i++) {
- const rowStr = rawRows[i];
- maxWidth = Math.max(rowStr.replace(/([_\s.-])/g, '').length, maxWidth);
-
- const rowArray = rowStr.split('');
-
- this.rows.unshift(rowArray);
- }
-
- this.currentFlows = [];
- this.previousFlows = [];
-
- this.gitGraphCanvas = new GitGraphCanvas(
- canvas,
- maxWidth,
- this.rows.length,
- config
- );
- }
-
- generateNewFlow(column) {
- let newId;
-
- do {
- newId = generateRandomColorString();
- } while (this.hasFlow(newId, column));
-
- return {id: newId, color: `#${newId}`};
- }
-
- hasFlow(id, column) {
- // We want to find the flow with the current ID
- // Possible flows are those in the currentFlows
- // Or flows in previousFlows[column-2:...]
- for (
- let idx = column - 2 < 0 ? 0 : column - 2;
- idx < this.previousFlows.length;
- idx++
- ) {
- if (this.previousFlows[idx] && this.previousFlows[idx].id === id) {
- return true;
- }
- }
- for (let idx = 0; idx < this.currentFlows.length; idx++) {
- if (this.currentFlows[idx] && this.currentFlows[idx].id === id) {
- return true;
- }
- }
- return false;
- }
-
- takePreviousFlow(column) {
- if (column < this.previousFlows.length && this.previousFlows[column]) {
- const flow = this.previousFlows[column];
- this.previousFlows[column] = null;
- return flow;
- }
- return this.generateNewFlow(column);
- }
-
- draw() {
- if (this.rows.length === 0) {
- return;
- }
-
- this.currentFlows = new Array(this.rows[0].length);
-
- // Generate flows for the first row - I do not believe that this can contain '_', '-', '.'
- for (let column = 0; column < this.rows[0].length; column++) {
- if (this.rows[0][column] === ' ') {
- continue;
- }
- this.currentFlows[column] = this.generateNewFlow(column);
- }
-
- // Draw the first row
- for (let column = 0; column < this.rows[0].length; column++) {
- const symbol = this.rows[0][column];
- const color = this.currentFlows[column] ? this.currentFlows[column].color : '';
- this.gitGraphCanvas.drawSymbol(symbol, column, 0, color);
- }
-
- for (let row = 1; row < this.rows.length; row++) {
- // Done previous row - step up the row
- const currentRow = this.rows[row];
- const previousRow = this.rows[row - 1];
-
- this.previousFlows = this.currentFlows;
- this.currentFlows = new Array(currentRow.length);
-
- // Set flows for this row
- for (let column = 0; column < currentRow.length; column++) {
- column = this.setFlowFor(column, currentRow, previousRow);
- }
-
- // Draw this row
- for (let column = 0; column < currentRow.length; column++) {
- const symbol = currentRow[column];
- const color = this.currentFlows[column] ? this.currentFlows[column].color : '';
- this.gitGraphCanvas.drawSymbol(symbol, column, row, color);
- }
- }
- }
-
- setFlowFor(column, currentRow, previousRow) {
- const symbol = currentRow[column];
- switch (symbol) {
- case '|':
- case '*':
- return this.setUpFlow(column, currentRow, previousRow);
- case '/':
- return this.setOutFlow(column, currentRow, previousRow);
- case '\\':
- return this.setInFlow(column, currentRow, previousRow);
- case '_':
- return this.setRightFlow(column, currentRow, previousRow);
- case '-':
- return this.setLeftFlow(column, currentRow, previousRow);
- case ' ':
- // In space no one can hear you flow ... (?)
- return column;
- default:
- // Unexpected so let's generate a new flow and wait for bug-reports
- this.currentFlows[column] = this.generateNewFlow(column);
- return column;
- }
- }
-
- // setUpFlow handles '|' or '*' - returns the last column that was set
- // generally we prefer to take the left most flow from the previous row
- setUpFlow(column, currentRow, previousRow) {
- // If ' |/' or ' |_'
- // '/|' '/|' -> Take the '|' flow directly beneath us
- if (
- column + 1 < currentRow.length &&
- (currentRow[column + 1] === '/' || currentRow[column + 1] === '_') &&
- column < previousRow.length &&
- (previousRow[column] === '|' || previousRow[column] === '*') &&
- previousRow[column - 1] === '/'
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column);
- return column;
- }
-
- // If ' |/' or ' |_'
- // '/ ' '/ ' -> Take the '/' flow from the preceding column
- if (
- column + 1 < currentRow.length &&
- (currentRow[column + 1] === '/' || currentRow[column + 1] === '_') &&
- column - 1 < previousRow.length &&
- previousRow[column - 1] === '/'
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column - 1);
- return column;
- }
-
- // If ' |'
- // '/' -> Take the '/' flow - (we always prefer the left-most flow)
- if (
- column > 0 &&
- column - 1 < previousRow.length &&
- previousRow[column - 1] === '/'
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column - 1);
- return column;
- }
-
- // If '|' OR '|' take the '|' flow
- // '|' '*'
- if (
- column < previousRow.length &&
- (previousRow[column] === '|' || previousRow[column] === '*')
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column);
- return column;
- }
-
- // If '| ' keep the '\' flow
- // ' \'
- if (column + 1 < previousRow.length && previousRow[column + 1] === '\\') {
- this.currentFlows[column] = this.takePreviousFlow(column + 1);
- return column;
- }
-
- // Otherwise just create a new flow - probably this is an error...
- this.currentFlows[column] = this.generateNewFlow(column);
- return column;
- }
-
- // setOutFlow handles '/' - returns the last column that was set
- // generally we prefer to take the left most flow from the previous row
- setOutFlow(column, currentRow, previousRow) {
- // If '_/' -> keep the '_' flow
- if (column > 0 && currentRow[column - 1] === '_') {
- this.currentFlows[column] = this.currentFlows[column - 1];
- return column;
- }
-
- // If '_|/' -> keep the '_' flow
- if (
- column > 1 &&
- (currentRow[column - 1] === '|' || currentRow[column - 1] === '*') &&
- currentRow[column - 2] === '_'
- ) {
- this.currentFlows[column] = this.currentFlows[column - 2];
- return column;
- }
-
- // If '|/'
- // '/' -> take the '/' flow (if it is still available)
- if (
- column > 1 &&
- currentRow[column - 1] === '|' &&
- column - 2 < previousRow.length &&
- previousRow[column - 2] === '/'
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column - 2);
- return column;
- }
-
- // If ' /'
- // '/' -> take the '/' flow, but transform the symbol to '|' due to our spacing
- // This should only happen if there are 3 '/' - in a row so we don't need to be cleverer here
- if (
- column > 0 &&
- currentRow[column - 1] === ' ' &&
- column - 1 < previousRow.length &&
- previousRow[column - 1] === '/'
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column - 1);
- currentRow[column] = '|';
- return column;
- }
-
- // If ' /'
- // '|' -> take the '|' flow
- if (
- column > 0 &&
- currentRow[column - 1] === ' ' &&
- column - 1 < previousRow.length &&
- (previousRow[column - 1] === '|' || previousRow[column - 1] === '*')
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column - 1);
- return column;
- }
-
- // If '/' <- Not sure this ever happens... but take the '\' flow
- // '\'
- if (column < previousRow.length && previousRow[column] === '\\') {
- this.currentFlows[column] = this.takePreviousFlow(column);
- return column;
- }
-
- // Otherwise just generate a new flow and wait for bug-reports...
- this.currentFlows[column] = this.generateNewFlow(column);
- return column;
- }
-
- // setInFlow handles '\' - returns the last column that was set
- // generally we prefer to take the left most flow from the previous row
- setInFlow(column, currentRow, previousRow) {
- // If '\?'
- // '/?' -> take the '/' flow
- if (column < previousRow.length && previousRow[column] === '/') {
- this.currentFlows[column] = this.takePreviousFlow(column);
- return column;
- }
-
- // If '\?'
- // ' \' -> take the '\' flow and reassign to '|'
- // This should only happen if there are 3 '\' - in a row so we don't need to be cleverer here
- if (column + 1 < previousRow.length && previousRow[column + 1] === '\\') {
- this.currentFlows[column] = this.takePreviousFlow(column + 1);
- currentRow[column] = '|';
- return column;
- }
-
- // If '\?'
- // ' |' -> take the '|' flow
- if (
- column + 1 < previousRow.length &&
- (previousRow[column + 1] === '|' || previousRow[column + 1] === '*')
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column + 1);
- return column;
- }
-
- // Otherwise just generate a new flow and wait for bug-reports if we're wrong...
- this.currentFlows[column] = this.generateNewFlow(column);
- return column;
- }
-
- // setRightFlow handles '_' - returns the last column that was set
- // generally we prefer to take the left most flow from the previous row
- setRightFlow(column, currentRow, previousRow) {
- // if '__' keep the '_' flow
- if (column > 0 && currentRow[column - 1] === '_') {
- this.currentFlows[column] = this.currentFlows[column - 1];
- return column;
- }
-
- // if '_|_' -> keep the '_' flow
- if (
- column > 1 &&
- currentRow[column - 1] === '|' &&
- currentRow[column - 2] === '_'
- ) {
- this.currentFlows[column] = this.currentFlows[column - 2];
- return column;
- }
-
- // if ' _' -> take the '/' flow
- // '/ '
- if (
- column > 0 &&
- column - 1 < previousRow.length &&
- previousRow[column - 1] === '/'
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column - 1);
- return column;
- }
-
- // if ' |_'
- // '/? ' -> take the '/' flow (this may cause generation...)
- // we can do this because we know that git graph
- // doesn't create compact graphs like: ' |_'
- // '//'
- if (
- column > 1 &&
- column - 2 < previousRow.length &&
- previousRow[column - 2] === '/'
- ) {
- this.currentFlows[column] = this.takePreviousFlow(column - 2);
- return column;
- }
-
- // There really shouldn't be another way of doing this - generate and wait for bug-reports...
-
- this.currentFlows[column] = this.generateNewFlow(column);
- return column;
- }
-
- // setLeftFlow handles '----.' - returns the last column that was set
- // generally we prefer to take the left most flow from the previous row that terminates this left recursion
- setLeftFlow(column, currentRow, previousRow) {
- // This is: '----------.' or the like
- // ' \ \ /|\'
-
- // Find the end of the '-' or nearest '/|\' in the previousRow :
- let originalColumn = column;
- let flow;
- for (; column < currentRow.length && currentRow[column] === '-'; column++) {
- if (column > 0 && column - 1 < previousRow.length && previousRow[column - 1] === '/') {
- flow = this.takePreviousFlow(column - 1);
- break;
- } else if (column < previousRow.length && previousRow[column] === '|') {
- flow = this.takePreviousFlow(column);
- break;
- } else if (
- column + 1 < previousRow.length &&
- previousRow[column + 1] === '\\'
- ) {
- flow = this.takePreviousFlow(column + 1);
- break;
- }
- }
-
- // if we have a flow then we found a '/|\' in the previousRow
- if (flow) {
- for (; originalColumn < column + 1; originalColumn++) {
- this.currentFlows[originalColumn] = flow;
- }
- return column;
- }
-
- // If the symbol in the column is not a '.' then there's likely an error
- if (currentRow[column] !== '.') {
- // It really should end in a '.' but this one doesn't...
- // 1. Step back - we don't want to eat this column
- column--;
- // 2. Generate a new flow and await bug-reports...
- this.currentFlows[column] = this.generateNewFlow(column);
-
- // 3. Assign all of the '-' to the same flow.
- for (; originalColumn < column; originalColumn++) {
- this.currentFlows[originalColumn] = this.currentFlows[column];
- }
- return column;
- }
-
- // We have a terminal '.' eg. the current row looks like '----.'
- // the previous row should look like one of '/|\' eg. ' \'
- if (column > 0 && column - 1 < previousRow.length && previousRow[column - 1] === '/') {
- flow = this.takePreviousFlow(column - 1);
- } else if (column < previousRow.length && previousRow[column] === '|') {
- flow = this.takePreviousFlow(column);
- } else if (
- column + 1 < previousRow.length &&
- previousRow[column + 1] === '\\'
- ) {
- flow = this.takePreviousFlow(column + 1);
+export default async function initGitGraph() {
+ const graphContainer = document.getElementById('git-graph-container');
+ if (!graphContainer) return;
+
+ $('#flow-color-monochrome').on('click', () => {
+ $('#flow-color-monochrome').addClass('active');
+ $('#flow-color-colored').removeClass('active');
+ $('#git-graph-container').removeClass('colored').addClass('monochrome');
+ const params = new URLSearchParams(window.location.search);
+ params.set('mode', 'monochrome');
+ const queryString = params.toString();
+ if (queryString) {
+ window.history.replaceState({}, '', `?${queryString}`);
} else {
- // Again unexpected so let's generate and wait the bug-report
- flow = this.generateNewFlow(column);
- }
-
- // Assign all of the rest of the ----. to this flow.
- for (; originalColumn < column + 1; originalColumn++) {
- this.currentFlows[originalColumn] = flow;
+ window.history.replaceState({}, '', window.location.pathname);
+ }
+ $('.pagination a').each((_, that) => {
+ const href = $(that).attr('href');
+ if (!href) return;
+ const url = new URL(href, window.location);
+ const params = url.searchParams;
+ params.set('mode', 'monochrome');
+ url.search = `?${params.toString()}`;
+ $(that).attr('href', url.href);
+ });
+ });
+ $('#flow-color-colored').on('click', () => {
+ $('#flow-color-colored').addClass('active');
+ $('#flow-color-monochrome').removeClass('active');
+ $('#git-graph-container').addClass('colored').removeClass('monochrome');
+ $('.pagination a').each((_, that) => {
+ const href = $(that).attr('href');
+ if (!href) return;
+ const url = new URL(href, window.location);
+ const params = url.searchParams;
+ params.delete('mode');
+ url.search = `?${params.toString()}`;
+ $(that).attr('href', url.href);
+ });
+ const params = new URLSearchParams(window.location.search);
+ params.delete('mode');
+ const queryString = params.toString();
+ if (queryString) {
+ window.history.replaceState({}, '', `?${queryString}`);
+ } else {
+ window.history.replaceState({}, '', window.location.pathname);
}
-
- return column;
- }
-}
-
-function generateRandomColorString() {
- const chars = '0123456789ABCDEF';
- const stringLength = 6;
- let randomString = '',
- rnum,
- i;
- for (i = 0; i < stringLength; i++) {
- rnum = Math.floor(Math.random() * chars.length);
- randomString += chars.substring(rnum, rnum + 1);
- }
-
- return randomString;
-}
-
-export default async function initGitGraph() {
- const graphCanvas = document.getElementById('graph-canvas');
- if (!graphCanvas || !graphCanvas.getContext) return;
-
- // Grab the raw graphList
- const graphList = [];
- $('#graph-raw-list li span.node-relation').each(function () {
- graphList.push($(this).text());
});
-
- // Define some drawing parameters
- const config = {
- unitSize: 20,
- lineWidth: 3,
- nodeRadius: 4
- };
-
-
- const gitGraph = new GitGraph(graphCanvas, graphList, config);
- gitGraph.draw();
- graphCanvas.closest('#git-graph-container').classList.add('in');
+ $('#git-graph-container').on('mouseenter', '#rev-list li', (e) => {
+ const flow = $(e.currentTarget).data('flow');
+ if (flow === 0) return;
+ $(`#flow-${flow}`).addClass('highlight');
+ $(e.currentTarget).addClass('hover');
+ $(`#rev-list li[data-flow='${flow}']`).addClass('highlight');
+ });
+ $('#git-graph-container').on('mouseleave', '#rev-list li', (e) => {
+ const flow = $(e.currentTarget).data('flow');
+ if (flow === 0) return;
+ $(`#flow-${flow}`).removeClass('highlight');
+ $(e.currentTarget).removeClass('hover');
+ $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight');
+ });
+ $('#git-graph-container').on('mouseenter', '#rel-container .flow-group', (e) => {
+ $(e.currentTarget).addClass('highlight');
+ const flow = $(e.currentTarget).data('flow');
+ $(`#rev-list li[data-flow='${flow}']`).addClass('highlight');
+ });
+ $('#git-graph-container').on('mouseleave', '#rel-container .flow-group', (e) => {
+ $(e.currentTarget).removeClass('highlight');
+ const flow = $(e.currentTarget).data('flow');
+ $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight');
+ });
+ $('#git-graph-container').on('mouseenter', '#rel-container .flow-commit', (e) => {
+ const rev = $(e.currentTarget).data('rev');
+ $(`#rev-list li#commit-${rev}`).addClass('hover');
+ });
+ $('#git-graph-container').on('mouseleave', '#rel-container .flow-commit', (e) => {
+ const rev = $(e.currentTarget).data('rev');
+ $(`#rev-list li#commit-${rev}`).removeClass('hover');
+ });
}