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>tokarchuk/v1.17
parent
f1a42f5d5e
commit
2c1ae6c82d
@ -0,0 +1,186 @@ |
|||||||
|
// 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" |
||||||
|
) |
||||||
|
|
||||||
|
// NewGraph creates a basic graph
|
||||||
|
func NewGraph() *Graph { |
||||||
|
graph := &Graph{} |
||||||
|
graph.relationCommit = &Commit{ |
||||||
|
Row: -1, |
||||||
|
Column: -1, |
||||||
|
} |
||||||
|
graph.Flows = map[int64]*Flow{} |
||||||
|
return graph |
||||||
|
} |
||||||
|
|
||||||
|
// Graph represents a collection of flows
|
||||||
|
type Graph struct { |
||||||
|
Flows map[int64]*Flow |
||||||
|
Commits []*Commit |
||||||
|
MinRow int |
||||||
|
MinColumn int |
||||||
|
MaxRow int |
||||||
|
MaxColumn int |
||||||
|
relationCommit *Commit |
||||||
|
} |
||||||
|
|
||||||
|
// Width returns the width of the graph
|
||||||
|
func (graph *Graph) Width() int { |
||||||
|
return graph.MaxColumn - graph.MinColumn + 1 |
||||||
|
} |
||||||
|
|
||||||
|
// Height returns the height of the graph
|
||||||
|
func (graph *Graph) Height() int { |
||||||
|
return graph.MaxRow - graph.MinRow + 1 |
||||||
|
} |
||||||
|
|
||||||
|
// AddGlyph adds glyph to flows
|
||||||
|
func (graph *Graph) AddGlyph(row, column int, flowID int64, color int, glyph byte) { |
||||||
|
flow, ok := graph.Flows[flowID] |
||||||
|
if !ok { |
||||||
|
flow = NewFlow(flowID, color, row, column) |
||||||
|
graph.Flows[flowID] = flow |
||||||
|
} |
||||||
|
flow.AddGlyph(row, column, glyph) |
||||||
|
|
||||||
|
if row < graph.MinRow { |
||||||
|
graph.MinRow = row |
||||||
|
} |
||||||
|
if row > graph.MaxRow { |
||||||
|
graph.MaxRow = row |
||||||
|
} |
||||||
|
if column < graph.MinColumn { |
||||||
|
graph.MinColumn = column |
||||||
|
} |
||||||
|
if column > graph.MaxColumn { |
||||||
|
graph.MaxColumn = column |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// AddCommit adds a commit at row, column on flowID with the provided data
|
||||||
|
func (graph *Graph) AddCommit(row, column int, flowID int64, data []byte) error { |
||||||
|
commit, err := NewCommit(row, column, data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
commit.Flow = flowID |
||||||
|
graph.Commits = append(graph.Commits, commit) |
||||||
|
|
||||||
|
graph.Flows[flowID].Commits = append(graph.Flows[flowID].Commits, commit) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// NewFlow creates a new flow
|
||||||
|
func NewFlow(flowID int64, color, row, column int) *Flow { |
||||||
|
return &Flow{ |
||||||
|
ID: flowID, |
||||||
|
ColorNumber: color, |
||||||
|
MinRow: row, |
||||||
|
MinColumn: column, |
||||||
|
MaxRow: row, |
||||||
|
MaxColumn: column, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Flow represents a series of glyphs
|
||||||
|
type Flow struct { |
||||||
|
ID int64 |
||||||
|
ColorNumber int |
||||||
|
Glyphs []Glyph |
||||||
|
Commits []*Commit |
||||||
|
MinRow int |
||||||
|
MinColumn int |
||||||
|
MaxRow int |
||||||
|
MaxColumn int |
||||||
|
} |
||||||
|
|
||||||
|
// Color16 wraps the color numbers around mod 16
|
||||||
|
func (flow *Flow) Color16() int { |
||||||
|
return flow.ColorNumber % 16 |
||||||
|
} |
||||||
|
|
||||||
|
// AddGlyph adds glyph at row and column
|
||||||
|
func (flow *Flow) AddGlyph(row, column int, glyph byte) { |
||||||
|
if row < flow.MinRow { |
||||||
|
flow.MinRow = row |
||||||
|
} |
||||||
|
if row > flow.MaxRow { |
||||||
|
flow.MaxRow = row |
||||||
|
} |
||||||
|
if column < flow.MinColumn { |
||||||
|
flow.MinColumn = column |
||||||
|
} |
||||||
|
if column > flow.MaxColumn { |
||||||
|
flow.MaxColumn = column |
||||||
|
} |
||||||
|
|
||||||
|
flow.Glyphs = append(flow.Glyphs, Glyph{ |
||||||
|
row, |
||||||
|
column, |
||||||
|
glyph, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Glyph represents a co-ordinate and glyph
|
||||||
|
type Glyph struct { |
||||||
|
Row int |
||||||
|
Column int |
||||||
|
Glyph byte |
||||||
|
} |
||||||
|
|
||||||
|
// RelationCommit represents an empty relation commit
|
||||||
|
var RelationCommit = &Commit{ |
||||||
|
Row: -1, |
||||||
|
} |
||||||
|
|
||||||
|
// NewCommit creates a new commit from a provided line
|
||||||
|
func NewCommit(row, column int, line []byte) (*Commit, error) { |
||||||
|
data := bytes.SplitN(line, []byte("|"), 7) |
||||||
|
if len(data) < 7 { |
||||||
|
return nil, fmt.Errorf("malformed data section on line %d with commit: %s", row, string(line)) |
||||||
|
} |
||||||
|
return &Commit{ |
||||||
|
Row: row, |
||||||
|
Column: column, |
||||||
|
// 0 matches git log --pretty=format:%d => ref names, like the --decorate option of git-log(1)
|
||||||
|
Branch: string(data[0]), |
||||||
|
// 1 matches git log --pretty=format:%H => commit hash
|
||||||
|
Rev: string(data[1]), |
||||||
|
// 2 matches git log --pretty=format:%ad => author date (format respects --date= option)
|
||||||
|
Date: string(data[2]), |
||||||
|
// 3 matches git log --pretty=format:%an => author name
|
||||||
|
Author: string(data[3]), |
||||||
|
// 4 matches git log --pretty=format:%ae => author email
|
||||||
|
AuthorEmail: string(data[4]), |
||||||
|
// 5 matches git log --pretty=format:%h => abbreviated commit hash
|
||||||
|
ShortRev: string(data[5]), |
||||||
|
// 6 matches git log --pretty=format:%s => subject
|
||||||
|
Subject: string(data[6]), |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Commit represents a commit at co-ordinate X, Y with the data
|
||||||
|
type Commit struct { |
||||||
|
Flow int64 |
||||||
|
Row int |
||||||
|
Column int |
||||||
|
Branch string |
||||||
|
Rev string |
||||||
|
Date string |
||||||
|
Author string |
||||||
|
AuthorEmail string |
||||||
|
ShortRev string |
||||||
|
Subject string |
||||||
|
} |
||||||
|
|
||||||
|
// OnlyRelation returns whether this a relation only commit
|
||||||
|
func (c *Commit) OnlyRelation() bool { |
||||||
|
return c.Row == -1 |
||||||
|
} |
@ -0,0 +1,338 @@ |
|||||||
|
// 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) |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 319 B |
After Width: | Height: | Size: 540 B |
@ -1,568 +1,81 @@ |
|||||||
// Although inspired by the https://github.com/bluef/gitgraph.js/blob/master/gitgraph.js
|
export default async function initGitGraph() { |
||||||
// this has been completely rewritten with almost no remaining code
|
const graphContainer = document.getElementById('git-graph-container'); |
||||||
|
if (!graphContainer) return; |
||||||
// GitGraphCanvas is a canvas for drawing gitgraphs on to
|
|
||||||
class GitGraphCanvas { |
$('#flow-color-monochrome').on('click', () => { |
||||||
constructor(canvas, widthUnits, heightUnits, config) { |
$('#flow-color-monochrome').addClass('active'); |
||||||
this.ctx = canvas.getContext('2d'); |
$('#flow-color-colored').removeClass('active'); |
||||||
|
$('#git-graph-container').removeClass('colored').addClass('monochrome'); |
||||||
const width = widthUnits * config.unitSize; |
const params = new URLSearchParams(window.location.search); |
||||||
this.height = heightUnits * config.unitSize; |
params.set('mode', 'monochrome'); |
||||||
|
const queryString = params.toString(); |
||||||
const ratio = window.devicePixelRatio || 1; |
if (queryString) { |
||||||
|
window.history.replaceState({}, '', `?${queryString}`); |
||||||
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); |
|
||||||
} else { |
} else { |
||||||
// Again unexpected so let's generate and wait the bug-report
|
window.history.replaceState({}, '', window.location.pathname); |
||||||
flow = this.generateNewFlow(column); |
} |
||||||
} |
$('.pagination a').each((_, that) => { |
||||||
|
const href = $(that).attr('href'); |
||||||
// Assign all of the rest of the ----. to this flow.
|
if (!href) return; |
||||||
for (; originalColumn < column + 1; originalColumn++) { |
const url = new URL(href, window.location); |
||||||
this.currentFlows[originalColumn] = flow; |
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()); |
|
||||||
}); |
}); |
||||||
|
$('#git-graph-container').on('mouseenter', '#rev-list li', (e) => { |
||||||
// Define some drawing parameters
|
const flow = $(e.currentTarget).data('flow'); |
||||||
const config = { |
if (flow === 0) return; |
||||||
unitSize: 20, |
$(`#flow-${flow}`).addClass('highlight'); |
||||||
lineWidth: 3, |
$(e.currentTarget).addClass('hover'); |
||||||
nodeRadius: 4 |
$(`#rev-list li[data-flow='${flow}']`).addClass('highlight'); |
||||||
}; |
}); |
||||||
|
$('#git-graph-container').on('mouseleave', '#rev-list li', (e) => { |
||||||
|
const flow = $(e.currentTarget).data('flow'); |
||||||
const gitGraph = new GitGraph(graphCanvas, graphList, config); |
if (flow === 0) return; |
||||||
gitGraph.draw(); |
$(`#flow-${flow}`).removeClass('highlight'); |
||||||
graphCanvas.closest('#git-graph-container').classList.add('in'); |
$(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'); |
||||||
|
}); |
||||||
} |
} |
||||||
|
@ -0,0 +1,256 @@ |
|||||||
|
#git-graph-container { |
||||||
|
float: left; |
||||||
|
display: block; |
||||||
|
overflow-x: auto; |
||||||
|
width: 100%; |
||||||
|
|
||||||
|
.color-buttons { |
||||||
|
margin-right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.ui.header.dividing { |
||||||
|
padding-bottom: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
li { |
||||||
|
list-style-type: none; |
||||||
|
height: 20px; |
||||||
|
line-height: 20px; |
||||||
|
white-space: nowrap; |
||||||
|
|
||||||
|
.node-relation { |
||||||
|
font-family: "Bitstream Vera Sans Mono", "Courier", monospace; |
||||||
|
} |
||||||
|
|
||||||
|
.author { |
||||||
|
color: #666666; |
||||||
|
} |
||||||
|
|
||||||
|
.time { |
||||||
|
color: #999999; |
||||||
|
font-size: 80%; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: #000000; |
||||||
|
} |
||||||
|
|
||||||
|
a:hover { |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
|
||||||
|
a em { |
||||||
|
color: #bb0000; |
||||||
|
border-bottom: 1px dotted #bbbbbb; |
||||||
|
text-decoration: none; |
||||||
|
font-style: normal; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#rel-container { |
||||||
|
max-width: 30%; |
||||||
|
overflow-x: auto; |
||||||
|
float: left; |
||||||
|
} |
||||||
|
|
||||||
|
#rev-container { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
#rev-list { |
||||||
|
margin: 0; |
||||||
|
padding: 0 5px; |
||||||
|
min-width: 95%; |
||||||
|
|
||||||
|
li.highlight, |
||||||
|
li.hover { |
||||||
|
background-color: rgba(0, 0, 0, .05); |
||||||
|
} |
||||||
|
|
||||||
|
li.highlight.hover { |
||||||
|
background-color: rgba(0, 0, 0, .1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#graph-raw-list { |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&.monochrome #rel-container { |
||||||
|
.flow-group { |
||||||
|
stroke: grey; |
||||||
|
fill: grey; |
||||||
|
} |
||||||
|
|
||||||
|
.flow-group.highlight { |
||||||
|
stroke: black; |
||||||
|
fill: black; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&:not(.monochrome) #rel-container { |
||||||
|
.flow-group { |
||||||
|
&.flow-color-16-1 { |
||||||
|
stroke: #499a37; |
||||||
|
fill: #499a37; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-2 { |
||||||
|
stroke: hsl(356, 58%, 54%); |
||||||
|
fill: #ce4751; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-3 { |
||||||
|
stroke: #8f9121; |
||||||
|
fill: #8f9121; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-4 { |
||||||
|
stroke: #ac32a6; |
||||||
|
fill: #ac32a6; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-5 { |
||||||
|
stroke: #3d27aa; |
||||||
|
fill: #3d27aa; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-6 { |
||||||
|
stroke: #c67d28; |
||||||
|
fill: #c67d28; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-7 { |
||||||
|
stroke: #4db392; |
||||||
|
fill: #4db392; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-8 { |
||||||
|
stroke: #aa4d30; |
||||||
|
fill: #aa4d30; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-9 { |
||||||
|
stroke: #2a6f84; |
||||||
|
fill: #2a6f84; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-10 { |
||||||
|
stroke: #c45327; |
||||||
|
fill: #c45327; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-11 { |
||||||
|
stroke: #3d965c; |
||||||
|
fill: #3d965c; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-12 { |
||||||
|
stroke: #792a93; |
||||||
|
fill: #792a93; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-13 { |
||||||
|
stroke: #439d73; |
||||||
|
fill: #439d73; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-14 { |
||||||
|
stroke: #103aad; |
||||||
|
fill: #103aad; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-15 { |
||||||
|
stroke: #982e85; |
||||||
|
fill: #982e85; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-0 { |
||||||
|
stroke: #7db233; |
||||||
|
fill: #7db233; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.flow-group.highlight { |
||||||
|
&.flow-color-16-1 { |
||||||
|
stroke: #5ac144; |
||||||
|
fill: #5ac144; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-2 { |
||||||
|
stroke: #ed5a8b; |
||||||
|
fill: #ed5a8b; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-3 { |
||||||
|
stroke: #ced049; |
||||||
|
fill: #ced048; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-4 { |
||||||
|
stroke: #db61d7; |
||||||
|
fill: #db62d6; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-5 { |
||||||
|
stroke: #4e33d1; |
||||||
|
fill: #4f35d1; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-6 { |
||||||
|
stroke: #e6a151; |
||||||
|
fill: #e6a151; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-7 { |
||||||
|
stroke: #44daaa; |
||||||
|
fill: #44daaa; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-8 { |
||||||
|
stroke: #dd7a5c; |
||||||
|
fill: #dd7a5c; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-9 { |
||||||
|
stroke: #38859c; |
||||||
|
fill: #38859c; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-10 { |
||||||
|
stroke: #d95520; |
||||||
|
fill: #d95520; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-11 { |
||||||
|
stroke: #42ae68; |
||||||
|
fill: #42ae68; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-12 { |
||||||
|
stroke: #9126b5; |
||||||
|
fill: #9126b5; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-13 { |
||||||
|
stroke: #4ab080; |
||||||
|
fill: #4ab080; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-14 { |
||||||
|
stroke: #284fb8; |
||||||
|
fill: #284fb8; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-15 { |
||||||
|
stroke: #971c80; |
||||||
|
fill: #971c80; |
||||||
|
} |
||||||
|
|
||||||
|
&.flow-color-16-0 { |
||||||
|
stroke: #87ca28; |
||||||
|
fill: #87ca28; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,15 +0,0 @@ |
|||||||
/* This is a customized version of https://github.com/bluef/gitgraph.js/blob/master/gitgraph.css |
|
||||||
Changes include the removal of `body` and `em` styles */ |
|
||||||
#git-graph-container, #rel-container {float:left;} |
|
||||||
#rel-container {max-width:30%; overflow-x:auto;} |
|
||||||
#git-graph-container {overflow-x:auto; width:100%} |
|
||||||
#git-graph-container li {list-style-type:none;height:20px;line-height:20px; white-space:nowrap;} |
|
||||||
#git-graph-container li .node-relation {font-family:'Bitstream Vera Sans Mono', 'Courier', monospace;} |
|
||||||
#git-graph-container li .author {color:#666666;} |
|
||||||
#git-graph-container li .time {color:#999999;font-size:80%} |
|
||||||
#git-graph-container li a {color:#000000;} |
|
||||||
#git-graph-container li a:hover {text-decoration:underline;} |
|
||||||
#git-graph-container li a em {color:#BB0000;border-bottom:1px dotted #BBBBBB;text-decoration:none;font-style:normal;} |
|
||||||
#rev-container {width:100%} |
|
||||||
#rev-list {margin:0;padding:0 5px 0 5px;min-width:95%} |
|
||||||
#graph-raw-list {margin:0px;} |
|
After Width: | Height: | Size: 242 B |
After Width: | Height: | Size: 469 B |
Loading…
Reference in new issue