diff options
author | zeripath <art27@cantab.net> | 2020-08-06 09:04:08 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-06 09:04:08 +0100 |
commit | 2c1ae6c82d0b3fa62dda7e6a30fb91e27aba6e04 (patch) | |
tree | be14ac1376125be2482e6ca7de3eedc276203304 /web_src/js | |
parent | f1a42f5d5ee0279ddec7973a1ba9236c70bd5b5e (diff) | |
download | gitea-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.js | 641 |
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'); + }); } |