mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-27 01:44:10 -05:00
2c1ae6c82d
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>
338 lines
9.5 KiB
Go
338 lines
9.5 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package gitgraph
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
)
|
|
|
|
// Parser represents a git graph parser. It is stateful containing the previous
|
|
// glyphs, detected flows and color assignments.
|
|
type Parser struct {
|
|
glyphs []byte
|
|
oldGlyphs []byte
|
|
flows []int64
|
|
oldFlows []int64
|
|
maxFlow int64
|
|
colors []int
|
|
oldColors []int
|
|
availableColors []int
|
|
nextAvailable int
|
|
firstInUse int
|
|
firstAvailable int
|
|
maxAllowedColors int
|
|
}
|
|
|
|
// Reset resets the internal parser state.
|
|
func (parser *Parser) Reset() {
|
|
parser.glyphs = parser.glyphs[0:0]
|
|
parser.oldGlyphs = parser.oldGlyphs[0:0]
|
|
parser.flows = parser.flows[0:0]
|
|
parser.oldFlows = parser.oldFlows[0:0]
|
|
parser.maxFlow = 0
|
|
parser.colors = parser.colors[0:0]
|
|
parser.oldColors = parser.oldColors[0:0]
|
|
parser.availableColors = parser.availableColors[0:0]
|
|
parser.availableColors = append(parser.availableColors, 1, 2)
|
|
parser.nextAvailable = 0
|
|
parser.firstInUse = -1
|
|
parser.firstAvailable = 0
|
|
parser.maxAllowedColors = 0
|
|
}
|
|
|
|
// AddLineToGraph adds the line as a row to the graph
|
|
func (parser *Parser) AddLineToGraph(graph *Graph, row int, line []byte) error {
|
|
idx := bytes.Index(line, []byte("DATA:"))
|
|
if idx < 0 {
|
|
parser.ParseGlyphs(line)
|
|
} else {
|
|
parser.ParseGlyphs(line[:idx])
|
|
}
|
|
|
|
var err error
|
|
commitDone := false
|
|
|
|
for column, glyph := range parser.glyphs {
|
|
if glyph == ' ' {
|
|
continue
|
|
}
|
|
|
|
flowID := parser.flows[column]
|
|
|
|
graph.AddGlyph(row, column, flowID, parser.colors[column], glyph)
|
|
|
|
if glyph == '*' {
|
|
if commitDone {
|
|
if err != nil {
|
|
err = fmt.Errorf("double commit on line %d: %s. %w", row, string(line), err)
|
|
} else {
|
|
err = fmt.Errorf("double commit on line %d: %s", row, string(line))
|
|
}
|
|
}
|
|
commitDone = true
|
|
if idx < 0 {
|
|
if err != nil {
|
|
err = fmt.Errorf("missing data section on line %d with commit: %s. %w", row, string(line), err)
|
|
} else {
|
|
err = fmt.Errorf("missing data section on line %d with commit: %s", row, string(line))
|
|
}
|
|
continue
|
|
}
|
|
err2 := graph.AddCommit(row, column, flowID, line[idx+5:])
|
|
if err != nil && err2 != nil {
|
|
err = fmt.Errorf("%v %w", err2, err)
|
|
continue
|
|
} else if err2 != nil {
|
|
err = err2
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
if !commitDone {
|
|
graph.Commits = append(graph.Commits, RelationCommit)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (parser *Parser) releaseUnusedColors() {
|
|
if parser.firstInUse > -1 {
|
|
// Here we step through the old colors, searching for them in the
|
|
// "in-use" section of availableColors (that is, the colors between
|
|
// firstInUse and firstAvailable)
|
|
// Ensure that the benchmarks are not worsened with proposed changes
|
|
stepstaken := 0
|
|
position := parser.firstInUse
|
|
for _, color := range parser.oldColors {
|
|
if color == 0 {
|
|
continue
|
|
}
|
|
found := false
|
|
i := position
|
|
for j := stepstaken; i != parser.firstAvailable && j < len(parser.availableColors); j++ {
|
|
colorToCheck := parser.availableColors[i]
|
|
if colorToCheck == color {
|
|
found = true
|
|
break
|
|
}
|
|
i = (i + 1) % len(parser.availableColors)
|
|
}
|
|
if !found {
|
|
// Duplicate color
|
|
continue
|
|
}
|
|
// Swap them around
|
|
parser.availableColors[position], parser.availableColors[i] = parser.availableColors[i], parser.availableColors[position]
|
|
stepstaken++
|
|
position = (parser.firstInUse + stepstaken) % len(parser.availableColors)
|
|
if position == parser.firstAvailable || stepstaken == len(parser.availableColors) {
|
|
break
|
|
}
|
|
}
|
|
if stepstaken == len(parser.availableColors) {
|
|
parser.firstAvailable = -1
|
|
} else {
|
|
parser.firstAvailable = position
|
|
if parser.nextAvailable == -1 {
|
|
parser.nextAvailable = parser.firstAvailable
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ParseGlyphs parses the provided glyphs and sets the internal state
|
|
func (parser *Parser) ParseGlyphs(glyphs []byte) {
|
|
|
|
// Clean state for parsing this row
|
|
parser.glyphs, parser.oldGlyphs = parser.oldGlyphs, parser.glyphs
|
|
parser.glyphs = parser.glyphs[0:0]
|
|
parser.flows, parser.oldFlows = parser.oldFlows, parser.flows
|
|
parser.flows = parser.flows[0:0]
|
|
parser.colors, parser.oldColors = parser.oldColors, parser.colors
|
|
|
|
// Ensure we have enough flows and colors
|
|
parser.colors = parser.colors[0:0]
|
|
for range glyphs {
|
|
parser.flows = append(parser.flows, 0)
|
|
parser.colors = append(parser.colors, 0)
|
|
}
|
|
|
|
// Copy the provided glyphs in to state.glyphs for safekeeping
|
|
parser.glyphs = append(parser.glyphs, glyphs...)
|
|
|
|
// release unused colors
|
|
parser.releaseUnusedColors()
|
|
|
|
for i := len(glyphs) - 1; i >= 0; i-- {
|
|
glyph := glyphs[i]
|
|
switch glyph {
|
|
case '|':
|
|
fallthrough
|
|
case '*':
|
|
parser.setUpFlow(i)
|
|
case '/':
|
|
parser.setOutFlow(i)
|
|
case '\\':
|
|
parser.setInFlow(i)
|
|
case '_':
|
|
parser.setRightFlow(i)
|
|
case '.':
|
|
fallthrough
|
|
case '-':
|
|
parser.setLeftFlow(i)
|
|
case ' ':
|
|
// no-op
|
|
default:
|
|
parser.newFlow(i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (parser *Parser) takePreviousFlow(i, j int) {
|
|
if j < len(parser.oldFlows) && parser.oldFlows[j] > 0 {
|
|
parser.flows[i] = parser.oldFlows[j]
|
|
parser.oldFlows[j] = 0
|
|
parser.colors[i] = parser.oldColors[j]
|
|
parser.oldColors[j] = 0
|
|
} else {
|
|
parser.newFlow(i)
|
|
}
|
|
}
|
|
|
|
func (parser *Parser) takeCurrentFlow(i, j int) {
|
|
if j < len(parser.flows) && parser.flows[j] > 0 {
|
|
parser.flows[i] = parser.flows[j]
|
|
parser.colors[i] = parser.colors[j]
|
|
} else {
|
|
parser.newFlow(i)
|
|
}
|
|
}
|
|
|
|
func (parser *Parser) newFlow(i int) {
|
|
parser.maxFlow++
|
|
parser.flows[i] = parser.maxFlow
|
|
|
|
// Now give this flow a color
|
|
if parser.nextAvailable == -1 {
|
|
next := len(parser.availableColors)
|
|
if parser.maxAllowedColors < 1 || next < parser.maxAllowedColors {
|
|
parser.nextAvailable = next
|
|
parser.firstAvailable = next
|
|
parser.availableColors = append(parser.availableColors, next+1)
|
|
}
|
|
}
|
|
parser.colors[i] = parser.availableColors[parser.nextAvailable]
|
|
if parser.firstInUse == -1 {
|
|
parser.firstInUse = parser.nextAvailable
|
|
}
|
|
parser.availableColors[parser.firstAvailable], parser.availableColors[parser.nextAvailable] = parser.availableColors[parser.nextAvailable], parser.availableColors[parser.firstAvailable]
|
|
|
|
parser.nextAvailable = (parser.nextAvailable + 1) % len(parser.availableColors)
|
|
parser.firstAvailable = (parser.firstAvailable + 1) % len(parser.availableColors)
|
|
|
|
if parser.nextAvailable == parser.firstInUse {
|
|
parser.nextAvailable = parser.firstAvailable
|
|
}
|
|
if parser.nextAvailable == parser.firstInUse {
|
|
parser.nextAvailable = -1
|
|
parser.firstAvailable = -1
|
|
}
|
|
}
|
|
|
|
// setUpFlow handles '|' or '*'
|
|
func (parser *Parser) setUpFlow(i int) {
|
|
// In preference order:
|
|
//
|
|
// Previous Row: '\? ' ' |' ' /'
|
|
// Current Row: ' | ' ' |' ' | '
|
|
if i > 0 && i-1 < len(parser.oldGlyphs) && parser.oldGlyphs[i-1] == '\\' {
|
|
parser.takePreviousFlow(i, i-1)
|
|
} else if i < len(parser.oldGlyphs) && (parser.oldGlyphs[i] == '|' || parser.oldGlyphs[i] == '*') {
|
|
parser.takePreviousFlow(i, i)
|
|
} else if i+1 < len(parser.oldGlyphs) && parser.oldGlyphs[i+1] == '/' {
|
|
parser.takePreviousFlow(i, i+1)
|
|
} else {
|
|
parser.newFlow(i)
|
|
}
|
|
}
|
|
|
|
// setOutFlow handles '/'
|
|
func (parser *Parser) setOutFlow(i int) {
|
|
// In preference order:
|
|
//
|
|
// Previous Row: ' |/' ' |_' ' |' ' /' ' _' '\'
|
|
// Current Row: '/| ' '/| ' '/ ' '/ ' '/ ' '/'
|
|
if i+2 < len(parser.oldGlyphs) &&
|
|
(parser.oldGlyphs[i+1] == '|' || parser.oldGlyphs[i+1] == '*') &&
|
|
(parser.oldGlyphs[i+2] == '/' || parser.oldGlyphs[i+2] == '_') &&
|
|
i+1 < len(parser.glyphs) &&
|
|
(parser.glyphs[i+1] == '|' || parser.glyphs[i+1] == '*') {
|
|
parser.takePreviousFlow(i, i+2)
|
|
} else if i+1 < len(parser.oldGlyphs) &&
|
|
(parser.oldGlyphs[i+1] == '|' || parser.oldGlyphs[i+1] == '*' ||
|
|
parser.oldGlyphs[i+1] == '/' || parser.oldGlyphs[i+1] == '_') {
|
|
parser.takePreviousFlow(i, i+1)
|
|
if parser.oldGlyphs[i+1] == '/' {
|
|
parser.glyphs[i] = '|'
|
|
}
|
|
} else if i < len(parser.oldGlyphs) && parser.oldGlyphs[i] == '\\' {
|
|
parser.takePreviousFlow(i, i)
|
|
} else {
|
|
parser.newFlow(i)
|
|
}
|
|
}
|
|
|
|
// setInFlow handles '\'
|
|
func (parser *Parser) setInFlow(i int) {
|
|
// In preference order:
|
|
//
|
|
// Previous Row: '| ' '-. ' '| ' '\ ' '/' '---'
|
|
// Current Row: '|\' ' \' ' \' ' \' '\' ' \ '
|
|
if i > 0 && i-1 < len(parser.oldGlyphs) &&
|
|
(parser.oldGlyphs[i-1] == '|' || parser.oldGlyphs[i-1] == '*') &&
|
|
(parser.glyphs[i-1] == '|' || parser.glyphs[i-1] == '*') {
|
|
parser.newFlow(i)
|
|
} else if i > 0 && i-1 < len(parser.oldGlyphs) &&
|
|
(parser.oldGlyphs[i-1] == '|' || parser.oldGlyphs[i-1] == '*' ||
|
|
parser.oldGlyphs[i-1] == '.' || parser.oldGlyphs[i-1] == '\\') {
|
|
parser.takePreviousFlow(i, i-1)
|
|
if parser.oldGlyphs[i-1] == '\\' {
|
|
parser.glyphs[i] = '|'
|
|
}
|
|
} else if i < len(parser.oldGlyphs) && parser.oldGlyphs[i] == '/' {
|
|
parser.takePreviousFlow(i, i)
|
|
} else {
|
|
parser.newFlow(i)
|
|
}
|
|
}
|
|
|
|
// setRightFlow handles '_'
|
|
func (parser *Parser) setRightFlow(i int) {
|
|
// In preference order:
|
|
//
|
|
// Current Row: '__' '_/' '_|_' '_|/'
|
|
if i+1 < len(parser.glyphs) &&
|
|
(parser.glyphs[i+1] == '_' || parser.glyphs[i+1] == '/') {
|
|
parser.takeCurrentFlow(i, i+1)
|
|
} else if i+2 < len(parser.glyphs) &&
|
|
(parser.glyphs[i+1] == '|' || parser.glyphs[i+1] == '*') &&
|
|
(parser.glyphs[i+2] == '_' || parser.glyphs[i+2] == '/') {
|
|
parser.takeCurrentFlow(i, i+2)
|
|
} else {
|
|
parser.newFlow(i)
|
|
}
|
|
}
|
|
|
|
// setLeftFlow handles '----.'
|
|
func (parser *Parser) setLeftFlow(i int) {
|
|
if parser.glyphs[i] == '.' {
|
|
parser.newFlow(i)
|
|
} else if i+1 < len(parser.glyphs) &&
|
|
(parser.glyphs[i+1] == '-' || parser.glyphs[i+1] == '.') {
|
|
parser.takeCurrentFlow(i, i+1)
|
|
} else {
|
|
parser.newFlow(i)
|
|
}
|
|
}
|