mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:45:25 +01: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:
		
							parent
							
								
									f1a42f5d5e
								
							
						
					
					
						commit
						2c1ae6c82d
					
				| @ -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 -}} | ||||||
| 						{{ end }} | 								{{- if or (eq $glyph.Glyph '*') (eq $glyph.Glyph '|') -}} | ||||||
| 					</ul> | 									M {{Add (Mul $glyph.Column 5) 5}} {{Add (Mul $glyph.Row 10) 0}} v 10 {{/* */ -}} | ||||||
| 				</canvas> | 								{{- 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}} | ||||||
|  | 						</g> | ||||||
|  | 					{{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'); | ||||||
|  |     const queryString = params.toString(); | ||||||
|  |     if (queryString) { | ||||||
|  |       window.history.replaceState({}, '', `?${queryString}`); | ||||||
|  |     } else { | ||||||
|  |       window.history.replaceState({}, '', window.location.pathname); | ||||||
|  |     } | ||||||
|  |     $('.pagination a').each((_, that) => { | ||||||
|  |       const href = $(that).attr('href'); | ||||||
|  |       if (!href) return; | ||||||
|  |       const url = new URL(href, window.location); | ||||||
|  |       const params = url.searchParams; | ||||||
|  |       params.set('mode', 'monochrome'); | ||||||
|  |       url.search = `?${params.toString()}`; | ||||||
|  |       $(that).attr('href', url.href); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |   $('#flow-color-colored').on('click', () => { | ||||||
|  |     $('#flow-color-colored').addClass('active'); | ||||||
|  |     $('#flow-color-monochrome').removeClass('active'); | ||||||
|  |     $('#git-graph-container').addClass('colored').removeClass('monochrome'); | ||||||
|  |     $('.pagination a').each((_, that) => { | ||||||
|  |       const href = $(that).attr('href'); | ||||||
|  |       if (!href) return; | ||||||
|  |       const url = new URL(href, window.location); | ||||||
|  |       const params = url.searchParams; | ||||||
|  |       params.delete('mode'); | ||||||
|  |       url.search = `?${params.toString()}`; | ||||||
|  |       $(that).attr('href', url.href); | ||||||
|  |     }); | ||||||
|  |     const params = new URLSearchParams(window.location.search); | ||||||
|  |     params.delete('mode'); | ||||||
|  |     const queryString = params.toString(); | ||||||
|  |     if (queryString) { | ||||||
|  |       window.history.replaceState({}, '', `?${queryString}`); | ||||||
|  |     } else { | ||||||
|  |       window.history.replaceState({}, '', window.location.pathname); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   $('#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'); | ||||||
|   }); |   }); | ||||||
| 
 |  | ||||||
|   // Define some drawing parameters
 |  | ||||||
|   const config = { |  | ||||||
|     unitSize: 20, |  | ||||||
|     lineWidth: 3, |  | ||||||
|     nodeRadius: 4 |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   const gitGraph = new GitGraph(graphCanvas, graphList, config); |  | ||||||
|   gitGraph.draw(); |  | ||||||
|   graphCanvas.closest('#git-graph-container').classList.add('in'); |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -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 | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user