mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	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>
This commit is contained in:
		@@ -1,8 +1,5 @@
 | 
				
			|||||||
extends: stylelint-config-standard
 | 
					extends: stylelint-config-standard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ignoreFiles:
 | 
					 | 
				
			||||||
  - web_src/less/vendor/**/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
rules:
 | 
					rules:
 | 
				
			||||||
  at-rule-empty-line-before: null
 | 
					  at-rule-empty-line-before: null
 | 
				
			||||||
  block-closing-brace-empty-line-before: null
 | 
					  block-closing-brace-empty-line-before: null
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,26 +16,9 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GraphItem represent one commit, or one relation in timeline
 | 
					 | 
				
			||||||
type GraphItem struct {
 | 
					 | 
				
			||||||
	GraphAcii    string
 | 
					 | 
				
			||||||
	Relation     string
 | 
					 | 
				
			||||||
	Branch       string
 | 
					 | 
				
			||||||
	Rev          string
 | 
					 | 
				
			||||||
	Date         string
 | 
					 | 
				
			||||||
	Author       string
 | 
					 | 
				
			||||||
	AuthorEmail  string
 | 
					 | 
				
			||||||
	ShortRev     string
 | 
					 | 
				
			||||||
	Subject      string
 | 
					 | 
				
			||||||
	OnlyRelation bool
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GraphItems is a list of commits from all branches
 | 
					 | 
				
			||||||
type GraphItems []GraphItem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetCommitGraph return a list of commit (GraphItems) from all branches
 | 
					// GetCommitGraph return a list of commit (GraphItems) from all branches
 | 
				
			||||||
func GetCommitGraph(r *git.Repository, page int) (GraphItems, error) {
 | 
					func GetCommitGraph(r *git.Repository, page int, maxAllowedColors int) (*Graph, error) {
 | 
				
			||||||
	format := "DATA:|%d|%H|%ad|%an|%ae|%h|%s"
 | 
						format := "DATA:%d|%H|%ad|%an|%ae|%h|%s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if page == 0 {
 | 
						if page == 0 {
 | 
				
			||||||
		page = 1
 | 
							page = 1
 | 
				
			||||||
@@ -51,7 +34,8 @@ func GetCommitGraph(r *git.Repository, page int) (GraphItems, error) {
 | 
				
			|||||||
		"--date=iso",
 | 
							"--date=iso",
 | 
				
			||||||
		fmt.Sprintf("--pretty=format:%s", format),
 | 
							fmt.Sprintf("--pretty=format:%s", format),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	commitGraph := make([]GraphItem, 0, 100)
 | 
						graph := NewGraph()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	stderr := new(strings.Builder)
 | 
						stderr := new(strings.Builder)
 | 
				
			||||||
	stdoutReader, stdoutWriter, err := os.Pipe()
 | 
						stdoutReader, stdoutWriter, err := os.Pipe()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -64,86 +48,56 @@ func GetCommitGraph(r *git.Repository, page int) (GraphItems, error) {
 | 
				
			|||||||
	if err := graphCmd.RunInDirTimeoutEnvFullPipelineFunc(nil, -1, r.Path, stdoutWriter, stderr, nil, func(ctx context.Context, cancel context.CancelFunc) error {
 | 
						if err := graphCmd.RunInDirTimeoutEnvFullPipelineFunc(nil, -1, r.Path, stdoutWriter, stderr, nil, func(ctx context.Context, cancel context.CancelFunc) error {
 | 
				
			||||||
		_ = stdoutWriter.Close()
 | 
							_ = stdoutWriter.Close()
 | 
				
			||||||
		defer stdoutReader.Close()
 | 
							defer stdoutReader.Close()
 | 
				
			||||||
 | 
							parser := &Parser{}
 | 
				
			||||||
 | 
							parser.firstInUse = -1
 | 
				
			||||||
 | 
							parser.maxAllowedColors = maxAllowedColors
 | 
				
			||||||
 | 
							if maxAllowedColors > 0 {
 | 
				
			||||||
 | 
								parser.availableColors = make([]int, maxAllowedColors)
 | 
				
			||||||
 | 
								for i := range parser.availableColors {
 | 
				
			||||||
 | 
									parser.availableColors[i] = i + 1
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								parser.availableColors = []int{1, 2}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		for commitsToSkip > 0 && scanner.Scan() {
 | 
							for commitsToSkip > 0 && scanner.Scan() {
 | 
				
			||||||
			line := scanner.Bytes()
 | 
								line := scanner.Bytes()
 | 
				
			||||||
			dataIdx := bytes.Index(line, []byte("DATA:"))
 | 
								dataIdx := bytes.Index(line, []byte("DATA:"))
 | 
				
			||||||
 | 
								if dataIdx < 0 {
 | 
				
			||||||
 | 
									dataIdx = len(line)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			starIdx := bytes.IndexByte(line, '*')
 | 
								starIdx := bytes.IndexByte(line, '*')
 | 
				
			||||||
			if starIdx >= 0 && starIdx < dataIdx {
 | 
								if starIdx >= 0 && starIdx < dataIdx {
 | 
				
			||||||
				commitsToSkip--
 | 
									commitsToSkip--
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								parser.ParseGlyphs(line[:dataIdx])
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							row := 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Skip initial non-commit lines
 | 
							// Skip initial non-commit lines
 | 
				
			||||||
		for scanner.Scan() {
 | 
							for scanner.Scan() {
 | 
				
			||||||
			if bytes.IndexByte(scanner.Bytes(), '*') >= 0 {
 | 
								line := scanner.Bytes()
 | 
				
			||||||
				line := scanner.Text()
 | 
								if bytes.IndexByte(line, '*') >= 0 {
 | 
				
			||||||
				graphItem, err := graphItemFromString(line, r)
 | 
									if err := parser.AddLineToGraph(graph, row, line); err != nil {
 | 
				
			||||||
				if err != nil {
 | 
					 | 
				
			||||||
					cancel()
 | 
										cancel()
 | 
				
			||||||
					return err
 | 
										return err
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				commitGraph = append(commitGraph, graphItem)
 | 
					 | 
				
			||||||
				break
 | 
									break
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								parser.ParseGlyphs(line)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for scanner.Scan() {
 | 
							for scanner.Scan() {
 | 
				
			||||||
			line := scanner.Text()
 | 
								row++
 | 
				
			||||||
			graphItem, err := graphItemFromString(line, r)
 | 
								line := scanner.Bytes()
 | 
				
			||||||
			if err != nil {
 | 
								if err := parser.AddLineToGraph(graph, row, line); err != nil {
 | 
				
			||||||
				cancel()
 | 
									cancel()
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			commitGraph = append(commitGraph, graphItem)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return scanner.Err()
 | 
							return scanner.Err()
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
		return commitGraph, err
 | 
							return graph, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						return graph, nil
 | 
				
			||||||
	return commitGraph, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func graphItemFromString(s string, r *git.Repository) (GraphItem, error) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var ascii string
 | 
					 | 
				
			||||||
	var data = "|||||||"
 | 
					 | 
				
			||||||
	lines := strings.SplitN(s, "DATA:", 2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	switch len(lines) {
 | 
					 | 
				
			||||||
	case 1:
 | 
					 | 
				
			||||||
		ascii = lines[0]
 | 
					 | 
				
			||||||
	case 2:
 | 
					 | 
				
			||||||
		ascii = lines[0]
 | 
					 | 
				
			||||||
		data = lines[1]
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s. Expect 1 or two fields", s)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	rows := strings.SplitN(data, "|", 8)
 | 
					 | 
				
			||||||
	if len(rows) < 8 {
 | 
					 | 
				
			||||||
		return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s - Should containt 8 datafields", s)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/* // see format in getCommitGraph()
 | 
					 | 
				
			||||||
	   0	Relation string
 | 
					 | 
				
			||||||
	   1	Branch string
 | 
					 | 
				
			||||||
	   2	Rev string
 | 
					 | 
				
			||||||
	   3	Date string
 | 
					 | 
				
			||||||
	   4	Author string
 | 
					 | 
				
			||||||
	   5	AuthorEmail string
 | 
					 | 
				
			||||||
	   6	ShortRev string
 | 
					 | 
				
			||||||
	   7	Subject string
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
	gi := GraphItem{ascii,
 | 
					 | 
				
			||||||
		rows[0],
 | 
					 | 
				
			||||||
		rows[1],
 | 
					 | 
				
			||||||
		rows[2],
 | 
					 | 
				
			||||||
		rows[3],
 | 
					 | 
				
			||||||
		rows[4],
 | 
					 | 
				
			||||||
		rows[5],
 | 
					 | 
				
			||||||
		rows[6],
 | 
					 | 
				
			||||||
		rows[7],
 | 
					 | 
				
			||||||
		len(rows[2]) == 0, // no commits referred to, only relation in current line.
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return gi, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										186
									
								
								modules/gitgraph/graph_models.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								modules/gitgraph/graph_models.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,7 +5,9 @@
 | 
				
			|||||||
package gitgraph
 | 
					package gitgraph
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/modules/git"
 | 
						"code.gitea.io/gitea/modules/git"
 | 
				
			||||||
@@ -14,40 +16,235 @@ import (
 | 
				
			|||||||
func BenchmarkGetCommitGraph(b *testing.B) {
 | 
					func BenchmarkGetCommitGraph(b *testing.B) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	currentRepo, err := git.OpenRepository(".")
 | 
						currentRepo, err := git.OpenRepository(".")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil || currentRepo == nil {
 | 
				
			||||||
		b.Error("Could not open repository")
 | 
							b.Error("Could not open repository")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer currentRepo.Close()
 | 
						defer currentRepo.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for i := 0; i < b.N; i++ {
 | 
						for i := 0; i < b.N; i++ {
 | 
				
			||||||
		graph, err := GetCommitGraph(currentRepo, 1)
 | 
							graph, err := GetCommitGraph(currentRepo, 1, 0)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			b.Error("Could get commit graph")
 | 
								b.Error("Could get commit graph")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if len(graph) < 100 {
 | 
							if len(graph.Commits) < 100 {
 | 
				
			||||||
			b.Error("Should get 100 log lines.")
 | 
								b.Error("Should get 100 log lines.")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func BenchmarkParseCommitString(b *testing.B) {
 | 
					func BenchmarkParseCommitString(b *testing.B) {
 | 
				
			||||||
	testString := "* DATA:||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Kjell Kvinge|kjell@kvinge.biz|4e61bac|Add route for graph"
 | 
						testString := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Kjell Kvinge|kjell@kvinge.biz|4e61bac|Add route for graph"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						parser := &Parser{}
 | 
				
			||||||
 | 
						parser.Reset()
 | 
				
			||||||
	for i := 0; i < b.N; i++ {
 | 
						for i := 0; i < b.N; i++ {
 | 
				
			||||||
		graphItem, err := graphItemFromString(testString, nil)
 | 
							parser.Reset()
 | 
				
			||||||
		if err != nil {
 | 
							graph := NewGraph()
 | 
				
			||||||
 | 
							if err := parser.AddLineToGraph(graph, 0, []byte(testString)); err != nil {
 | 
				
			||||||
			b.Error("could not parse teststring")
 | 
								b.Error("could not parse teststring")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if graph.Flows[1].Commits[0].Author != "Kjell Kvinge" {
 | 
				
			||||||
		if graphItem.Author != "Kjell Kvinge" {
 | 
					 | 
				
			||||||
			b.Error("Did not get expected data")
 | 
								b.Error("Did not get expected data")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BenchmarkParseGlyphs(b *testing.B) {
 | 
				
			||||||
 | 
						parser := &Parser{}
 | 
				
			||||||
 | 
						parser.Reset()
 | 
				
			||||||
 | 
						tgBytes := []byte(testglyphs)
 | 
				
			||||||
 | 
						tg := tgBytes
 | 
				
			||||||
 | 
						idx := bytes.Index(tg, []byte("\n"))
 | 
				
			||||||
 | 
						for i := 0; i < b.N; i++ {
 | 
				
			||||||
 | 
							parser.Reset()
 | 
				
			||||||
 | 
							tg = tgBytes
 | 
				
			||||||
 | 
							idx = bytes.Index(tg, []byte("\n"))
 | 
				
			||||||
 | 
							for idx > 0 {
 | 
				
			||||||
 | 
								parser.ParseGlyphs(tg[:idx])
 | 
				
			||||||
 | 
								tg = tg[idx+1:]
 | 
				
			||||||
 | 
								idx = bytes.Index(tg, []byte("\n"))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestReleaseUnusedColors(t *testing.T) {
 | 
				
			||||||
 | 
						testcases := []struct {
 | 
				
			||||||
 | 
							availableColors []int
 | 
				
			||||||
 | 
							oldColors       []int
 | 
				
			||||||
 | 
							firstInUse      int // these values have to be either be correct or suggest less is
 | 
				
			||||||
 | 
							firstAvailable  int // available than possibly is - i.e. you cannot say 10 is available when it
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								availableColors: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
 | 
				
			||||||
 | 
								oldColors:       []int{1, 1, 1, 1, 1},
 | 
				
			||||||
 | 
								firstAvailable:  -1,
 | 
				
			||||||
 | 
								firstInUse:      1,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								availableColors: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
 | 
				
			||||||
 | 
								oldColors:       []int{1, 2, 3, 4},
 | 
				
			||||||
 | 
								firstAvailable:  6,
 | 
				
			||||||
 | 
								firstInUse:      0,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								availableColors: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
 | 
				
			||||||
 | 
								oldColors:       []int{6, 0, 3, 5, 3, 4, 0, 0},
 | 
				
			||||||
 | 
								firstAvailable:  6,
 | 
				
			||||||
 | 
								firstInUse:      0,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								availableColors: []int{1, 2, 3, 4, 5, 6, 7},
 | 
				
			||||||
 | 
								oldColors:       []int{6, 1, 3, 5, 3, 4, 2, 7},
 | 
				
			||||||
 | 
								firstAvailable:  -1,
 | 
				
			||||||
 | 
								firstInUse:      0,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								availableColors: []int{1, 2, 3, 4, 5, 6, 7},
 | 
				
			||||||
 | 
								oldColors:       []int{6, 0, 3, 5, 3, 4, 2, 7},
 | 
				
			||||||
 | 
								firstAvailable:  -1,
 | 
				
			||||||
 | 
								firstInUse:      0,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, testcase := range testcases {
 | 
				
			||||||
 | 
							parser := &Parser{}
 | 
				
			||||||
 | 
							parser.Reset()
 | 
				
			||||||
 | 
							parser.availableColors = append([]int{}, testcase.availableColors...)
 | 
				
			||||||
 | 
							parser.oldColors = append(parser.oldColors, testcase.oldColors...)
 | 
				
			||||||
 | 
							parser.firstAvailable = testcase.firstAvailable
 | 
				
			||||||
 | 
							parser.firstInUse = testcase.firstInUse
 | 
				
			||||||
 | 
							parser.releaseUnusedColors()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if parser.firstAvailable == -1 {
 | 
				
			||||||
 | 
								// All in use
 | 
				
			||||||
 | 
								for _, color := range parser.availableColors {
 | 
				
			||||||
 | 
									found := false
 | 
				
			||||||
 | 
									for _, oldColor := range parser.oldColors {
 | 
				
			||||||
 | 
										if oldColor == color {
 | 
				
			||||||
 | 
											found = true
 | 
				
			||||||
 | 
											break
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if !found {
 | 
				
			||||||
 | 
										t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should be available but is not",
 | 
				
			||||||
 | 
											testcase.availableColors,
 | 
				
			||||||
 | 
											testcase.oldColors,
 | 
				
			||||||
 | 
											testcase.firstAvailable,
 | 
				
			||||||
 | 
											testcase.firstInUse,
 | 
				
			||||||
 | 
											parser.availableColors,
 | 
				
			||||||
 | 
											parser.oldColors,
 | 
				
			||||||
 | 
											parser.firstAvailable,
 | 
				
			||||||
 | 
											parser.firstInUse,
 | 
				
			||||||
 | 
											color)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else if parser.firstInUse != -1 {
 | 
				
			||||||
 | 
								// Some in use
 | 
				
			||||||
 | 
								for i := parser.firstInUse; i != parser.firstAvailable; i = (i + 1) % len(parser.availableColors) {
 | 
				
			||||||
 | 
									color := parser.availableColors[i]
 | 
				
			||||||
 | 
									found := false
 | 
				
			||||||
 | 
									for _, oldColor := range parser.oldColors {
 | 
				
			||||||
 | 
										if oldColor == color {
 | 
				
			||||||
 | 
											found = true
 | 
				
			||||||
 | 
											break
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if !found {
 | 
				
			||||||
 | 
										t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should be available but is not",
 | 
				
			||||||
 | 
											testcase.availableColors,
 | 
				
			||||||
 | 
											testcase.oldColors,
 | 
				
			||||||
 | 
											testcase.firstAvailable,
 | 
				
			||||||
 | 
											testcase.firstInUse,
 | 
				
			||||||
 | 
											parser.availableColors,
 | 
				
			||||||
 | 
											parser.oldColors,
 | 
				
			||||||
 | 
											parser.firstAvailable,
 | 
				
			||||||
 | 
											parser.firstInUse,
 | 
				
			||||||
 | 
											color)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for i := parser.firstAvailable; i != parser.firstInUse; i = (i + 1) % len(parser.availableColors) {
 | 
				
			||||||
 | 
									color := parser.availableColors[i]
 | 
				
			||||||
 | 
									found := false
 | 
				
			||||||
 | 
									for _, oldColor := range parser.oldColors {
 | 
				
			||||||
 | 
										if oldColor == color {
 | 
				
			||||||
 | 
											found = true
 | 
				
			||||||
 | 
											break
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if found {
 | 
				
			||||||
 | 
										t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should not be available but is",
 | 
				
			||||||
 | 
											testcase.availableColors,
 | 
				
			||||||
 | 
											testcase.oldColors,
 | 
				
			||||||
 | 
											testcase.firstAvailable,
 | 
				
			||||||
 | 
											testcase.firstInUse,
 | 
				
			||||||
 | 
											parser.availableColors,
 | 
				
			||||||
 | 
											parser.oldColors,
 | 
				
			||||||
 | 
											parser.firstAvailable,
 | 
				
			||||||
 | 
											parser.firstInUse,
 | 
				
			||||||
 | 
											color)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// None in use
 | 
				
			||||||
 | 
								for _, color := range parser.oldColors {
 | 
				
			||||||
 | 
									if color != 0 {
 | 
				
			||||||
 | 
										t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should not be available but is",
 | 
				
			||||||
 | 
											testcase.availableColors,
 | 
				
			||||||
 | 
											testcase.oldColors,
 | 
				
			||||||
 | 
											testcase.firstAvailable,
 | 
				
			||||||
 | 
											testcase.firstInUse,
 | 
				
			||||||
 | 
											parser.availableColors,
 | 
				
			||||||
 | 
											parser.oldColors,
 | 
				
			||||||
 | 
											parser.firstAvailable,
 | 
				
			||||||
 | 
											parser.firstInUse,
 | 
				
			||||||
 | 
											color)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestParseGlyphs(t *testing.T) {
 | 
				
			||||||
 | 
						parser := &Parser{}
 | 
				
			||||||
 | 
						parser.Reset()
 | 
				
			||||||
 | 
						tgBytes := []byte(testglyphs)
 | 
				
			||||||
 | 
						tg := tgBytes
 | 
				
			||||||
 | 
						idx := bytes.Index(tg, []byte("\n"))
 | 
				
			||||||
 | 
						row := 0
 | 
				
			||||||
 | 
						for idx > 0 {
 | 
				
			||||||
 | 
							parser.ParseGlyphs(tg[:idx])
 | 
				
			||||||
 | 
							tg = tg[idx+1:]
 | 
				
			||||||
 | 
							idx = bytes.Index(tg, []byte("\n"))
 | 
				
			||||||
 | 
							if parser.flows[0] != 1 {
 | 
				
			||||||
 | 
								t.Errorf("First column flow should be 1 but was %d", parser.flows[0])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							colorToFlow := map[int]int64{}
 | 
				
			||||||
 | 
							flowToColor := map[int64]int{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for i, flow := range parser.flows {
 | 
				
			||||||
 | 
								if flow == 0 {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								color := parser.colors[i]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if fColor, in := flowToColor[flow]; in && fColor != color {
 | 
				
			||||||
 | 
									t.Errorf("Row %d column %d flow %d has color %d but should be %d", row, i, flow, color, fColor)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								flowToColor[flow] = color
 | 
				
			||||||
 | 
								if cFlow, in := colorToFlow[color]; in && cFlow != flow {
 | 
				
			||||||
 | 
									t.Errorf("Row %d column %d flow %d has color %d but conflicts with flow %d", row, i, flow, color, cFlow)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								colorToFlow[color] = flow
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							row++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(parser.availableColors) != 9 {
 | 
				
			||||||
 | 
							t.Errorf("Expected 9 colors but have %d", len(parser.availableColors))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestCommitStringParsing(t *testing.T) {
 | 
					func TestCommitStringParsing(t *testing.T) {
 | 
				
			||||||
	dataFirstPart := "* DATA:||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Author|user@mail.something|4e61bac|"
 | 
						dataFirstPart := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Author|user@mail.something|4e61bac|"
 | 
				
			||||||
	tests := []struct {
 | 
						tests := []struct {
 | 
				
			||||||
		shouldPass    bool
 | 
							shouldPass    bool
 | 
				
			||||||
		testName      string
 | 
							testName      string
 | 
				
			||||||
@@ -62,15 +259,460 @@ func TestCommitStringParsing(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		t.Run(test.testName, func(t *testing.T) {
 | 
							t.Run(test.testName, func(t *testing.T) {
 | 
				
			||||||
			testString := fmt.Sprintf("%s%s", dataFirstPart, test.commitMessage)
 | 
								testString := fmt.Sprintf("%s%s", dataFirstPart, test.commitMessage)
 | 
				
			||||||
			graphItem, err := graphItemFromString(testString, nil)
 | 
								idx := strings.Index(testString, "DATA:")
 | 
				
			||||||
 | 
								commit, err := NewCommit(0, 0, []byte(testString[idx+5:]))
 | 
				
			||||||
			if err != nil && test.shouldPass {
 | 
								if err != nil && test.shouldPass {
 | 
				
			||||||
				t.Errorf("Could not parse %s", testString)
 | 
									t.Errorf("Could not parse %s", testString)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if test.commitMessage != graphItem.Subject {
 | 
								if test.commitMessage != commit.Subject {
 | 
				
			||||||
				t.Errorf("%s does not match %s", test.commitMessage, graphItem.Subject)
 | 
									t.Errorf("%s does not match %s", test.commitMessage, commit.Subject)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var testglyphs = `* 
 | 
				
			||||||
 | 
					* 
 | 
				
			||||||
 | 
					* 
 | 
				
			||||||
 | 
					* 
 | 
				
			||||||
 | 
					* 
 | 
				
			||||||
 | 
					* 
 | 
				
			||||||
 | 
					* 
 | 
				
			||||||
 | 
					*   
 | 
				
			||||||
 | 
					|\  
 | 
				
			||||||
 | 
					* | 
 | 
				
			||||||
 | 
					* | 
 | 
				
			||||||
 | 
					* | 
 | 
				
			||||||
 | 
					* | 
 | 
				
			||||||
 | 
					* | 
 | 
				
			||||||
 | 
					| * 
 | 
				
			||||||
 | 
					* | 
 | 
				
			||||||
 | 
					| *   
 | 
				
			||||||
 | 
					| |\  
 | 
				
			||||||
 | 
					* | | 
 | 
				
			||||||
 | 
					| | *   
 | 
				
			||||||
 | 
					| | |\  
 | 
				
			||||||
 | 
					* | | \   
 | 
				
			||||||
 | 
					|\ \ \ \  
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| |\| | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					|/ / / /  
 | 
				
			||||||
 | 
					| | | * 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | |   
 | 
				
			||||||
 | 
					|\ \ \ \  
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| | |\| | 
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					| | | | * 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | |   
 | 
				
			||||||
 | 
					|\ \ \ \ \  
 | 
				
			||||||
 | 
					| * | | | | 
 | 
				
			||||||
 | 
					|/| | | | | 
 | 
				
			||||||
 | 
					| | |/ / /  
 | 
				
			||||||
 | 
					| |/| | |   
 | 
				
			||||||
 | 
					| | | | * 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					|/| | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					|/| | | | 
 | 
				
			||||||
 | 
					| | |/ /  
 | 
				
			||||||
 | 
					| |/| |   
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | |   
 | 
				
			||||||
 | 
					| |\ \ \  
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| |/| | | 
 | 
				
			||||||
 | 
					| | | |/  
 | 
				
			||||||
 | 
					| | |/|   
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| | * |   
 | 
				
			||||||
 | 
					| | |\ \  
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					| | |/| | 
 | 
				
			||||||
 | 
					| | | * |   
 | 
				
			||||||
 | 
					| | | |\ \  
 | 
				
			||||||
 | 
					| | | | * | 
 | 
				
			||||||
 | 
					| | | |/| | 
 | 
				
			||||||
 | 
					| | * | | | 
 | 
				
			||||||
 | 
					| | * | | |   
 | 
				
			||||||
 | 
					| | |\ \ \ \  
 | 
				
			||||||
 | 
					| | | * | | | 
 | 
				
			||||||
 | 
					| | |/| | | | 
 | 
				
			||||||
 | 
					| | | | | * | 
 | 
				
			||||||
 | 
					| | | | |/ /  
 | 
				
			||||||
 | 
					* | | | / / 
 | 
				
			||||||
 | 
					|/ / / / /  
 | 
				
			||||||
 | 
					* | | | |   
 | 
				
			||||||
 | 
					|\ \ \ \ \  
 | 
				
			||||||
 | 
					| * | | | | 
 | 
				
			||||||
 | 
					|/| | | | | 
 | 
				
			||||||
 | 
					| * | | | | 
 | 
				
			||||||
 | 
					| * | | | |   
 | 
				
			||||||
 | 
					| |\ \ \ \ \  
 | 
				
			||||||
 | 
					| | | * \ \ \   
 | 
				
			||||||
 | 
					| | | |\ \ \ \  
 | 
				
			||||||
 | 
					| | | | * | | | 
 | 
				
			||||||
 | 
					| | | |/| | | | 
 | 
				
			||||||
 | 
					| | | | | |/ /  
 | 
				
			||||||
 | 
					| | | | |/| |   
 | 
				
			||||||
 | 
					* | | | | | | 
 | 
				
			||||||
 | 
					* | | | | | | 
 | 
				
			||||||
 | 
					* | | | | | | 
 | 
				
			||||||
 | 
					| | | | * | | 
 | 
				
			||||||
 | 
					* | | | | | | 
 | 
				
			||||||
 | 
					| | * | | | | 
 | 
				
			||||||
 | 
					| |/| | | | | 
 | 
				
			||||||
 | 
					* | | | | | | 
 | 
				
			||||||
 | 
					| |/ / / / /  
 | 
				
			||||||
 | 
					|/| | | | |   
 | 
				
			||||||
 | 
					| | | | * | 
 | 
				
			||||||
 | 
					| | | |/ /  
 | 
				
			||||||
 | 
					| | |/| |   
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| | | | * 
 | 
				
			||||||
 | 
					| | * | |   
 | 
				
			||||||
 | 
					| | |\ \ \  
 | 
				
			||||||
 | 
					| | | * | | 
 | 
				
			||||||
 | 
					| | |/| | | 
 | 
				
			||||||
 | 
					| | | |/ /  
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					| | * | |   
 | 
				
			||||||
 | 
					| | |\ \ \  
 | 
				
			||||||
 | 
					| | | * | | 
 | 
				
			||||||
 | 
					| | |/| | | 
 | 
				
			||||||
 | 
					| | | |/ /  
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					* | | | |   
 | 
				
			||||||
 | 
					|\ \ \ \ \  
 | 
				
			||||||
 | 
					| * \ \ \ \   
 | 
				
			||||||
 | 
					| |\ \ \ \ \  
 | 
				
			||||||
 | 
					| | | |/ / /  
 | 
				
			||||||
 | 
					| | |/| | |   
 | 
				
			||||||
 | 
					| | | | * | 
 | 
				
			||||||
 | 
					| | | | * | 
 | 
				
			||||||
 | 
					* | | | | | 
 | 
				
			||||||
 | 
					* | | | | | 
 | 
				
			||||||
 | 
					|/ / / / /  
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | |   
 | 
				
			||||||
 | 
					|\ \ \ \ \  
 | 
				
			||||||
 | 
					| * | | | | 
 | 
				
			||||||
 | 
					|/| | | | | 
 | 
				
			||||||
 | 
					| | * | | |   
 | 
				
			||||||
 | 
					| | |\ \ \ \  
 | 
				
			||||||
 | 
					| | | * | | | 
 | 
				
			||||||
 | 
					| | |/| | | | 
 | 
				
			||||||
 | 
					| |/| | |/ /  
 | 
				
			||||||
 | 
					| | | |/| |   
 | 
				
			||||||
 | 
					| | | | | * 
 | 
				
			||||||
 | 
					| |_|_|_|/  
 | 
				
			||||||
 | 
					|/| | | |   
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| |/ / /  
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | |   
 | 
				
			||||||
 | 
					|\ \ \ \  
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					|/| | | | 
 | 
				
			||||||
 | 
					| |/ / /  
 | 
				
			||||||
 | 
					| * | |   
 | 
				
			||||||
 | 
					| |\ \ \  
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| |/| | | 
 | 
				
			||||||
 | 
					| | |/ /  
 | 
				
			||||||
 | 
					| | * |   
 | 
				
			||||||
 | 
					| | |\ \  
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					| | |/| | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | |   
 | 
				
			||||||
 | 
					|\ \ \ \ \  
 | 
				
			||||||
 | 
					| * | | | | 
 | 
				
			||||||
 | 
					|/| | | | | 
 | 
				
			||||||
 | 
					| | * | | | 
 | 
				
			||||||
 | 
					| | * | | | 
 | 
				
			||||||
 | 
					| | * | | | 
 | 
				
			||||||
 | 
					| |/ / / /  
 | 
				
			||||||
 | 
					| * | | |   
 | 
				
			||||||
 | 
					| |\ \ \ \  
 | 
				
			||||||
 | 
					| | * | | | 
 | 
				
			||||||
 | 
					| |/| | | | 
 | 
				
			||||||
 | 
					* | | | | | 
 | 
				
			||||||
 | 
					* | | | | | 
 | 
				
			||||||
 | 
					* | | | | | 
 | 
				
			||||||
 | 
					* | | | | | 
 | 
				
			||||||
 | 
					* | | | | | 
 | 
				
			||||||
 | 
					| | | | * | 
 | 
				
			||||||
 | 
					* | | | | |   
 | 
				
			||||||
 | 
					|\ \ \ \ \ \  
 | 
				
			||||||
 | 
					| * | | | | | 
 | 
				
			||||||
 | 
					|/| | | | | | 
 | 
				
			||||||
 | 
					| | | | | * | 
 | 
				
			||||||
 | 
					| | | | |/ /  
 | 
				
			||||||
 | 
					* | | | | |   
 | 
				
			||||||
 | 
					|\ \ \ \ \ \  
 | 
				
			||||||
 | 
					* | | | | | | 
 | 
				
			||||||
 | 
					* | | | | | | 
 | 
				
			||||||
 | 
					| | | | * | | 
 | 
				
			||||||
 | 
					* | | | | | | 
 | 
				
			||||||
 | 
					* | | | | | |   
 | 
				
			||||||
 | 
					|\ \ \ \ \ \ \  
 | 
				
			||||||
 | 
					| | |_|_|/ / /  
 | 
				
			||||||
 | 
					| |/| | | | |   
 | 
				
			||||||
 | 
					| | | | * | | 
 | 
				
			||||||
 | 
					| | | | * | | 
 | 
				
			||||||
 | 
					| | | | * | | 
 | 
				
			||||||
 | 
					| | | | * | | 
 | 
				
			||||||
 | 
					| | | | * | | 
 | 
				
			||||||
 | 
					| | | | * | | 
 | 
				
			||||||
 | 
					| | | |/ / /  
 | 
				
			||||||
 | 
					| | | * | | 
 | 
				
			||||||
 | 
					| | | * | | 
 | 
				
			||||||
 | 
					| | | * | | 
 | 
				
			||||||
 | 
					| | |/| | | 
 | 
				
			||||||
 | 
					| | | * | | 
 | 
				
			||||||
 | 
					| | |/| | | 
 | 
				
			||||||
 | 
					| | | |/ /  
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| |/| | | 
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					| | |/ /  
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| * | |   
 | 
				
			||||||
 | 
					| |\ \ \  
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| |/| | | 
 | 
				
			||||||
 | 
					| | |/ /  
 | 
				
			||||||
 | 
					| | * |   
 | 
				
			||||||
 | 
					| | |\ \  
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					|\| | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| |\| | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					|\| | | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| |\| | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					|\| | | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| | |/ /  
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| |\| | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| |\| | 
 | 
				
			||||||
 | 
					| | * |   
 | 
				
			||||||
 | 
					| | |\ \  
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					|\| | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| |\| | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					| | |/ /  
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| |\| | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | | * 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| | | *   
 | 
				
			||||||
 | 
					| | | |\  
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					| |_|_|/  
 | 
				
			||||||
 | 
					|/| | |   
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| |\| | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					|/| | | 
 | 
				
			||||||
 | 
					| |/ /  
 | 
				
			||||||
 | 
					| * |   
 | 
				
			||||||
 | 
					| |\ \  
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| | * |   
 | 
				
			||||||
 | 
					| | |\ \  
 | 
				
			||||||
 | 
					| | |/ /  
 | 
				
			||||||
 | 
					| |/| |   
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | |   
 | 
				
			||||||
 | 
					| |\ \ \  
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| | |/ /  
 | 
				
			||||||
 | 
					| |/| |   
 | 
				
			||||||
 | 
					| | * | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | |   
 | 
				
			||||||
 | 
					| |\ \ \  
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					|\| | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					|\| | | | 
 | 
				
			||||||
 | 
					| | | | *   
 | 
				
			||||||
 | 
					| | | | |\  
 | 
				
			||||||
 | 
					| |_|_|_|/  
 | 
				
			||||||
 | 
					|/| | | |   
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					* | | | | 
 | 
				
			||||||
 | 
					|\| | | | 
 | 
				
			||||||
 | 
					| * | | |   
 | 
				
			||||||
 | 
					| |\ \ \ \  
 | 
				
			||||||
 | 
					| | | |/ /  
 | 
				
			||||||
 | 
					| | |/| |   
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| * | | | 
 | 
				
			||||||
 | 
					| | * | | 
 | 
				
			||||||
 | 
					| | | * | 
 | 
				
			||||||
 | 
					| | |/ /  
 | 
				
			||||||
 | 
					| |/| |   
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					| | | * 
 | 
				
			||||||
 | 
					* | | | 
 | 
				
			||||||
 | 
					|\| | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					| * | | 
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										338
									
								
								modules/gitgraph/parser.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										338
									
								
								modules/gitgraph/parser.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -99,8 +99,19 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"Subtract":      base.Subtract,
 | 
							"Subtract":      base.Subtract,
 | 
				
			||||||
		"EntryIcon":     base.EntryIcon,
 | 
							"EntryIcon":     base.EntryIcon,
 | 
				
			||||||
		"MigrationIcon": MigrationIcon,
 | 
							"MigrationIcon": MigrationIcon,
 | 
				
			||||||
		"Add": func(a, b int) int {
 | 
							"Add": func(a ...int) int {
 | 
				
			||||||
			return a + b
 | 
								sum := 0
 | 
				
			||||||
 | 
								for _, val := range a {
 | 
				
			||||||
 | 
									sum += val
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return sum
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"Mul": func(a ...int) int {
 | 
				
			||||||
 | 
								sum := 1
 | 
				
			||||||
 | 
								for _, val := range a {
 | 
				
			||||||
 | 
									sum *= val
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return sum
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		"ActionIcon": ActionIcon,
 | 
							"ActionIcon": ActionIcon,
 | 
				
			||||||
		"DateFmtLong": func(t time.Time) string {
 | 
							"DateFmtLong": func(t time.Time) string {
 | 
				
			||||||
@@ -437,6 +448,20 @@ func NewTextFuncMap() []texttmpl.FuncMap {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			return float32(n) * 100 / float32(sum)
 | 
								return float32(n) * 100 / float32(sum)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							"Add": func(a ...int) int {
 | 
				
			||||||
 | 
								sum := 0
 | 
				
			||||||
 | 
								for _, val := range a {
 | 
				
			||||||
 | 
									sum += val
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return sum
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"Mul": func(a ...int) int {
 | 
				
			||||||
 | 
								sum := 1
 | 
				
			||||||
 | 
								for _, val := range a {
 | 
				
			||||||
 | 
									sum *= val
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return sum
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}}
 | 
						}}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -775,6 +775,8 @@ audio_not_supported_in_browser = Your browser does not support the HTML5 'audio'
 | 
				
			|||||||
stored_lfs = Stored with Git LFS
 | 
					stored_lfs = Stored with Git LFS
 | 
				
			||||||
symbolic_link = Symbolic link
 | 
					symbolic_link = Symbolic link
 | 
				
			||||||
commit_graph = Commit Graph
 | 
					commit_graph = Commit Graph
 | 
				
			||||||
 | 
					commit_graph.monochrome = Mono
 | 
				
			||||||
 | 
					commit_graph.color = Color
 | 
				
			||||||
blame = Blame
 | 
					blame = Blame
 | 
				
			||||||
normal_view = Normal View
 | 
					normal_view = Normal View
 | 
				
			||||||
line = line
 | 
					line = line
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								public/img/svg/material-invert-colors.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/img/svg/material-invert-colors.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg viewBox="0 0 768 768" class="svg material-invert-colors" width="16" height="16" aria-hidden="true"><path d="M384 627V163.5l-135 135c-36 36-57 85.5-57 136.5 0 103.19 88.8 192 192 192zm181.5-373.5C666 354 666 514.5 565.5 615 516 664.5 450 690 384 690s-132-25.5-181.5-75C102 514.5 102 354 202.5 253.5L384 72z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 319 B  | 
							
								
								
									
										1
									
								
								public/img/svg/material-palette.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/img/svg/material-palette.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg viewBox="0 0 768 768" class="svg material-palette" width="16" height="16" aria-hidden="true"><path d="M559.5 384c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zm-96-127.5c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zm-159 0c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zm-96 127.5c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zM384 96c159 0 288 115.5 288 256.5 0 88.5-72 159-160.5 159H456c-27 0-48 21-48 48 0 12 4.5 22.5 12 31.5s12 21 12 33c0 27-21 48-48 48-159 0-288-129-288-288S225 96 384 96z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 540 B  | 
@@ -90,6 +90,11 @@ func Commits(ctx *context.Context) {
 | 
				
			|||||||
func Graph(ctx *context.Context) {
 | 
					func Graph(ctx *context.Context) {
 | 
				
			||||||
	ctx.Data["PageIsCommits"] = true
 | 
						ctx.Data["PageIsCommits"] = true
 | 
				
			||||||
	ctx.Data["PageIsViewCode"] = true
 | 
						ctx.Data["PageIsViewCode"] = true
 | 
				
			||||||
 | 
						mode := strings.ToLower(ctx.QueryTrim("mode"))
 | 
				
			||||||
 | 
						if mode != "monochrome" {
 | 
				
			||||||
 | 
							mode = "color"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ctx.Data["Mode"] = mode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	commitsCount, err := ctx.Repo.GetCommitsCount()
 | 
						commitsCount, err := ctx.Repo.GetCommitsCount()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -105,7 +110,7 @@ func Graph(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	page := ctx.QueryInt("page")
 | 
						page := ctx.QueryInt("page")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page)
 | 
						graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("GetCommitGraph", err)
 | 
							ctx.ServerError("GetCommitGraph", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -116,7 +121,9 @@ func Graph(ctx *context.Context) {
 | 
				
			|||||||
	ctx.Data["Reponame"] = ctx.Repo.Repository.Name
 | 
						ctx.Data["Reponame"] = ctx.Repo.Repository.Name
 | 
				
			||||||
	ctx.Data["CommitCount"] = commitsCount
 | 
						ctx.Data["CommitCount"] = commitsCount
 | 
				
			||||||
	ctx.Data["Branch"] = ctx.Repo.BranchName
 | 
						ctx.Data["Branch"] = ctx.Repo.BranchName
 | 
				
			||||||
	ctx.Data["Page"] = context.NewPagination(int(allCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
 | 
						paginator := context.NewPagination(int(allCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
 | 
				
			||||||
 | 
						paginator.AddParam(ctx, "mode", "Mode")
 | 
				
			||||||
 | 
						ctx.Data["Page"] = paginator
 | 
				
			||||||
	ctx.HTML(200, tplGraph)
 | 
						ctx.HTML(200, tplGraph)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,21 +2,44 @@
 | 
				
			|||||||
<div class="repository commits">
 | 
					<div class="repository commits">
 | 
				
			||||||
	{{template "repo/header" .}}
 | 
						{{template "repo/header" .}}
 | 
				
			||||||
	<div class="ui container">
 | 
						<div class="ui container">
 | 
				
			||||||
		<div id="git-graph-container" class="ui segment">
 | 
							<div id="git-graph-container" class="ui segment{{if eq .Mode "monochrome"}} monochrome{{end}}">
 | 
				
			||||||
			<h1>{{.i18n.Tr "repo.commit_graph"}}</h1>
 | 
								<h2 class="ui header dividing">{{.i18n.Tr "repo.commit_graph"}}
 | 
				
			||||||
 | 
									<div class="ui right">
 | 
				
			||||||
 | 
										<div class="ui icon buttons tiny color-buttons">
 | 
				
			||||||
 | 
											<button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{.i18n.Tr "repo.commit_graph.monochrome"}}"><span class="emoji">{{svg "material-invert-colors" 16}}</span> {{.i18n.Tr "repo.commit_graph.monochrome"}}</button>
 | 
				
			||||||
 | 
											<button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{.i18n.Tr "repo.commit_graph.color"}}"><span class="emoji">{{svg "material-palette" 16}}</span> {{.i18n.Tr "repo.commit_graph.color"}}</button>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</h2>
 | 
				
			||||||
 | 
								<div class="ui dividing"></div>
 | 
				
			||||||
			<div id="rel-container">
 | 
								<div id="rel-container">
 | 
				
			||||||
				<canvas id="graph-canvas">
 | 
									<svg viewbox="{{Mul .Graph.MinColumn 5}} {{Mul .Graph.MinRow 10}} {{Add (Mul .Graph.Width 5) 5}} {{Mul .Graph.Height 10}}" width="{{Add (Mul .Graph.Width 10) 10}}px">
 | 
				
			||||||
					<ul id="graph-raw-list">
 | 
										{{range $flowid, $flow := .Graph.Flows}}
 | 
				
			||||||
						{{ range .Graph }}
 | 
											<g id="flow-{{$flow.ID}}" class="flow-group flow-color-{{$flow.ColorNumber}} flow-color-16-{{$flow.Color16}}" data-flow="{{$flow.ID}}" data-color="{{$flow.ColorNumber}}">
 | 
				
			||||||
							<li><span class="node-relation">{{ .GraphAcii -}}</span></li>
 | 
												<path d="{{range $i, $glyph := $flow.Glyphs -}}
 | 
				
			||||||
 | 
													{{- if or (eq $glyph.Glyph '*') (eq $glyph.Glyph '|') -}}
 | 
				
			||||||
 | 
														M {{Add (Mul $glyph.Column 5) 5}} {{Add (Mul $glyph.Row 10) 0}} v 10 {{/* */ -}}
 | 
				
			||||||
 | 
													{{- else if eq $glyph.Glyph '/' -}}
 | 
				
			||||||
 | 
														M {{Add (Mul $glyph.Column 5) 10}} {{Add (Mul $glyph.Row 10) 0}} l -10 10 {{/* */ -}}
 | 
				
			||||||
 | 
													{{- else if eq $glyph.Glyph '\\' -}}
 | 
				
			||||||
 | 
														M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 10) 0}} l 10 10 {{/* */ -}}
 | 
				
			||||||
 | 
													{{- else if or (eq $glyph.Glyph '-') (eq $glyph.Glyph '.') -}}
 | 
				
			||||||
 | 
														M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 10) 10}} h 5 {{/* */ -}}
 | 
				
			||||||
 | 
													{{- else if eq $glyph.Glyph '_' -}}
 | 
				
			||||||
 | 
														M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 10) 10}} h 10 {{/* */ -}}
 | 
				
			||||||
 | 
													{{- end -}}
 | 
				
			||||||
 | 
												{{- end}}" stroke-width="1" fill="none" id="flow-{{$flow.ID}}-path" stroke-linecap="round"/>
 | 
				
			||||||
 | 
												{{range $flow.Commits}}
 | 
				
			||||||
 | 
													<circle class="flow-commit" cx="{{Add (Mul .Column 5) 5}}" cy="{{Add (Mul .Row 10) 5}}" r="2.5" stroke="none" id="flow-commit-{{.Rev}}" data-rev="{{.Rev}}"/>
 | 
				
			||||||
							{{end}}
 | 
												{{end}}
 | 
				
			||||||
					</ul>
 | 
											</g>
 | 
				
			||||||
				</canvas>
 | 
										{{end}}
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div id="rev-container">
 | 
								<div id="rev-container">
 | 
				
			||||||
				<ul id="rev-list">
 | 
									<ul id="rev-list">
 | 
				
			||||||
					{{ range .Graph }}
 | 
										{{ range .Graph.Commits }}
 | 
				
			||||||
						<li>
 | 
											<li id="commit-{{.Rev}}" data-flow="{{.Flow}}">
 | 
				
			||||||
							{{ if .OnlyRelation }}
 | 
												{{ if .OnlyRelation }}
 | 
				
			||||||
								<span />
 | 
													<span />
 | 
				
			||||||
							{{ else }}
 | 
												{{ else }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
					 | 
				
			||||||
    } 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;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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() {
 | 
					export default async function initGitGraph() {
 | 
				
			||||||
  const graphCanvas = document.getElementById('graph-canvas');
 | 
					  const graphContainer = document.getElementById('git-graph-container');
 | 
				
			||||||
  if (!graphCanvas || !graphCanvas.getContext) return;
 | 
					  if (!graphContainer) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Grab the raw graphList
 | 
					  $('#flow-color-monochrome').on('click', () => {
 | 
				
			||||||
  const graphList = [];
 | 
					    $('#flow-color-monochrome').addClass('active');
 | 
				
			||||||
  $('#graph-raw-list li span.node-relation').each(function () {
 | 
					    $('#flow-color-colored').removeClass('active');
 | 
				
			||||||
    graphList.push($(this).text());
 | 
					    $('#git-graph-container').removeClass('colored').addClass('monochrome');
 | 
				
			||||||
  });
 | 
					    const params = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
					    params.set('mode', 'monochrome');
 | 
				
			||||||
  // Define some drawing parameters
 | 
					    const queryString = params.toString();
 | 
				
			||||||
  const config = {
 | 
					    if (queryString) {
 | 
				
			||||||
    unitSize: 20,
 | 
					      window.history.replaceState({}, '', `?${queryString}`);
 | 
				
			||||||
    lineWidth: 3,
 | 
					    } else {
 | 
				
			||||||
    nodeRadius: 4
 | 
					      window.history.replaceState({}, '', window.location.pathname);
 | 
				
			||||||
  };
 | 
					    }
 | 
				
			||||||
 | 
					    $('.pagination a').each((_, that) => {
 | 
				
			||||||
 | 
					      const href = $(that).attr('href');
 | 
				
			||||||
  const gitGraph = new GitGraph(graphCanvas, graphList, config);
 | 
					      if (!href) return;
 | 
				
			||||||
  gitGraph.draw();
 | 
					      const url = new URL(href, window.location);
 | 
				
			||||||
  graphCanvas.closest('#git-graph-container').classList.add('in');
 | 
					      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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  $('#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');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3082,11 +3082,3 @@ tbody.commit-list {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
#git-graph-container {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#git-graph-container.in {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										256
									
								
								web_src/less/features/gitgraph.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								web_src/less/features/gitgraph.less
									
									
									
									
									
										Normal file
									
								
							@@ -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,5 +1,6 @@
 | 
				
			|||||||
@import "~font-awesome/css/font-awesome.css";
 | 
					@import "~font-awesome/css/font-awesome.css";
 | 
				
			||||||
@import "./vendor/gitGraph.css";
 | 
					
 | 
				
			||||||
 | 
					@import "./features/gitgraph.less";
 | 
				
			||||||
@import "./features/animations.less";
 | 
					@import "./features/animations.less";
 | 
				
			||||||
@import "./markdown/mermaid.less";
 | 
					@import "./markdown/mermaid.less";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -919,11 +919,17 @@ a.ui.basic.green.label:hover {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.ui.active.button:active,
 | 
					.ui.active.button:active,
 | 
				
			||||||
.ui.button:active,
 | 
					.ui.button:active,
 | 
				
			||||||
.ui.button:focus {
 | 
					.ui.button:focus,
 | 
				
			||||||
 | 
					.ui.active.button {
 | 
				
			||||||
    background-color: #2e3e4e;
 | 
					    background-color: #2e3e4e;
 | 
				
			||||||
    color: #dbdbdb;
 | 
					    color: #dbdbdb;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ui.active.button:hover {
 | 
				
			||||||
 | 
					    background-color: #475e75;
 | 
				
			||||||
 | 
					    color: #dbdbdb;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.ui.dropdown .menu .selected.item,
 | 
					.ui.dropdown .menu .selected.item,
 | 
				
			||||||
.ui.dropdown.selected {
 | 
					.ui.dropdown.selected {
 | 
				
			||||||
    color: #dbdbdb;
 | 
					    color: #dbdbdb;
 | 
				
			||||||
@@ -1921,6 +1927,45 @@ footer .container .links > * {
 | 
				
			|||||||
    color: #2a2e3a;
 | 
					    color: #2a2e3a;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#git-graph-container.monochrome #rel-container .flow-group {
 | 
				
			||||||
 | 
					    stroke: dimgrey;
 | 
				
			||||||
 | 
					    fill: dimgrey;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#git-graph-container.monochrome #rel-container .flow-group.highlight {
 | 
				
			||||||
 | 
					    stroke: darkgrey;
 | 
				
			||||||
 | 
					    fill: darkgrey;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#git-graph-container:not(.monochrome) #rel-container .flow-group {
 | 
				
			||||||
 | 
					    &.flow-color-16-5 {
 | 
				
			||||||
 | 
					        stroke: #5543b1;
 | 
				
			||||||
 | 
					        fill: #5543b1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight {
 | 
				
			||||||
 | 
					    &.flow-color-16-5 {
 | 
				
			||||||
 | 
					        stroke: #7058e6;
 | 
				
			||||||
 | 
					        fill: #7058e6;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#git-graph-container #rev-list li.highlight,
 | 
				
			||||||
 | 
					#git-graph-container #rev-list li.hover {
 | 
				
			||||||
 | 
					    background-color: rgba(255, 255, 255, .05);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#git-graph-container #rev-list li.highlight.hover {
 | 
				
			||||||
 | 
					    background-color: rgba(255, 255, 255, .1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#git-graph-container .ui.buttons button#flow-color-monochrome.ui.button {
 | 
				
			||||||
 | 
					    border-left-color: rgb(76, 80, 92);
 | 
				
			||||||
 | 
					    border-left-style: solid;
 | 
				
			||||||
 | 
					    border-left-width: 1px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.mermaid-chart {
 | 
					.mermaid-chart {
 | 
				
			||||||
    filter: invert(84%) hue-rotate(180deg);
 | 
					    filter: invert(84%) hue-rotate(180deg);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								web_src/less/vendor/gitGraph.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								web_src/less/vendor/gitGraph.css
									
									
									
									
										vendored
									
									
								
							@@ -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;}
 | 
					 | 
				
			||||||
							
								
								
									
										1
									
								
								web_src/svg/material-invert-colors.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web_src/svg/material-invert-colors.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg viewBox="0 0 768 768"><path d="M384 627V163.5l-135 135c-36 36-57 85.5-57 136.5 0 103.19 88.8 192 192 192zm181.5-373.5C666 354 666 514.5 565.5 615 516 664.5 450 690 384 690s-132-25.5-181.5-75C102 514.5 102 354 202.5 253.5L384 72z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 242 B  | 
							
								
								
									
										1
									
								
								web_src/svg/material-palette.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web_src/svg/material-palette.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg viewBox="0 0 768 768"><path d="M559.5 384c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zm-96-127.5c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zm-159 0c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zm-96 127.5c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zM384 96c159 0 288 115.5 288 256.5 0 88.5-72 159-160.5 159H456c-27 0-48 21-48 48 0 12 4.5 22.5 12 31.5s12 21 12 33c0 27-21 48-48 48-159 0-288-129-288-288S225 96 384 96z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 469 B  | 
		Reference in New Issue
	
	Block a user