From dcddace8a4769e98ebd5d9f5a6688a828fb5bcd7 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Fri, 10 Jul 2020 21:46:18 +0100 Subject: [PATCH 01/11] Render the git graph on the server 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. Signed-off-by: Andrew Thornton --- modules/gitgraph/graph.go | 584 +++++++++++++++++--- modules/gitgraph/graph_test.go | 666 ++++++++++++++++++++++- modules/templates/helper.go | 29 +- routers/repo/commit.go | 7 +- templates/repo/graph.tmpl | 39 +- web_src/js/features/gitgraph.js | 647 +++------------------- web_src/less/_repository.less | 184 ++++++- web_src/less/themes/theme-arc-green.less | 184 +++++++ 8 files changed, 1669 insertions(+), 671 deletions(-) diff --git a/modules/gitgraph/graph.go b/modules/gitgraph/graph.go index 4ba110c70615d..7bbab51222bff 100644 --- a/modules/gitgraph/graph.go +++ b/modules/gitgraph/graph.go @@ -16,26 +16,485 @@ import ( "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 +// 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 +} + +// CX returns the centre of the circle for this commit +func (c *Commit) CX() int { + return c.Column*5 + 5 +} + +// CY returns the centre of the circle for this commit +func (c *Commit) CY() int { + return c.Row*10 + 5 +} + +// OnlyRelation returns whether this a relation only commit +func (c *Commit) OnlyRelation() bool { + return c.Row == -1 +} + +func newCommit(row, column, idx int, line []byte) (*Commit, error) { + data := bytes.SplitN(line[idx+5:], []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 +} + +// Glyph represents a co-ordinate and glyph +type Glyph struct { + Row int + Column int + Glyph byte +} + +// 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 (f *Flow) Color16() int { + return f.ColorNumber % 16 +} + +type parserState 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 +} + +// 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 (g *Graph) Width() int { + return g.MaxColumn - g.MinColumn + 1 +} + +// Height returns the height of the graph +func (g *Graph) Height() int { + return g.MaxRow - g.MinRow + 1 +} + +// NewGraph creates a basic graph +func NewGraph() *Graph { + graph := &Graph{} + graph.relationCommit = &Commit{ + Row: -1, + Column: -1, + } + graph.Flows = map[int64]*Flow{} + return graph +} + +func (state *parserState) reset() { + state.glyphs = state.glyphs[0:0] + state.oldGlyphs = state.oldGlyphs[0:0] + state.flows = state.flows[0:0] + state.oldFlows = state.oldFlows[0:0] + state.maxFlow = 0 + state.colors = state.colors[0:0] + state.oldColors = state.oldColors[0:0] + state.availableColors = state.availableColors[0:0] + state.availableColors = append(state.availableColors, 1, 2) + state.nextAvailable = 0 + state.firstInUse = -1 + state.firstAvailable = 0 + state.maxAllowedColors = 0 +} + +func (state *parserState) parseFlows(graph *Graph, row int, line []byte) error { + idx := bytes.Index(line, []byte("DATA:")) + if idx < 0 { + state.parseGlyphs(line) + } else { + state.parseGlyphs(line[:idx]) + } + + var err error + commitDone := false + + for column, glyph := range state.glyphs { + flowID := state.flows[column] + if glyph == ' ' { + continue + } + + flow, ok := graph.Flows[flowID] + if !ok { + flow = &Flow{ + ID: flowID, + ColorNumber: state.colors[column], + MinRow: row, + MinColumn: column, + MaxRow: row, + MaxColumn: column, + } + graph.Flows[flowID] = flow + } + 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 + } + 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 + } + + flow.Glyphs = append(flow.Glyphs, Glyph{ + row, + 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 + } + commit, err2 := newCommit(row, column, idx, line) + if err != nil && err2 != nil { + err = fmt.Errorf("%v %w", err2, err) + continue + } else if err2 != nil { + err = err2 + continue + } + commit.Flow = flowID + graph.Commits = append(graph.Commits, commit) + flow.Commits = append(flow.Commits, commit) + } + } + if !commitDone { + graph.Commits = append(graph.Commits, graph.relationCommit) + } + return err +} + +func (state *parserState) releaseUnusedColors() { + if state.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 := state.firstInUse + for _, color := range state.oldColors { + if color == 0 { + continue + } + found := false + i := position + for j := stepstaken; i != state.firstAvailable && j < len(state.availableColors); j++ { + colorToCheck := state.availableColors[i] + if colorToCheck == color { + found = true + break + } + i = (i + 1) % len(state.availableColors) + } + if !found { + // Duplicate color + continue + } + // Swap them around + state.availableColors[position], state.availableColors[i] = state.availableColors[i], state.availableColors[position] + stepstaken++ + position = (state.firstInUse + stepstaken) % len(state.availableColors) + if position == state.firstAvailable || stepstaken == len(state.availableColors) { + break + } + } + if stepstaken == len(state.availableColors) { + state.firstAvailable = -1 + } else { + state.firstAvailable = position + if state.nextAvailable == -1 { + state.nextAvailable = state.firstAvailable + } + } + } +} + +func (state *parserState) parseGlyphs(glyphs []byte) { + + // Clean state for parsing this row + state.glyphs, state.oldGlyphs = state.oldGlyphs, state.glyphs + state.glyphs = state.glyphs[0:0] + state.flows, state.oldFlows = state.oldFlows, state.flows + state.flows = state.flows[0:0] + state.colors, state.oldColors = state.oldColors, state.colors + + // Ensure we have enough flows and colors + state.colors = state.colors[0:0] + for range glyphs { + state.flows = append(state.flows, 0) + state.colors = append(state.colors, 0) + } + + // Copy the provided glyphs in to state.glyphs for safekeeping + state.glyphs = append(state.glyphs, glyphs...) + + // release unused colors + state.releaseUnusedColors() + + for i := len(glyphs) - 1; i >= 0; i-- { + glyph := glyphs[i] + switch glyph { + case '|': + fallthrough + case '*': + state.setUpFlow(i) + case '/': + state.setOutFlow(i) + case '\\': + state.setInFlow(i) + case '_': + state.setRightFlow(i) + case '.': + fallthrough + case '-': + state.setLeftFlow(i) + case ' ': + // no-op + default: + state.newFlow(i) + } + } +} + +func (state *parserState) takePreviousFlow(i, j int) { + if j < len(state.oldFlows) && state.oldFlows[j] > 0 { + state.flows[i] = state.oldFlows[j] + state.oldFlows[j] = 0 + state.colors[i] = state.oldColors[j] + state.oldColors[j] = 0 + } else { + state.newFlow(i) + } +} + +func (state *parserState) takeCurrentFlow(i, j int) { + if j < len(state.flows) && state.flows[j] > 0 { + state.flows[i] = state.flows[j] + state.colors[i] = state.colors[j] + } else { + state.newFlow(i) + } +} + +func (state *parserState) newFlow(i int) { + state.maxFlow++ + state.flows[i] = state.maxFlow + + // Now give this flow a color + if state.nextAvailable == -1 { + next := len(state.availableColors) + if state.maxAllowedColors < 1 || next < state.maxAllowedColors { + state.nextAvailable = next + state.firstAvailable = next + state.availableColors = append(state.availableColors, next+1) + } + } + state.colors[i] = state.availableColors[state.nextAvailable] + if state.firstInUse == -1 { + state.firstInUse = state.nextAvailable + } + state.availableColors[state.firstAvailable], state.availableColors[state.nextAvailable] = state.availableColors[state.nextAvailable], state.availableColors[state.firstAvailable] + + state.nextAvailable = (state.nextAvailable + 1) % len(state.availableColors) + state.firstAvailable = (state.firstAvailable + 1) % len(state.availableColors) + + if state.nextAvailable == state.firstInUse { + state.nextAvailable = state.firstAvailable + } + if state.nextAvailable == state.firstInUse { + state.nextAvailable = -1 + state.firstAvailable = -1 + } +} + +// setUpFlow handles '|' or '*' +func (state *parserState) setUpFlow(i int) { + // In preference order: + // + // Previous Row: '\? ' ' |' ' /' + // Current Row: ' | ' ' |' ' | ' + if i > 0 && i-1 < len(state.oldGlyphs) && state.oldGlyphs[i-1] == '\\' { + state.takePreviousFlow(i, i-1) + } else if i < len(state.oldGlyphs) && (state.oldGlyphs[i] == '|' || state.oldGlyphs[i] == '*') { + state.takePreviousFlow(i, i) + } else if i+1 < len(state.oldGlyphs) && state.oldGlyphs[i+1] == '/' { + state.takePreviousFlow(i, i+1) + } else { + state.newFlow(i) + } +} + +// setOutFlow handles '/' +func (state *parserState) setOutFlow(i int) { + // In preference order: + // + // Previous Row: ' |/' ' |_' ' |' ' /' ' _' '\' + // Current Row: '/| ' '/| ' '/ ' '/ ' '/ ' '/' + if i+2 < len(state.oldGlyphs) && + (state.oldGlyphs[i+1] == '|' || state.oldGlyphs[i+1] == '*') && + (state.oldGlyphs[i+2] == '/' || state.oldGlyphs[i+2] == '_') && + i+1 < len(state.glyphs) && + (state.glyphs[i+1] == '|' || state.glyphs[i+1] == '*') { + state.takePreviousFlow(i, i+2) + } else if i+1 < len(state.oldGlyphs) && + (state.oldGlyphs[i+1] == '|' || state.oldGlyphs[i+1] == '*' || + state.oldGlyphs[i+1] == '/' || state.oldGlyphs[i+1] == '_') { + state.takePreviousFlow(i, i+1) + if state.oldGlyphs[i+1] == '/' { + state.glyphs[i] = '|' + } + } else if i < len(state.oldGlyphs) && state.oldGlyphs[i] == '\\' { + state.takePreviousFlow(i, i) + } else { + state.newFlow(i) + } +} + +// setInFlow handles '\' +func (state *parserState) setInFlow(i int) { + // In preference order: + // + // Previous Row: '| ' '-. ' '| ' '\ ' '/' '---' + // Current Row: '|\' ' \' ' \' ' \' '\' ' \ ' + if i > 0 && i-1 < len(state.oldGlyphs) && + (state.oldGlyphs[i-1] == '|' || state.oldGlyphs[i-1] == '*') && + (state.glyphs[i-1] == '|' || state.glyphs[i-1] == '*') { + state.newFlow(i) + } else if i > 0 && i-1 < len(state.oldGlyphs) && + (state.oldGlyphs[i-1] == '|' || state.oldGlyphs[i-1] == '*' || + state.oldGlyphs[i-1] == '.' || state.oldGlyphs[i-1] == '\\') { + state.takePreviousFlow(i, i-1) + if state.oldGlyphs[i-1] == '\\' { + state.glyphs[i] = '|' + } + } else if i < len(state.oldGlyphs) && state.oldGlyphs[i] == '/' { + state.takePreviousFlow(i, i) + } else { + state.newFlow(i) + } +} + +// setRightFlow handles '_' +func (state *parserState) setRightFlow(i int) { + // In preference order: + // + // Current Row: '__' '_/' '_|_' '_|/' + if i+1 < len(state.glyphs) && + (state.glyphs[i+1] == '_' || state.glyphs[i+1] == '/') { + state.takeCurrentFlow(i, i+1) + } else if i+2 < len(state.glyphs) && + (state.glyphs[i+1] == '|' || state.glyphs[i+1] == '*') && + (state.glyphs[i+2] == '_' || state.glyphs[i+2] == '/') { + state.takeCurrentFlow(i, i+2) + } else { + state.newFlow(i) + } +} + +// setLeftFlow handles '----.' +func (state *parserState) setLeftFlow(i int) { + if state.glyphs[i] == '.' { + state.newFlow(i) + } else if i+1 < len(state.glyphs) && + (state.glyphs[i+1] == '-' || state.glyphs[i+1] == '.') { + state.takeCurrentFlow(i, i+1) + } else { + state.newFlow(i) + } +} // GetCommitGraph return a list of commit (GraphItems) from all branches -func GetCommitGraph(r *git.Repository, page int) (GraphItems, error) { - format := "DATA:|%d|%H|%ad|%an|%ae|%h|%s" +func GetCommitGraph(r *git.Repository, page int, maxAllowedColors int) (*Graph, error) { + format := "DATA:%d|%H|%ad|%an|%ae|%h|%s" if page == 0 { page = 1 @@ -51,7 +510,8 @@ func GetCommitGraph(r *git.Repository, page int) (GraphItems, error) { "--date=iso", fmt.Sprintf("--pretty=format:%s", format), ) - commitGraph := make([]GraphItem, 0, 100) + graph := NewGraph() + stderr := new(strings.Builder) stdoutReader, stdoutWriter, err := os.Pipe() if err != nil { @@ -64,86 +524,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 { _ = stdoutWriter.Close() defer stdoutReader.Close() + parser := &parserState{} + 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() { line := scanner.Bytes() dataIdx := bytes.Index(line, []byte("DATA:")) + if dataIdx < 0 { + dataIdx = len(line) + } starIdx := bytes.IndexByte(line, '*') if starIdx >= 0 && starIdx < dataIdx { commitsToSkip-- } + parser.parseGlyphs(line[:dataIdx]) } + + row := 0 + // Skip initial non-commit lines for scanner.Scan() { - if bytes.IndexByte(scanner.Bytes(), '*') >= 0 { - line := scanner.Text() - graphItem, err := graphItemFromString(line, r) - if err != nil { + line := scanner.Bytes() + if bytes.IndexByte(line, '*') >= 0 { + if err := parser.parseFlows(graph, row, line); err != nil { cancel() return err } - commitGraph = append(commitGraph, graphItem) break } + parser.parseGlyphs(line) } for scanner.Scan() { - line := scanner.Text() - graphItem, err := graphItemFromString(line, r) - if err != nil { + row++ + line := scanner.Bytes() + if err := parser.parseFlows(graph, row, line); err != nil { cancel() return err } - commitGraph = append(commitGraph, graphItem) } return scanner.Err() }); err != nil { - return commitGraph, err - } - - 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 + return graph, err + } + return graph, nil } diff --git a/modules/gitgraph/graph_test.go b/modules/gitgraph/graph_test.go index a2c7f447b6850..7c95cf02d3dfc 100644 --- a/modules/gitgraph/graph_test.go +++ b/modules/gitgraph/graph_test.go @@ -5,7 +5,9 @@ package gitgraph import ( + "bytes" "fmt" + "strings" "testing" "code.gitea.io/gitea/modules/git" @@ -14,40 +16,235 @@ import ( func BenchmarkGetCommitGraph(b *testing.B) { currentRepo, err := git.OpenRepository(".") - if err != nil { + if err != nil || currentRepo == nil { b.Error("Could not open repository") } defer currentRepo.Close() for i := 0; i < b.N; i++ { - graph, err := GetCommitGraph(currentRepo, 1) + graph, err := GetCommitGraph(currentRepo, 1, 0) if err != nil { b.Error("Could get commit graph") } - if len(graph) < 100 { + if len(graph.Commits) < 100 { b.Error("Should get 100 log lines.") } } } 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 := &parserState{} + parser.reset() for i := 0; i < b.N; i++ { - graphItem, err := graphItemFromString(testString, nil) - if err != nil { + parser.reset() + graph := NewGraph() + if err := parser.parseFlows(graph, 0, []byte(testString)); err != nil { b.Error("could not parse teststring") } - - if graphItem.Author != "Kjell Kvinge" { + if graph.Flows[1].Commits[0].Author != "Kjell Kvinge" { b.Error("Did not get expected data") } } } +func BenchmarkParseGlyphs(b *testing.B) { + parser := &parserState{} + 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 := &parserState{} + 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 := &parserState{} + 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) != 11 { + t.Errorf("Expected 11 colors but have %d", len(parser.availableColors)) + } +} + 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 { shouldPass bool testName string @@ -62,15 +259,460 @@ func TestCommitStringParsing(t *testing.T) { t.Run(test.testName, func(t *testing.T) { testString := fmt.Sprintf("%s%s", dataFirstPart, test.commitMessage) - graphItem, err := graphItemFromString(testString, nil) + idx := strings.Index(testString, "DATA:") + commit, err := newCommit(0, 0, idx, []byte(testString)) if err != nil && test.shouldPass { t.Errorf("Could not parse %s", testString) return } - if test.commitMessage != graphItem.Subject { - t.Errorf("%s does not match %s", test.commitMessage, graphItem.Subject) + if test.commitMessage != commit.Subject { + t.Errorf("%s does not match %s", test.commitMessage, commit.Subject) } }) } } + +var testglyphs = `* +* +* +* +* +* +* +* +|\ +* | +* | +* | +* | +* | +| * +* | +| * +| |\ +* | | +| | * +| | |\ +* | | \ +|\ \ \ \ +| * | | | +| |\| | | +* | | | | +|/ / / / +| | | * +| * | | +| * | | +| * | | +* | | | +* | | | +* | | | +* | | | +* | | | +|\ \ \ \ +| | * | | +| | |\| | +| | | * | +| | | | * +* | | | | +* | | | | +* | | | | +* | | | | +* | | | | +|\ \ \ \ \ +| * | | | | +|/| | | | | +| | |/ / / +| |/| | | +| | | | * +| * | | | +|/| | | | +| * | | | +|/| | | | +| | |/ / +| |/| | +| * | | +| * | | +| |\ \ \ +| | * | | +| |/| | | +| | | |/ +| | |/| +| * | | +| * | | +| * | | +| | * | +| | |\ \ +| | | * | +| | |/| | +| | | * | +| | | |\ \ +| | | | * | +| | | |/| | +| | * | | | +| | * | | | +| | |\ \ \ \ +| | | * | | | +| | |/| | | | +| | | | | * | +| | | | |/ / +* | | | / / +|/ / / / / +* | | | | +|\ \ \ \ \ +| * | | | | +|/| | | | | +| * | | | | +| * | | | | +| |\ \ \ \ \ +| | | * \ \ \ +| | | |\ \ \ \ +| | | | * | | | +| | | |/| | | | +| | | | | |/ / +| | | | |/| | +* | | | | | | +* | | | | | | +* | | | | | | +| | | | * | | +* | | | | | | +| | * | | | | +| |/| | | | | +* | | | | | | +| |/ / / / / +|/| | | | | +| | | | * | +| | | |/ / +| | |/| | +| * | | | +| | | | * +| | * | | +| | |\ \ \ +| | | * | | +| | |/| | | +| | | |/ / +| | | * | +| | * | | +| | |\ \ \ +| | | * | | +| | |/| | | +| | | |/ / +| | | * | +* | | | | +|\ \ \ \ \ +| * \ \ \ \ +| |\ \ \ \ \ +| | | |/ / / +| | |/| | | +| | | | * | +| | | | * | +* | | | | | +* | | | | | +|/ / / / / +| | | * | +* | | | | +* | | | | +* | | | | +* | | | | +|\ \ \ \ \ +| * | | | | +|/| | | | | +| | * | | | +| | |\ \ \ \ +| | | * | | | +| | |/| | | | +| |/| | |/ / +| | | |/| | +| | | | | * +| |_|_|_|/ +|/| | | | +| | * | | +| |/ / / +* | | | +* | | | +| | * | +* | | | +* | | | +| * | | +| | * | +| * | | +* | | | +|\ \ \ \ +| * | | | +|/| | | | +| |/ / / +| * | | +| |\ \ \ +| | * | | +| |/| | | +| | |/ / +| | * | +| | |\ \ +| | | * | +| | |/| | +* | | | | +* | | | | +|\ \ \ \ \ +| * | | | | +|/| | | | | +| | * | | | +| | * | | | +| | * | | | +| |/ / / / +| * | | | +| |\ \ \ \ +| | * | | | +| |/| | | | +* | | | | | +* | | | | | +* | | | | | +* | | | | | +* | | | | | +| | | | * | +* | | | | | +|\ \ \ \ \ \ +| * | | | | | +|/| | | | | | +| | | | | * | +| | | | |/ / +* | | | | | +|\ \ \ \ \ \ +* | | | | | | +* | | | | | | +| | | | * | | +* | | | | | | +* | | | | | | +|\ \ \ \ \ \ \ +| | |_|_|/ / / +| |/| | | | | +| | | | * | | +| | | | * | | +| | | | * | | +| | | | * | | +| | | | * | | +| | | | * | | +| | | |/ / / +| | | * | | +| | | * | | +| | | * | | +| | |/| | | +| | | * | | +| | |/| | | +| | | |/ / +| | * | | +| |/| | | +| | | * | +| | |/ / +| | * | +| * | | +| |\ \ \ +| * | | | +| | * | | +| |/| | | +| | |/ / +| | * | +| | |\ \ +| | * | | +* | | | | +|\| | | | +| * | | | +| * | | | +| * | | | +| | * | | +| * | | | +| |\| | | +| * | | | +| | * | | +| | * | | +| * | | | +| * | | | +| * | | | +| * | | | +| * | | | +| * | | | +| * | | | +| * | | | +| | * | | +| * | | | +| * | | | +| * | | | +| * | | | +| | * | | +* | | | | +|\| | | | +| | * | | +| * | | | +| |\| | | +| | * | | +| | * | | +| | * | | +| | | * | +* | | | | +|\| | | | +| | * | | +| | |/ / +| * | | +| * | | +| |\| | +* | | | +|\| | | +| | * | +| | * | +| | * | +| * | | +| | * | +| * | | +| | * | +| | * | +| | * | +| * | | +| * | | +| * | | +| * | | +| * | | +| * | | +| * | | +* | | | +|\| | | +| * | | +| |\| | +| | * | +| | |\ \ +* | | | | +|\| | | | +| * | | | +| |\| | | +| | * | | +| | | * | +| | |/ / +* | | | +* | | | +|\| | | +| * | | +| |\| | +| | * | +| | * | +| | * | +| | | * +* | | | +|\| | | +| * | | +| * | | +| | | * +| | | |\ +* | | | | +| |_|_|/ +|/| | | +| * | | +| |\| | +| | * | +| | * | +| | * | +| | * | +| | * | +| * | | +* | | | +|\| | | +| * | | +|/| | | +| |/ / +| * | +| |\ \ +| * | | +| * | | +* | | | +|\| | | +| | * | +| * | | +| * | | +| * | | +* | | | +|\| | | +| * | | +| * | | +| | * | +| | |\ \ +| | |/ / +| |/| | +| * | | +* | | | +|\| | | +| * | | +* | | | +|\| | | +| * | | +| |\ \ \ +| * | | | +| * | | | +| | | * | +| * | | | +| * | | | +| | |/ / +| |/| | +| | * | +* | | | +|\| | | +| * | | +| * | | +| * | | +| * | | +| * | | +| |\ \ \ +* | | | | +|\| | | | +| * | | | +| * | | | +* | | | | +* | | | | +|\| | | | +| | | | * +| | | | |\ +| |_|_|_|/ +|/| | | | +| * | | | +* | | | | +* | | | | +|\| | | | +| * | | | +| |\ \ \ \ +| | | |/ / +| | |/| | +| * | | | +| * | | | +| * | | | +| * | | | +| | * | | +| | | * | +| | |/ / +| |/| | +* | | | +|\| | | +| * | | +| * | | +| * | | +| * | | +| * | | +* | | | +|\| | | +| * | | +| * | | +* | | | +| * | | +| * | | +| * | | +* | | | +* | | | +* | | | +|\| | | +| * | | +* | | | +* | | | +* | | | +* | | | +| | | * +* | | | +|\| | | +| * | | +| * | | +| * | | +` diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 8c078a0e825b0..3fe4c84eb6b40 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -99,8 +99,19 @@ func NewFuncMap() []template.FuncMap { "Subtract": base.Subtract, "EntryIcon": base.EntryIcon, "MigrationIcon": MigrationIcon, - "Add": func(a, b int) int { - return a + b + "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 }, "ActionIcon": ActionIcon, "DateFmtLong": func(t time.Time) string { @@ -437,6 +448,20 @@ func NewTextFuncMap() []texttmpl.FuncMap { } 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 + }, }} } diff --git a/routers/repo/commit.go b/routers/repo/commit.go index 004d4915df8bd..b3d5939e4d0de 100644 --- a/routers/repo/commit.go +++ b/routers/repo/commit.go @@ -90,6 +90,7 @@ func Commits(ctx *context.Context) { func Graph(ctx *context.Context) { ctx.Data["PageIsCommits"] = true ctx.Data["PageIsViewCode"] = true + ctx.Data["Mode"] = ctx.Query("mode") commitsCount, err := ctx.Repo.GetCommitsCount() if err != nil { @@ -105,7 +106,7 @@ func Graph(ctx *context.Context) { page := ctx.QueryInt("page") - graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page) + graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0) if err != nil { ctx.ServerError("GetCommitGraph", err) return @@ -116,7 +117,9 @@ func Graph(ctx *context.Context) { ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["CommitCount"] = commitsCount 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) } diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl index 493ac7a696c09..a00dfc3f35dc3 100644 --- a/templates/repo/graph.tmpl +++ b/templates/repo/graph.tmpl @@ -2,21 +2,38 @@
{{template "repo/header" .}}
-
-

{{.i18n.Tr "repo.commit_graph"}}

+
+

{{.i18n.Tr "repo.commit_graph"}} +
+ + +

- -
    - {{ range .Graph }} -
  • {{ .GraphAcii -}}
  • - {{ end }} -
-
+ + {{range $flowid, $flow := .Graph.Flows}} + + + {{range $flow.Commits}} + + {{end}} + + {{end}} +
    - {{ range .Graph }} -
  • + {{ range .Graph.Commits }} +
  • {{ if .OnlyRelation }} {{ else }} diff --git a/web_src/js/features/gitgraph.js b/web_src/js/features/gitgraph.js index 3e6b27436daf0..d53dd7fb7ba2d 100644 --- a/web_src/js/features/gitgraph.js +++ b/web_src/js/features/gitgraph.js @@ -1,568 +1,87 @@ -// Although inspired by the https://github.com/bluef/gitgraph.js/blob/master/gitgraph.js -// this has been completely rewritten with almost no remaining code - -// GitGraphCanvas is a canvas for drawing gitgraphs on to -class GitGraphCanvas { - constructor(canvas, widthUnits, heightUnits, config) { - this.ctx = canvas.getContext('2d'); - - const width = widthUnits * config.unitSize; - this.height = heightUnits * config.unitSize; - - const ratio = window.devicePixelRatio || 1; - - canvas.width = width * ratio; - canvas.height = this.height * ratio; - - canvas.style.width = `${width}px`; - canvas.style.height = `${this.height}px`; - - this.ctx.lineWidth = config.lineWidth; - this.ctx.lineJoin = 'round'; - this.ctx.lineCap = 'round'; - - this.ctx.scale(ratio, ratio); - this.config = config; - } - drawLine(moveX, moveY, lineX, lineY, color) { - this.ctx.strokeStyle = color; - this.ctx.beginPath(); - this.ctx.moveTo(moveX, moveY); - this.ctx.lineTo(lineX, lineY); - this.ctx.stroke(); - } - drawLineRight(x, y, color) { - this.drawLine( - x - 0.5 * this.config.unitSize, - y + this.config.unitSize / 2, - x + 0.5 * this.config.unitSize, - y + this.config.unitSize / 2, - color - ); - } - drawLineUp(x, y, color) { - this.drawLine( - x, - y + this.config.unitSize / 2, - x, - y - this.config.unitSize / 2, - color - ); - } - drawNode(x, y, color) { - this.ctx.strokeStyle = color; - - this.drawLineUp(x, y, color); - - this.ctx.beginPath(); - this.ctx.arc(x, y, this.config.nodeRadius, 0, Math.PI * 2, true); - this.ctx.fillStyle = color; - this.ctx.fill(); - } - drawLineIn(x, y, color) { - this.drawLine( - x + 0.5 * this.config.unitSize, - y + this.config.unitSize / 2, - x - 0.5 * this.config.unitSize, - y - this.config.unitSize / 2, - color - ); - } - drawLineOut(x, y, color) { - this.drawLine( - x - 0.5 * this.config.unitSize, - y + this.config.unitSize / 2, - x + 0.5 * this.config.unitSize, - y - this.config.unitSize / 2, - color - ); - } - drawSymbol(symbol, columnNumber, rowNumber, color) { - const y = this.height - this.config.unitSize * (rowNumber + 0.5); - const x = this.config.unitSize * 0.5 * (columnNumber + 1); - switch (symbol) { - case '-': - if (columnNumber % 2 === 1) { - this.drawLineRight(x, y, color); - } - break; - case '_': - this.drawLineRight(x, y, color); - break; - case '*': - this.drawNode(x, y, color); - break; - case '|': - this.drawLineUp(x, y, color); - break; - case '/': - this.drawLineOut(x, y, color); - break; - case '\\': - this.drawLineIn(x, y, color); - break; - case '.': - case ' ': - break; - default: - console.error('Unknown symbol', symbol, color); - } - } -} - -class GitGraph { - constructor(canvas, rawRows, config) { - this.rows = []; - let maxWidth = 0; - - for (let i = 0; i < rawRows.length; i++) { - const rowStr = rawRows[i]; - maxWidth = Math.max(rowStr.replace(/([_\s.-])/g, '').length, maxWidth); - - const rowArray = rowStr.split(''); - - this.rows.unshift(rowArray); - } - - this.currentFlows = []; - this.previousFlows = []; - - this.gitGraphCanvas = new GitGraphCanvas( - canvas, - maxWidth, - this.rows.length, - config - ); - } - - generateNewFlow(column) { - let newId; - - do { - newId = generateRandomColorString(); - } while (this.hasFlow(newId, column)); - - return {id: newId, color: `#${newId}`}; - } - - hasFlow(id, column) { - // We want to find the flow with the current ID - // Possible flows are those in the currentFlows - // Or flows in previousFlows[column-2:...] - for ( - let idx = column - 2 < 0 ? 0 : column - 2; - idx < this.previousFlows.length; - idx++ - ) { - if (this.previousFlows[idx] && this.previousFlows[idx].id === id) { - return true; - } - } - for (let idx = 0; idx < this.currentFlows.length; idx++) { - if (this.currentFlows[idx] && this.currentFlows[idx].id === id) { - return true; - } - } - return false; - } - - takePreviousFlow(column) { - if (column < this.previousFlows.length && this.previousFlows[column]) { - const flow = this.previousFlows[column]; - this.previousFlows[column] = null; - return flow; - } - return this.generateNewFlow(column); - } - - draw() { - if (this.rows.length === 0) { - return; - } - - this.currentFlows = new Array(this.rows[0].length); - - // Generate flows for the first row - I do not believe that this can contain '_', '-', '.' - for (let column = 0; column < this.rows[0].length; column++) { - if (this.rows[0][column] === ' ') { - continue; - } - this.currentFlows[column] = this.generateNewFlow(column); - } - - // Draw the first row - for (let column = 0; column < this.rows[0].length; column++) { - const symbol = this.rows[0][column]; - const color = this.currentFlows[column] ? this.currentFlows[column].color : ''; - this.gitGraphCanvas.drawSymbol(symbol, column, 0, color); - } - - for (let row = 1; row < this.rows.length; row++) { - // Done previous row - step up the row - const currentRow = this.rows[row]; - const previousRow = this.rows[row - 1]; - - this.previousFlows = this.currentFlows; - this.currentFlows = new Array(currentRow.length); - - // Set flows for this row - for (let column = 0; column < currentRow.length; column++) { - column = this.setFlowFor(column, currentRow, previousRow); - } - - // Draw this row - for (let column = 0; column < currentRow.length; column++) { - const symbol = currentRow[column]; - const color = this.currentFlows[column] ? this.currentFlows[column].color : ''; - this.gitGraphCanvas.drawSymbol(symbol, column, row, color); - } - } - } - - setFlowFor(column, currentRow, previousRow) { - const symbol = currentRow[column]; - switch (symbol) { - case '|': - case '*': - return this.setUpFlow(column, currentRow, previousRow); - case '/': - return this.setOutFlow(column, currentRow, previousRow); - case '\\': - return this.setInFlow(column, currentRow, previousRow); - case '_': - return this.setRightFlow(column, currentRow, previousRow); - case '-': - return this.setLeftFlow(column, currentRow, previousRow); - case ' ': - // In space no one can hear you flow ... (?) - return column; - default: - // Unexpected so let's generate a new flow and wait for bug-reports - this.currentFlows[column] = this.generateNewFlow(column); - return column; - } - } - - // setUpFlow handles '|' or '*' - returns the last column that was set - // generally we prefer to take the left most flow from the previous row - setUpFlow(column, currentRow, previousRow) { - // If ' |/' or ' |_' - // '/|' '/|' -> Take the '|' flow directly beneath us - if ( - column + 1 < currentRow.length && - (currentRow[column + 1] === '/' || currentRow[column + 1] === '_') && - column < previousRow.length && - (previousRow[column] === '|' || previousRow[column] === '*') && - previousRow[column - 1] === '/' - ) { - this.currentFlows[column] = this.takePreviousFlow(column); - return column; - } - - // If ' |/' or ' |_' - // '/ ' '/ ' -> Take the '/' flow from the preceding column - if ( - column + 1 < currentRow.length && - (currentRow[column + 1] === '/' || currentRow[column + 1] === '_') && - column - 1 < previousRow.length && - previousRow[column - 1] === '/' - ) { - this.currentFlows[column] = this.takePreviousFlow(column - 1); - return column; - } - - // If ' |' - // '/' -> Take the '/' flow - (we always prefer the left-most flow) - if ( - column > 0 && - column - 1 < previousRow.length && - previousRow[column - 1] === '/' - ) { - this.currentFlows[column] = this.takePreviousFlow(column - 1); - return column; - } - - // If '|' OR '|' take the '|' flow - // '|' '*' - if ( - column < previousRow.length && - (previousRow[column] === '|' || previousRow[column] === '*') - ) { - this.currentFlows[column] = this.takePreviousFlow(column); - return column; - } - - // If '| ' keep the '\' flow - // ' \' - if (column + 1 < previousRow.length && previousRow[column + 1] === '\\') { - this.currentFlows[column] = this.takePreviousFlow(column + 1); - return column; - } - - // Otherwise just create a new flow - probably this is an error... - this.currentFlows[column] = this.generateNewFlow(column); - return column; - } - - // setOutFlow handles '/' - returns the last column that was set - // generally we prefer to take the left most flow from the previous row - setOutFlow(column, currentRow, previousRow) { - // If '_/' -> keep the '_' flow - if (column > 0 && currentRow[column - 1] === '_') { - this.currentFlows[column] = this.currentFlows[column - 1]; - return column; - } - - // If '_|/' -> keep the '_' flow - if ( - column > 1 && - (currentRow[column - 1] === '|' || currentRow[column - 1] === '*') && - currentRow[column - 2] === '_' - ) { - this.currentFlows[column] = this.currentFlows[column - 2]; - return column; - } - - // If '|/' - // '/' -> take the '/' flow (if it is still available) - if ( - column > 1 && - currentRow[column - 1] === '|' && - column - 2 < previousRow.length && - previousRow[column - 2] === '/' - ) { - this.currentFlows[column] = this.takePreviousFlow(column - 2); - return column; - } - - // If ' /' - // '/' -> take the '/' flow, but transform the symbol to '|' due to our spacing - // This should only happen if there are 3 '/' - in a row so we don't need to be cleverer here - if ( - column > 0 && - currentRow[column - 1] === ' ' && - column - 1 < previousRow.length && - previousRow[column - 1] === '/' - ) { - this.currentFlows[column] = this.takePreviousFlow(column - 1); - currentRow[column] = '|'; - return column; - } - - // If ' /' - // '|' -> take the '|' flow - if ( - column > 0 && - currentRow[column - 1] === ' ' && - column - 1 < previousRow.length && - (previousRow[column - 1] === '|' || previousRow[column - 1] === '*') - ) { - this.currentFlows[column] = this.takePreviousFlow(column - 1); - return column; - } - - // If '/' <- Not sure this ever happens... but take the '\' flow - // '\' - if (column < previousRow.length && previousRow[column] === '\\') { - this.currentFlows[column] = this.takePreviousFlow(column); - return column; - } - - // Otherwise just generate a new flow and wait for bug-reports... - this.currentFlows[column] = this.generateNewFlow(column); - return column; - } - - // setInFlow handles '\' - returns the last column that was set - // generally we prefer to take the left most flow from the previous row - setInFlow(column, currentRow, previousRow) { - // If '\?' - // '/?' -> take the '/' flow - if (column < previousRow.length && previousRow[column] === '/') { - this.currentFlows[column] = this.takePreviousFlow(column); - return column; - } - - // If '\?' - // ' \' -> take the '\' flow and reassign to '|' - // This should only happen if there are 3 '\' - in a row so we don't need to be cleverer here - if (column + 1 < previousRow.length && previousRow[column + 1] === '\\') { - this.currentFlows[column] = this.takePreviousFlow(column + 1); - currentRow[column] = '|'; - return column; - } - - // If '\?' - // ' |' -> take the '|' flow - if ( - column + 1 < previousRow.length && - (previousRow[column + 1] === '|' || previousRow[column + 1] === '*') - ) { - this.currentFlows[column] = this.takePreviousFlow(column + 1); - return column; - } - - // Otherwise just generate a new flow and wait for bug-reports if we're wrong... - this.currentFlows[column] = this.generateNewFlow(column); - return column; - } - - // setRightFlow handles '_' - returns the last column that was set - // generally we prefer to take the left most flow from the previous row - setRightFlow(column, currentRow, previousRow) { - // if '__' keep the '_' flow - if (column > 0 && currentRow[column - 1] === '_') { - this.currentFlows[column] = this.currentFlows[column - 1]; - return column; - } - - // if '_|_' -> keep the '_' flow - if ( - column > 1 && - currentRow[column - 1] === '|' && - currentRow[column - 2] === '_' - ) { - this.currentFlows[column] = this.currentFlows[column - 2]; - return column; - } - - // if ' _' -> take the '/' flow - // '/ ' - if ( - column > 0 && - column - 1 < previousRow.length && - previousRow[column - 1] === '/' - ) { - this.currentFlows[column] = this.takePreviousFlow(column - 1); - return column; - } - - // if ' |_' - // '/? ' -> take the '/' flow (this may cause generation...) - // we can do this because we know that git graph - // doesn't create compact graphs like: ' |_' - // '//' - if ( - column > 1 && - column - 2 < previousRow.length && - previousRow[column - 2] === '/' - ) { - this.currentFlows[column] = this.takePreviousFlow(column - 2); - return column; - } - - // There really shouldn't be another way of doing this - generate and wait for bug-reports... - - this.currentFlows[column] = this.generateNewFlow(column); - return column; - } - - // setLeftFlow handles '----.' - returns the last column that was set - // generally we prefer to take the left most flow from the previous row that terminates this left recursion - setLeftFlow(column, currentRow, previousRow) { - // This is: '----------.' or the like - // ' \ \ /|\' - - // Find the end of the '-' or nearest '/|\' in the previousRow : - let originalColumn = column; - let flow; - for (; column < currentRow.length && currentRow[column] === '-'; column++) { - if (column > 0 && column - 1 < previousRow.length && previousRow[column - 1] === '/') { - flow = this.takePreviousFlow(column - 1); - break; - } else if (column < previousRow.length && previousRow[column] === '|') { - flow = this.takePreviousFlow(column); - break; - } else if ( - column + 1 < previousRow.length && - previousRow[column + 1] === '\\' - ) { - flow = this.takePreviousFlow(column + 1); - break; - } - } - - // if we have a flow then we found a '/|\' in the previousRow - if (flow) { - for (; originalColumn < column + 1; originalColumn++) { - this.currentFlows[originalColumn] = flow; - } - return column; - } - - // If the symbol in the column is not a '.' then there's likely an error - if (currentRow[column] !== '.') { - // It really should end in a '.' but this one doesn't... - // 1. Step back - we don't want to eat this column - column--; - // 2. Generate a new flow and await bug-reports... - this.currentFlows[column] = this.generateNewFlow(column); - - // 3. Assign all of the '-' to the same flow. - for (; originalColumn < column; originalColumn++) { - this.currentFlows[originalColumn] = this.currentFlows[column]; - } - return column; - } - - // We have a terminal '.' eg. the current row looks like '----.' - // the previous row should look like one of '/|\' eg. ' \' - if (column > 0 && column - 1 < previousRow.length && previousRow[column - 1] === '/') { - flow = this.takePreviousFlow(column - 1); - } else if (column < previousRow.length && previousRow[column] === '|') { - flow = this.takePreviousFlow(column); - } else if ( - column + 1 < previousRow.length && - previousRow[column + 1] === '\\' - ) { - flow = this.takePreviousFlow(column + 1); +export default async function initGitGraph() { + const graphContainer = document.getElementById('git-graph-container'); + if (!graphContainer) return; + + $('#flow-color-monochrome').click(() => { + $('#flow-color-monochrome').addClass('selected'); + $('#flow-color-colored').removeClass('selected'); + $('#git-graph-container').removeClass('colored').addClass('monochrome'); + const params = new URLSearchParams(window.location.search); + params.set('mode', 'monochrome'); + const queryString = params.toString(); + if (queryString) { + window.history.replaceState({}, '', `?${queryString}`); } else { - // Again unexpected so let's generate and wait the bug-report - flow = this.generateNewFlow(column); - } - - // Assign all of the rest of the ----. to this flow. - for (; originalColumn < column + 1; originalColumn++) { - this.currentFlows[originalColumn] = flow; + window.history.replaceState({}, '', window.location.pathname); + } + $('.pagination a').each((_, that) => { + const href = $(that).attr('href'); + if (!href) return; + const url = new URL(href, window.location); + const params = url.searchParams; + params.set('mode', 'monochrome'); + url.search = `?${params.toString()}`; + $(that).attr('href', url.href); + }); + }); + $('#flow-color-colored').click(() => { + $('#flow-color-colored').addClass('selected'); + $('#flow-color-monochrome').removeClass('selected'); + $('#git-graph-container').addClass('colored').removeClass('monochrome'); + $('.pagination a').each((_, that) => { + const href = $(that).attr('href'); + if (!href) return; + const url = new URL(href, window.location); + const params = url.searchParams; + params.delete('mode'); + url.search = `?${params.toString()}`; + $(that).attr('href', url.href); + }); + const params = new URLSearchParams(window.location.search); + params.delete('mode'); + const queryString = params.toString(); + if (queryString) { + window.history.replaceState({}, '', `?${queryString}`); + } else { + window.history.replaceState({}, '', window.location.pathname); } - - return column; - } -} - -function generateRandomColorString() { - const chars = '0123456789ABCDEF'; - const stringLength = 6; - let randomString = '', - rnum, - i; - for (i = 0; i < stringLength; i++) { - rnum = Math.floor(Math.random() * chars.length); - randomString += chars.substring(rnum, rnum + 1); - } - - return randomString; -} - -export default async function initGitGraph() { - const graphCanvas = document.getElementById('graph-canvas'); - if (!graphCanvas || !graphCanvas.getContext) return; - - // Grab the raw graphList - const graphList = []; - $('#graph-raw-list li span.node-relation').each(function () { - graphList.push($(this).text()); }); - - // Define some drawing parameters - const config = { - unitSize: 20, - lineWidth: 3, - nodeRadius: 4 - }; - - - const gitGraph = new GitGraph(graphCanvas, graphList, config); - gitGraph.draw(); - graphCanvas.closest('#git-graph-container').classList.add('in'); + $('#git-graph-container #rev-list li').hover( + (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'); + }, + (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 #rel-container .flow-group').hover( + (e) => { + $(e.currentTarget).addClass('highlight'); + const flow = $(e.currentTarget).data('flow'); + $(`#rev-list li[data-flow='${flow}']`).addClass('highlight'); + }, + (e) => { + $(e.currentTarget).removeClass('highlight'); + const flow = $(e.currentTarget).data('flow'); + $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight'); + }, + ); + $('#git-graph-container #rel-container .flow-commit').hover( + (e) => { + const rev = $(e.currentTarget).data('rev'); + $(`#rev-list li#commit-${rev}`).addClass('hover'); + }, + (e) => { + const rev = $(e.currentTarget).data('rev'); + $(`#rev-list li#commit-${rev}`).removeClass('hover'); + }, + ); } diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index e539f7475cd7f..5685889c7487f 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -3083,9 +3083,187 @@ tbody.commit-list { } #git-graph-container { - display: none; + display: block; } -#git-graph-container.in { - display: block; +#git-graph-container.monochrome #rel-container .flow-group { + stroke: grey; + fill: grey; +} + +#git-graph-container.monochrome #rel-container .flow-group.highlight { + stroke: black; + fill: black; +} + +#git-graph-container:not(.monochrome) #rel-container .flow-group { + &.flow-color-16-1 { + stroke: #ed5a8b; + fill: #ed5a8b; + } + + &.flow-color-16-2 { + stroke: #66e049; + fill: #66e049; + } + + &.flow-color-16-3 { + stroke: #db62d6; + fill: #db62d6; + } + + &.flow-color-16-4 { + stroke: #b7e83e; + fill: #b7e83e; + } + + &.flow-color-16-5 { + stroke: #8683ee; + fill: #8683ee; + } + + &.flow-color-16-6 { + stroke: #e1d037; + fill: #e1d037; + } + + &.flow-color-16-7 { + stroke: #4be9b7; + fill: #4be9b7; + } + + &.flow-color-16-8 { + stroke: #ea6542; + fill: #ea6542; + } + + &.flow-color-16-9 { + stroke: #57e185; + fill: #57e185; + } + + &.flow-color-16-10 { + stroke: #db9333; + fill: #db9333; + } + + &.flow-color-16-11 { + stroke: #42af69; + fill: #42af69; + } + + &.flow-color-16-12 { + stroke: #afa63a; + fill: #afa63a; + } + + &.flow-color-16-13 { + stroke: #95e57b; + fill: #95e57b; + } + + &.flow-color-16-14 { + stroke: #d4e572; + fill: #d4e572; + } + + &.flow-color-16-15 { + stroke: #4da841; + fill: #4da841; + } + + &.flow-color-16-0 { + stroke: #7db233; + fill: #7db233; + } +} + +#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight { + &.flow-color-16-1 { + stroke: #ce4751; + fill: #ce4751; + } + + &.flow-color-16-2 { + stroke: #499a37; + fill: #499a37; + } + + &.flow-color-16-3 { + stroke: #a846e5; + fill: #a846e5; + } + + &.flow-color-16-4 { + stroke: #8f9121; + fill: #8f9121; + } + + &.flow-color-16-5 { + stroke: #5d45d1; + fill: #5d45d1; + } + + &.flow-color-16-6 { + stroke: #c67d28; + fill: #c67d28; + } + + &.flow-color-16-7 { + stroke: #5d7ce2; + fill: #5d7ce2; + } + + &.flow-color-16-8 { + stroke: #e33a28; + fill: #e33a28; + } + + &.flow-color-16-9 { + stroke: #5742a1; + fill: #5742a1; + } + + &.flow-color-16-10 { + stroke: #c45327; + fill: #c45327; + } + + &.flow-color-16-11 { + stroke: #872ea6; + fill: #872ea6; + } + + &.flow-color-16-12 { + stroke: #af2f63; + fill: #af2f63; + } + + &.flow-color-16-13 { + stroke: #d747ce; + fill: #d747ce; + } + + &.flow-color-16-14 { + stroke: #e64996; + fill: #e64996; + } + + &.flow-color-16-15 { + stroke: #bf6fce; + fill: #bf6fce; + } + + &.flow-color-16-0 { + stroke: #982e85; + fill: #982e85; + } +} + +#git-graph-container #rev-list li.hover { + font-weight: bold; +} + +#git-graph-container #rev-list li.highlight { + background-color: #fcf8e9; } diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index 49d9821cdd9d3..fc7034201dcb5 100644 --- a/web_src/less/themes/theme-arc-green.less +++ b/web_src/less/themes/theme-arc-green.less @@ -1898,3 +1898,187 @@ footer .container .links > * { .repository .repo-header .ui.huge.breadcrumb.repo-title .repo-header-icon .avatar { 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-1 { + stroke: #ce4751; + fill: #ce4751; + } + + &.flow-color-16-2 { + stroke: #499a37; + fill: #499a37; + } + + &.flow-color-16-3 { + stroke: #a846e5; + fill: #a846e5; + } + + &.flow-color-16-4 { + stroke: #8f9121; + fill: #8f9121; + } + + &.flow-color-16-5 { + stroke: #5d45d1; + fill: #5d45d1; + } + + &.flow-color-16-6 { + stroke: #c67d28; + fill: #c67d28; + } + + &.flow-color-16-7 { + stroke: #5d7ce2; + fill: #5d7ce2; + } + + &.flow-color-16-8 { + stroke: #e33a28; + fill: #e33a28; + } + + &.flow-color-16-9 { + stroke: #5742a1; + fill: #5742a1; + } + + &.flow-color-16-10 { + stroke: #c45327; + fill: #c45327; + } + + &.flow-color-16-11 { + stroke: #872ea6; + fill: #872ea6; + } + + &.flow-color-16-12 { + stroke: #af2f63; + fill: #af2f63; + } + + &.flow-color-16-13 { + stroke: #d747ce; + fill: #d747ce; + } + + &.flow-color-16-14 { + stroke: #e64996; + fill: #e64996; + } + + &.flow-color-16-15 { + stroke: #bf6fce; + fill: #bf6fce; + } + + &.flow-color-16-0 { + stroke: #982e85; + fill: #982e85; + } +} + +#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight { + &.flow-color-16-1 { + stroke: #ed5a8b; + fill: #ed5a8b; + } + + &.flow-color-16-2 { + stroke: #66e049; + fill: #66e049; + } + + &.flow-color-16-3 { + stroke: #db62d6; + fill: #db62d6; + } + + &.flow-color-16-4 { + stroke: #b7e83e; + fill: #b7e83e; + } + + &.flow-color-16-5 { + stroke: #8683ee; + fill: #8683ee; + } + + &.flow-color-16-6 { + stroke: #e1d037; + fill: #e1d037; + } + + &.flow-color-16-7 { + stroke: #4be9b7; + fill: #4be9b7; + } + + &.flow-color-16-8 { + stroke: #ea6542; + fill: #ea6542; + } + + &.flow-color-16-9 { + stroke: #57e185; + fill: #57e185; + } + + &.flow-color-16-10 { + stroke: #db9333; + fill: #db9333; + } + + &.flow-color-16-11 { + stroke: #42af69; + fill: #42af69; + } + + &.flow-color-16-12 { + stroke: #afa63a; + fill: #afa63a; + } + + &.flow-color-16-13 { + stroke: #95e57b; + fill: #95e57b; + } + + &.flow-color-16-14 { + stroke: #d4e572; + fill: #d4e572; + } + + &.flow-color-16-15 { + stroke: #4da841; + fill: #4da841; + } + + &.flow-color-16-0 { + stroke: #7db233; + fill: #7db233; + } +} + +#git-graph-container #rev-list li.highlight { + background-color: #39423e; +} + +#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; +} From 667994371f30da39b5957175142439f6ff254b5b Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 27 Jul 2020 16:45:28 +0100 Subject: [PATCH 02/11] Fix test Signed-off-by: Andrew Thornton --- modules/gitgraph/graph_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/gitgraph/graph_test.go b/modules/gitgraph/graph_test.go index 7c95cf02d3dfc..89855543cdf11 100644 --- a/modules/gitgraph/graph_test.go +++ b/modules/gitgraph/graph_test.go @@ -238,8 +238,8 @@ func TestParseGlyphs(t *testing.T) { } row++ } - if len(parser.availableColors) != 11 { - t.Errorf("Expected 11 colors but have %d", len(parser.availableColors)) + if len(parser.availableColors) != 9 { + t.Errorf("Expected 9 colors but have %d", len(parser.availableColors)) } } From a8b5684e2b3e33f403cd824b82e083e10d3596a6 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 27 Jul 2020 18:39:32 +0100 Subject: [PATCH 03/11] more color changes Signed-off-by: Andrew Thornton --- options/locale/locale_en-US.ini | 2 + routers/repo/commit.go | 6 +- templates/repo/graph.tmpl | 20 +++-- web_src/js/features/gitgraph.js | 8 +- web_src/less/_repository.less | 127 ++++++++++++++++---------------- 5 files changed, 89 insertions(+), 74 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 7968d00b58276..e316f943f6b58 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -775,6 +775,8 @@ audio_not_supported_in_browser = Your browser does not support the HTML5 'audio' stored_lfs = Stored with Git LFS symbolic_link = Symbolic link commit_graph = Commit Graph +commit_graph.monochrome = Mono +commit_graph.color = Color blame = Blame normal_view = Normal View line = line diff --git a/routers/repo/commit.go b/routers/repo/commit.go index b3d5939e4d0de..d9547cc51de62 100644 --- a/routers/repo/commit.go +++ b/routers/repo/commit.go @@ -90,7 +90,11 @@ func Commits(ctx *context.Context) { func Graph(ctx *context.Context) { ctx.Data["PageIsCommits"] = true ctx.Data["PageIsViewCode"] = true - ctx.Data["Mode"] = ctx.Query("mode") + mode := strings.ToLower(ctx.QueryTrim("mode")) + if mode != "monochrome" { + mode = "color" + } + ctx.Data["Mode"] = mode commitsCount, err := ctx.Repo.GetCommitsCount() if err != nil { diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl index a00dfc3f35dc3..d760cb1c82ecc 100644 --- a/templates/repo/graph.tmpl +++ b/templates/repo/graph.tmpl @@ -3,13 +3,17 @@ {{template "repo/header" .}}
    -

    {{.i18n.Tr "repo.commit_graph"}} -
    - - -

    +

    {{.i18n.Tr "repo.commit_graph"}} +
    +
    + + +
    +
    +

    +
    - + {{range $flowid, $flow := .Graph.Flows}} diff --git a/web_src/js/features/gitgraph.js b/web_src/js/features/gitgraph.js index d53dd7fb7ba2d..418de50a36c1e 100644 --- a/web_src/js/features/gitgraph.js +++ b/web_src/js/features/gitgraph.js @@ -3,8 +3,8 @@ export default async function initGitGraph() { if (!graphContainer) return; $('#flow-color-monochrome').click(() => { - $('#flow-color-monochrome').addClass('selected'); - $('#flow-color-colored').removeClass('selected'); + $('#flow-color-monochrome').addClass('active'); + $('#flow-color-colored').removeClass('active'); $('#git-graph-container').removeClass('colored').addClass('monochrome'); const params = new URLSearchParams(window.location.search); params.set('mode', 'monochrome'); @@ -25,8 +25,8 @@ export default async function initGitGraph() { }); }); $('#flow-color-colored').click(() => { - $('#flow-color-colored').addClass('selected'); - $('#flow-color-monochrome').removeClass('selected'); + $('#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'); diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 5685889c7487f..1b3a96185550f 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -3084,6 +3084,9 @@ tbody.commit-list { #git-graph-container { display: block; + .ui.header.dividing { + padding-bottom: 10px; + } } #git-graph-container.monochrome #rel-container .flow-group { @@ -3096,112 +3099,112 @@ tbody.commit-list { fill: black; } -#git-graph-container:not(.monochrome) #rel-container .flow-group { +#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight { &.flow-color-16-1 { - stroke: #ed5a8b; - fill: #ed5a8b; + stroke: #5ac144; + fill: #5ac144; } &.flow-color-16-2 { - stroke: #66e049; - fill: #66e049; + stroke: #ed5a8b; + fill: #ed5a8b; } &.flow-color-16-3 { - stroke: #db62d6; - fill: #db62d6; + stroke: #ced049; + fill: #ced048; } &.flow-color-16-4 { - stroke: #b7e83e; - fill: #b7e83e; + stroke: #db61d7; + fill: #db62d6; } &.flow-color-16-5 { - stroke: #8683ee; - fill: #8683ee; + stroke: #4e33d1; + fill: #4f35d1; } &.flow-color-16-6 { - stroke: #e1d037; - fill: #e1d037; + stroke: #e6a151; + fill: #e6a151; } &.flow-color-16-7 { - stroke: #4be9b7; - fill: #4be9b7; + stroke: #44daaa; + fill: #44daaa; } &.flow-color-16-8 { - stroke: #ea6542; - fill: #ea6542; + stroke: #dd7a5c; + fill: #dd7a5c; } &.flow-color-16-9 { - stroke: #57e185; - fill: #57e185; + stroke: #3e61a3; + fill: #3d60a1; } &.flow-color-16-10 { - stroke: #db9333; - fill: #db9333; + stroke: #d95520; + fill: #d95520; } &.flow-color-16-11 { - stroke: #42af69; - fill: #42af69; + stroke: #42ae68; + fill: #42ae68; } &.flow-color-16-12 { - stroke: #afa63a; - fill: #afa63a; + stroke: #9126b5; + fill: #9126b5; } &.flow-color-16-13 { - stroke: #95e57b; - fill: #95e57b; + stroke: #4ab080; + fill: #4ab080; } &.flow-color-16-14 { - stroke: #d4e572; - fill: #d4e572; + stroke: #284fb8; + fill: #284fb8; } &.flow-color-16-15 { - stroke: #4da841; - fill: #4da841; + stroke: #971c80; + fill: #971c80; } &.flow-color-16-0 { - stroke: #7db233; - fill: #7db233; + stroke: #87ca28; + fill: #87ca28; } } -#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight { +#git-graph-container:not(.monochrome) #rel-container .flow-group { &.flow-color-16-1 { - stroke: #ce4751; - fill: #ce4751; + stroke: #499a37; + fill: #499a37; } &.flow-color-16-2 { - stroke: #499a37; - fill: #499a37; + stroke: hsl(356, 58%, 54%); + fill: #ce4751; } &.flow-color-16-3 { - stroke: #a846e5; - fill: #a846e5; + stroke: #8f9121; + fill: #8f9121; } &.flow-color-16-4 { - stroke: #8f9121; - fill: #8f9121; + stroke: #ac32a6; + fill: #ac32a6; } &.flow-color-16-5 { - stroke: #5d45d1; - fill: #5d45d1; + stroke: #3d27aa; + fill: #3d27aa; } &.flow-color-16-6 { @@ -3210,18 +3213,18 @@ tbody.commit-list { } &.flow-color-16-7 { - stroke: #5d7ce2; - fill: #5d7ce2; + stroke: #4db392; + fill: #4db392; } &.flow-color-16-8 { - stroke: #e33a28; - fill: #e33a28; + stroke: #aa4d30; + fill: #aa4d30; } &.flow-color-16-9 { - stroke: #5742a1; - fill: #5742a1; + stroke: #2a4984; + fill: #2a4984; } &.flow-color-16-10 { @@ -3230,33 +3233,33 @@ tbody.commit-list { } &.flow-color-16-11 { - stroke: #872ea6; - fill: #872ea6; + stroke: #3d965c; + fill: #3d965c; } &.flow-color-16-12 { - stroke: #af2f63; - fill: #af2f63; + stroke: #792a93; + fill: #792a93; } &.flow-color-16-13 { - stroke: #d747ce; - fill: #d747ce; + stroke: #439d73; + fill: #439d73; } &.flow-color-16-14 { - stroke: #e64996; - fill: #e64996; + stroke: #103aad; + fill: #103aad; } &.flow-color-16-15 { - stroke: #bf6fce; - fill: #bf6fce; + stroke: #982e85; + fill: #982e85; } &.flow-color-16-0 { - stroke: #982e85; - fill: #982e85; + stroke: #7db233; + fill: #7db233; } } From df4985eb07ec8e8da0fc2acb8a9e4f864066ee86 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 27 Jul 2020 18:46:02 +0100 Subject: [PATCH 04/11] slightly further differentiate flow-9 Signed-off-by: Andrew Thornton --- web_src/less/_repository.less | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 1b3a96185550f..c1d89de1958d7 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -3141,8 +3141,8 @@ tbody.commit-list { } &.flow-color-16-9 { - stroke: #3e61a3; - fill: #3d60a1; + stroke: #38859c; + fill: #38859c; } &.flow-color-16-10 { @@ -3223,8 +3223,8 @@ tbody.commit-list { } &.flow-color-16-9 { - stroke: #2a4984; - fill: #2a4984; + stroke: #2a6f84; + fill: #2a6f84; } &.flow-color-16-10 { From 1a285f2bbf77998a9699ffd3f4905e3505c55e84 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 27 Jul 2020 20:15:42 +0100 Subject: [PATCH 05/11] as per @silverwind Signed-off-by: Andrew Thornton --- web_src/js/features/gitgraph.js | 74 +++++++++++++++------------------ 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/web_src/js/features/gitgraph.js b/web_src/js/features/gitgraph.js index 418de50a36c1e..655cfb77c2228 100644 --- a/web_src/js/features/gitgraph.js +++ b/web_src/js/features/gitgraph.js @@ -2,7 +2,7 @@ export default async function initGitGraph() { const graphContainer = document.getElementById('git-graph-container'); if (!graphContainer) return; - $('#flow-color-monochrome').click(() => { + $('#flow-color-monochrome').on('click', () => { $('#flow-color-monochrome').addClass('active'); $('#flow-color-colored').removeClass('active'); $('#git-graph-container').removeClass('colored').addClass('monochrome'); @@ -24,7 +24,7 @@ export default async function initGitGraph() { $(that).attr('href', url.href); }); }); - $('#flow-color-colored').click(() => { + $('#flow-color-colored').on('click', () => { $('#flow-color-colored').addClass('active'); $('#flow-color-monochrome').removeClass('active'); $('#git-graph-container').addClass('colored').removeClass('monochrome'); @@ -46,42 +46,36 @@ export default async function initGitGraph() { window.history.replaceState({}, '', window.location.pathname); } }); - $('#git-graph-container #rev-list li').hover( - (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'); - }, - (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 #rel-container .flow-group').hover( - (e) => { - $(e.currentTarget).addClass('highlight'); - const flow = $(e.currentTarget).data('flow'); - $(`#rev-list li[data-flow='${flow}']`).addClass('highlight'); - }, - (e) => { - $(e.currentTarget).removeClass('highlight'); - const flow = $(e.currentTarget).data('flow'); - $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight'); - }, - ); - $('#git-graph-container #rel-container .flow-commit').hover( - (e) => { - const rev = $(e.currentTarget).data('rev'); - $(`#rev-list li#commit-${rev}`).addClass('hover'); - }, - (e) => { - const rev = $(e.currentTarget).data('rev'); - $(`#rev-list li#commit-${rev}`).removeClass('hover'); - }, - ); + $('#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'); + }); } From 5d0ba5c19301458adb9f03ea90d66bdb7dc07b3e Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 27 Jul 2020 20:48:38 +0100 Subject: [PATCH 06/11] fix-up color-5 on the dark theme Signed-off-by: Andrew Thornton --- web_src/less/themes/theme-arc-green.less | 158 +---------------------- 1 file changed, 4 insertions(+), 154 deletions(-) diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index a6f984d268ec4..26a95c092e7c1 100644 --- a/web_src/less/themes/theme-arc-green.less +++ b/web_src/less/themes/theme-arc-green.less @@ -1910,166 +1910,16 @@ footer .container .links > * { } #git-graph-container:not(.monochrome) #rel-container .flow-group { - &.flow-color-16-1 { - stroke: #ce4751; - fill: #ce4751; - } - - &.flow-color-16-2 { - stroke: #499a37; - fill: #499a37; - } - - &.flow-color-16-3 { - stroke: #a846e5; - fill: #a846e5; - } - - &.flow-color-16-4 { - stroke: #8f9121; - fill: #8f9121; - } - &.flow-color-16-5 { - stroke: #5d45d1; - fill: #5d45d1; - } - - &.flow-color-16-6 { - stroke: #c67d28; - fill: #c67d28; - } - - &.flow-color-16-7 { - stroke: #5d7ce2; - fill: #5d7ce2; - } - - &.flow-color-16-8 { - stroke: #e33a28; - fill: #e33a28; - } - - &.flow-color-16-9 { - stroke: #5742a1; - fill: #5742a1; - } - - &.flow-color-16-10 { - stroke: #c45327; - fill: #c45327; - } - - &.flow-color-16-11 { - stroke: #872ea6; - fill: #872ea6; - } - - &.flow-color-16-12 { - stroke: #af2f63; - fill: #af2f63; - } - - &.flow-color-16-13 { - stroke: #d747ce; - fill: #d747ce; - } - - &.flow-color-16-14 { - stroke: #e64996; - fill: #e64996; - } - - &.flow-color-16-15 { - stroke: #bf6fce; - fill: #bf6fce; - } - - &.flow-color-16-0 { - stroke: #982e85; - fill: #982e85; + stroke: #5543b1; + fill: #5543b1; } } #git-graph-container:not(.monochrome) #rel-container .flow-group.highlight { - &.flow-color-16-1 { - stroke: #ed5a8b; - fill: #ed5a8b; - } - - &.flow-color-16-2 { - stroke: #66e049; - fill: #66e049; - } - - &.flow-color-16-3 { - stroke: #db62d6; - fill: #db62d6; - } - - &.flow-color-16-4 { - stroke: #b7e83e; - fill: #b7e83e; - } - &.flow-color-16-5 { - stroke: #8683ee; - fill: #8683ee; - } - - &.flow-color-16-6 { - stroke: #e1d037; - fill: #e1d037; - } - - &.flow-color-16-7 { - stroke: #4be9b7; - fill: #4be9b7; - } - - &.flow-color-16-8 { - stroke: #ea6542; - fill: #ea6542; - } - - &.flow-color-16-9 { - stroke: #57e185; - fill: #57e185; - } - - &.flow-color-16-10 { - stroke: #db9333; - fill: #db9333; - } - - &.flow-color-16-11 { - stroke: #42af69; - fill: #42af69; - } - - &.flow-color-16-12 { - stroke: #afa63a; - fill: #afa63a; - } - - &.flow-color-16-13 { - stroke: #95e57b; - fill: #95e57b; - } - - &.flow-color-16-14 { - stroke: #d4e572; - fill: #d4e572; - } - - &.flow-color-16-15 { - stroke: #4da841; - fill: #4da841; - } - - &.flow-color-16-0 { - stroke: #7db233; - fill: #7db233; + stroke: #7058e6; + fill: #7058e6; } } From b639a6140ebe4cec229b46ff7c9f85cc5ffc06dd Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 27 Jul 2020 20:58:03 +0100 Subject: [PATCH 07/11] fix icon hover in dark theme Signed-off-by: Andrew Thornton --- web_src/less/themes/theme-arc-green.less | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index 26a95c092e7c1..cbbe2346027f6 100644 --- a/web_src/less/themes/theme-arc-green.less +++ b/web_src/less/themes/theme-arc-green.less @@ -916,11 +916,17 @@ a.ui.basic.green.label:hover { .ui.active.button:active, .ui.button:active, -.ui.button:focus { +.ui.button:focus, +.ui.active.button { background-color: #2e3e4e; color: #dbdbdb; } +.ui.active.button:hover { + background-color: #475e75; + color: #dbdbdb; +} + .ui.dropdown .menu .selected.item, .ui.dropdown.selected { color: #dbdbdb; From ae6e8d0a3121247cbaa41dc568291e528d15c3dd Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 27 Jul 2020 22:02:56 +0100 Subject: [PATCH 08/11] Clean-up graph.go a little to make things clearer Signed-off-by: Andrew Thornton --- modules/gitgraph/graph.go | 486 +------------------------------ modules/gitgraph/graph_models.go | 186 ++++++++++++ modules/gitgraph/graph_test.go | 28 +- modules/gitgraph/parser.go | 338 +++++++++++++++++++++ 4 files changed, 543 insertions(+), 495 deletions(-) create mode 100644 modules/gitgraph/graph_models.go create mode 100644 modules/gitgraph/parser.go diff --git a/modules/gitgraph/graph.go b/modules/gitgraph/graph.go index 7bbab51222bff..257e4f3af0de5 100644 --- a/modules/gitgraph/graph.go +++ b/modules/gitgraph/graph.go @@ -16,482 +16,6 @@ import ( "code.gitea.io/gitea/modules/setting" ) -// 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 -} - -// CX returns the centre of the circle for this commit -func (c *Commit) CX() int { - return c.Column*5 + 5 -} - -// CY returns the centre of the circle for this commit -func (c *Commit) CY() int { - return c.Row*10 + 5 -} - -// OnlyRelation returns whether this a relation only commit -func (c *Commit) OnlyRelation() bool { - return c.Row == -1 -} - -func newCommit(row, column, idx int, line []byte) (*Commit, error) { - data := bytes.SplitN(line[idx+5:], []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 -} - -// Glyph represents a co-ordinate and glyph -type Glyph struct { - Row int - Column int - Glyph byte -} - -// 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 (f *Flow) Color16() int { - return f.ColorNumber % 16 -} - -type parserState 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 -} - -// 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 (g *Graph) Width() int { - return g.MaxColumn - g.MinColumn + 1 -} - -// Height returns the height of the graph -func (g *Graph) Height() int { - return g.MaxRow - g.MinRow + 1 -} - -// NewGraph creates a basic graph -func NewGraph() *Graph { - graph := &Graph{} - graph.relationCommit = &Commit{ - Row: -1, - Column: -1, - } - graph.Flows = map[int64]*Flow{} - return graph -} - -func (state *parserState) reset() { - state.glyphs = state.glyphs[0:0] - state.oldGlyphs = state.oldGlyphs[0:0] - state.flows = state.flows[0:0] - state.oldFlows = state.oldFlows[0:0] - state.maxFlow = 0 - state.colors = state.colors[0:0] - state.oldColors = state.oldColors[0:0] - state.availableColors = state.availableColors[0:0] - state.availableColors = append(state.availableColors, 1, 2) - state.nextAvailable = 0 - state.firstInUse = -1 - state.firstAvailable = 0 - state.maxAllowedColors = 0 -} - -func (state *parserState) parseFlows(graph *Graph, row int, line []byte) error { - idx := bytes.Index(line, []byte("DATA:")) - if idx < 0 { - state.parseGlyphs(line) - } else { - state.parseGlyphs(line[:idx]) - } - - var err error - commitDone := false - - for column, glyph := range state.glyphs { - flowID := state.flows[column] - if glyph == ' ' { - continue - } - - flow, ok := graph.Flows[flowID] - if !ok { - flow = &Flow{ - ID: flowID, - ColorNumber: state.colors[column], - MinRow: row, - MinColumn: column, - MaxRow: row, - MaxColumn: column, - } - graph.Flows[flowID] = flow - } - 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 - } - 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 - } - - flow.Glyphs = append(flow.Glyphs, Glyph{ - row, - 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 - } - commit, err2 := newCommit(row, column, idx, line) - if err != nil && err2 != nil { - err = fmt.Errorf("%v %w", err2, err) - continue - } else if err2 != nil { - err = err2 - continue - } - commit.Flow = flowID - graph.Commits = append(graph.Commits, commit) - flow.Commits = append(flow.Commits, commit) - } - } - if !commitDone { - graph.Commits = append(graph.Commits, graph.relationCommit) - } - return err -} - -func (state *parserState) releaseUnusedColors() { - if state.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 := state.firstInUse - for _, color := range state.oldColors { - if color == 0 { - continue - } - found := false - i := position - for j := stepstaken; i != state.firstAvailable && j < len(state.availableColors); j++ { - colorToCheck := state.availableColors[i] - if colorToCheck == color { - found = true - break - } - i = (i + 1) % len(state.availableColors) - } - if !found { - // Duplicate color - continue - } - // Swap them around - state.availableColors[position], state.availableColors[i] = state.availableColors[i], state.availableColors[position] - stepstaken++ - position = (state.firstInUse + stepstaken) % len(state.availableColors) - if position == state.firstAvailable || stepstaken == len(state.availableColors) { - break - } - } - if stepstaken == len(state.availableColors) { - state.firstAvailable = -1 - } else { - state.firstAvailable = position - if state.nextAvailable == -1 { - state.nextAvailable = state.firstAvailable - } - } - } -} - -func (state *parserState) parseGlyphs(glyphs []byte) { - - // Clean state for parsing this row - state.glyphs, state.oldGlyphs = state.oldGlyphs, state.glyphs - state.glyphs = state.glyphs[0:0] - state.flows, state.oldFlows = state.oldFlows, state.flows - state.flows = state.flows[0:0] - state.colors, state.oldColors = state.oldColors, state.colors - - // Ensure we have enough flows and colors - state.colors = state.colors[0:0] - for range glyphs { - state.flows = append(state.flows, 0) - state.colors = append(state.colors, 0) - } - - // Copy the provided glyphs in to state.glyphs for safekeeping - state.glyphs = append(state.glyphs, glyphs...) - - // release unused colors - state.releaseUnusedColors() - - for i := len(glyphs) - 1; i >= 0; i-- { - glyph := glyphs[i] - switch glyph { - case '|': - fallthrough - case '*': - state.setUpFlow(i) - case '/': - state.setOutFlow(i) - case '\\': - state.setInFlow(i) - case '_': - state.setRightFlow(i) - case '.': - fallthrough - case '-': - state.setLeftFlow(i) - case ' ': - // no-op - default: - state.newFlow(i) - } - } -} - -func (state *parserState) takePreviousFlow(i, j int) { - if j < len(state.oldFlows) && state.oldFlows[j] > 0 { - state.flows[i] = state.oldFlows[j] - state.oldFlows[j] = 0 - state.colors[i] = state.oldColors[j] - state.oldColors[j] = 0 - } else { - state.newFlow(i) - } -} - -func (state *parserState) takeCurrentFlow(i, j int) { - if j < len(state.flows) && state.flows[j] > 0 { - state.flows[i] = state.flows[j] - state.colors[i] = state.colors[j] - } else { - state.newFlow(i) - } -} - -func (state *parserState) newFlow(i int) { - state.maxFlow++ - state.flows[i] = state.maxFlow - - // Now give this flow a color - if state.nextAvailable == -1 { - next := len(state.availableColors) - if state.maxAllowedColors < 1 || next < state.maxAllowedColors { - state.nextAvailable = next - state.firstAvailable = next - state.availableColors = append(state.availableColors, next+1) - } - } - state.colors[i] = state.availableColors[state.nextAvailable] - if state.firstInUse == -1 { - state.firstInUse = state.nextAvailable - } - state.availableColors[state.firstAvailable], state.availableColors[state.nextAvailable] = state.availableColors[state.nextAvailable], state.availableColors[state.firstAvailable] - - state.nextAvailable = (state.nextAvailable + 1) % len(state.availableColors) - state.firstAvailable = (state.firstAvailable + 1) % len(state.availableColors) - - if state.nextAvailable == state.firstInUse { - state.nextAvailable = state.firstAvailable - } - if state.nextAvailable == state.firstInUse { - state.nextAvailable = -1 - state.firstAvailable = -1 - } -} - -// setUpFlow handles '|' or '*' -func (state *parserState) setUpFlow(i int) { - // In preference order: - // - // Previous Row: '\? ' ' |' ' /' - // Current Row: ' | ' ' |' ' | ' - if i > 0 && i-1 < len(state.oldGlyphs) && state.oldGlyphs[i-1] == '\\' { - state.takePreviousFlow(i, i-1) - } else if i < len(state.oldGlyphs) && (state.oldGlyphs[i] == '|' || state.oldGlyphs[i] == '*') { - state.takePreviousFlow(i, i) - } else if i+1 < len(state.oldGlyphs) && state.oldGlyphs[i+1] == '/' { - state.takePreviousFlow(i, i+1) - } else { - state.newFlow(i) - } -} - -// setOutFlow handles '/' -func (state *parserState) setOutFlow(i int) { - // In preference order: - // - // Previous Row: ' |/' ' |_' ' |' ' /' ' _' '\' - // Current Row: '/| ' '/| ' '/ ' '/ ' '/ ' '/' - if i+2 < len(state.oldGlyphs) && - (state.oldGlyphs[i+1] == '|' || state.oldGlyphs[i+1] == '*') && - (state.oldGlyphs[i+2] == '/' || state.oldGlyphs[i+2] == '_') && - i+1 < len(state.glyphs) && - (state.glyphs[i+1] == '|' || state.glyphs[i+1] == '*') { - state.takePreviousFlow(i, i+2) - } else if i+1 < len(state.oldGlyphs) && - (state.oldGlyphs[i+1] == '|' || state.oldGlyphs[i+1] == '*' || - state.oldGlyphs[i+1] == '/' || state.oldGlyphs[i+1] == '_') { - state.takePreviousFlow(i, i+1) - if state.oldGlyphs[i+1] == '/' { - state.glyphs[i] = '|' - } - } else if i < len(state.oldGlyphs) && state.oldGlyphs[i] == '\\' { - state.takePreviousFlow(i, i) - } else { - state.newFlow(i) - } -} - -// setInFlow handles '\' -func (state *parserState) setInFlow(i int) { - // In preference order: - // - // Previous Row: '| ' '-. ' '| ' '\ ' '/' '---' - // Current Row: '|\' ' \' ' \' ' \' '\' ' \ ' - if i > 0 && i-1 < len(state.oldGlyphs) && - (state.oldGlyphs[i-1] == '|' || state.oldGlyphs[i-1] == '*') && - (state.glyphs[i-1] == '|' || state.glyphs[i-1] == '*') { - state.newFlow(i) - } else if i > 0 && i-1 < len(state.oldGlyphs) && - (state.oldGlyphs[i-1] == '|' || state.oldGlyphs[i-1] == '*' || - state.oldGlyphs[i-1] == '.' || state.oldGlyphs[i-1] == '\\') { - state.takePreviousFlow(i, i-1) - if state.oldGlyphs[i-1] == '\\' { - state.glyphs[i] = '|' - } - } else if i < len(state.oldGlyphs) && state.oldGlyphs[i] == '/' { - state.takePreviousFlow(i, i) - } else { - state.newFlow(i) - } -} - -// setRightFlow handles '_' -func (state *parserState) setRightFlow(i int) { - // In preference order: - // - // Current Row: '__' '_/' '_|_' '_|/' - if i+1 < len(state.glyphs) && - (state.glyphs[i+1] == '_' || state.glyphs[i+1] == '/') { - state.takeCurrentFlow(i, i+1) - } else if i+2 < len(state.glyphs) && - (state.glyphs[i+1] == '|' || state.glyphs[i+1] == '*') && - (state.glyphs[i+2] == '_' || state.glyphs[i+2] == '/') { - state.takeCurrentFlow(i, i+2) - } else { - state.newFlow(i) - } -} - -// setLeftFlow handles '----.' -func (state *parserState) setLeftFlow(i int) { - if state.glyphs[i] == '.' { - state.newFlow(i) - } else if i+1 < len(state.glyphs) && - (state.glyphs[i+1] == '-' || state.glyphs[i+1] == '.') { - state.takeCurrentFlow(i, i+1) - } else { - state.newFlow(i) - } -} - // GetCommitGraph return a list of commit (GraphItems) from all branches func GetCommitGraph(r *git.Repository, page int, maxAllowedColors int) (*Graph, error) { format := "DATA:%d|%H|%ad|%an|%ae|%h|%s" @@ -524,7 +48,7 @@ func GetCommitGraph(r *git.Repository, page int, maxAllowedColors int) (*Graph, if err := graphCmd.RunInDirTimeoutEnvFullPipelineFunc(nil, -1, r.Path, stdoutWriter, stderr, nil, func(ctx context.Context, cancel context.CancelFunc) error { _ = stdoutWriter.Close() defer stdoutReader.Close() - parser := &parserState{} + parser := &Parser{} parser.firstInUse = -1 parser.maxAllowedColors = maxAllowedColors if maxAllowedColors > 0 { @@ -545,7 +69,7 @@ func GetCommitGraph(r *git.Repository, page int, maxAllowedColors int) (*Graph, if starIdx >= 0 && starIdx < dataIdx { commitsToSkip-- } - parser.parseGlyphs(line[:dataIdx]) + parser.ParseGlyphs(line[:dataIdx]) } row := 0 @@ -554,19 +78,19 @@ func GetCommitGraph(r *git.Repository, page int, maxAllowedColors int) (*Graph, for scanner.Scan() { line := scanner.Bytes() if bytes.IndexByte(line, '*') >= 0 { - if err := parser.parseFlows(graph, row, line); err != nil { + if err := parser.AddLineToGraph(graph, row, line); err != nil { cancel() return err } break } - parser.parseGlyphs(line) + parser.ParseGlyphs(line) } for scanner.Scan() { row++ line := scanner.Bytes() - if err := parser.parseFlows(graph, row, line); err != nil { + if err := parser.AddLineToGraph(graph, row, line); err != nil { cancel() return err } diff --git a/modules/gitgraph/graph_models.go b/modules/gitgraph/graph_models.go new file mode 100644 index 0000000000000..ea6ba960846dc --- /dev/null +++ b/modules/gitgraph/graph_models.go @@ -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 +} diff --git a/modules/gitgraph/graph_test.go b/modules/gitgraph/graph_test.go index 89855543cdf11..ca9d653ceed3a 100644 --- a/modules/gitgraph/graph_test.go +++ b/modules/gitgraph/graph_test.go @@ -36,12 +36,12 @@ func BenchmarkGetCommitGraph(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" - parser := &parserState{} - parser.reset() + parser := &Parser{} + parser.Reset() for i := 0; i < b.N; i++ { - parser.reset() + parser.Reset() graph := NewGraph() - if err := parser.parseFlows(graph, 0, []byte(testString)); err != nil { + if err := parser.AddLineToGraph(graph, 0, []byte(testString)); err != nil { b.Error("could not parse teststring") } if graph.Flows[1].Commits[0].Author != "Kjell Kvinge" { @@ -51,17 +51,17 @@ func BenchmarkParseCommitString(b *testing.B) { } func BenchmarkParseGlyphs(b *testing.B) { - parser := &parserState{} - parser.reset() + parser := &Parser{} + parser.Reset() tgBytes := []byte(testglyphs) tg := tgBytes idx := bytes.Index(tg, []byte("\n")) for i := 0; i < b.N; i++ { - parser.reset() + parser.Reset() tg = tgBytes idx = bytes.Index(tg, []byte("\n")) for idx > 0 { - parser.parseGlyphs(tg[:idx]) + parser.ParseGlyphs(tg[:idx]) tg = tg[idx+1:] idx = bytes.Index(tg, []byte("\n")) } @@ -107,8 +107,8 @@ func TestReleaseUnusedColors(t *testing.T) { }, } for _, testcase := range testcases { - parser := &parserState{} - parser.reset() + parser := &Parser{} + parser.Reset() parser.availableColors = append([]int{}, testcase.availableColors...) parser.oldColors = append(parser.oldColors, testcase.oldColors...) parser.firstAvailable = testcase.firstAvailable @@ -205,14 +205,14 @@ func TestReleaseUnusedColors(t *testing.T) { } func TestParseGlyphs(t *testing.T) { - parser := &parserState{} - parser.reset() + parser := &Parser{} + parser.Reset() tgBytes := []byte(testglyphs) tg := tgBytes idx := bytes.Index(tg, []byte("\n")) row := 0 for idx > 0 { - parser.parseGlyphs(tg[:idx]) + parser.ParseGlyphs(tg[:idx]) tg = tg[idx+1:] idx = bytes.Index(tg, []byte("\n")) if parser.flows[0] != 1 { @@ -260,7 +260,7 @@ func TestCommitStringParsing(t *testing.T) { t.Run(test.testName, func(t *testing.T) { testString := fmt.Sprintf("%s%s", dataFirstPart, test.commitMessage) idx := strings.Index(testString, "DATA:") - commit, err := newCommit(0, 0, idx, []byte(testString)) + commit, err := NewCommit(0, 0, []byte(testString[idx+5:])) if err != nil && test.shouldPass { t.Errorf("Could not parse %s", testString) return diff --git a/modules/gitgraph/parser.go b/modules/gitgraph/parser.go new file mode 100644 index 0000000000000..62e05056520fe --- /dev/null +++ b/modules/gitgraph/parser.go @@ -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) + } +} From 892858dc3a12395f65f7024f884c227c2336cb0d Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 28 Jul 2020 21:52:05 +0200 Subject: [PATCH 09/11] tweaks from @silverwind Signed-off-by: Andrew Thornton --- public/img/svg/material-invert-colors.svg | 1 + public/img/svg/material-palette.svg | 1 + templates/repo/graph.tmpl | 10 +++++----- web_src/less/_repository.less | 11 ++++++++--- web_src/less/themes/theme-arc-green.less | 9 +++++++-- web_src/svg/material-invert-colors.svg | 1 + web_src/svg/material-palette.svg | 1 + 7 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 public/img/svg/material-invert-colors.svg create mode 100644 public/img/svg/material-palette.svg create mode 100644 web_src/svg/material-invert-colors.svg create mode 100644 web_src/svg/material-palette.svg diff --git a/public/img/svg/material-invert-colors.svg b/public/img/svg/material-invert-colors.svg new file mode 100644 index 0000000000000..018a693a02da9 --- /dev/null +++ b/public/img/svg/material-invert-colors.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/material-palette.svg b/public/img/svg/material-palette.svg new file mode 100644 index 0000000000000..d257e65d3355c --- /dev/null +++ b/public/img/svg/material-palette.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl index d760cb1c82ecc..c644b24d577c3 100644 --- a/templates/repo/graph.tmpl +++ b/templates/repo/graph.tmpl @@ -5,9 +5,9 @@

    {{.i18n.Tr "repo.commit_graph"}}
    -
    - - +
    + +

    @@ -28,9 +28,9 @@ {{- else if eq $glyph.Glyph '_' -}} M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 10) 10}} h 10 {{/* */ -}} {{- end -}} - {{- end}}" stroke-width="2" fill="none" id="flow-{{$flow.ID}}-path" stroke-linecap="round"/> + {{- end}}" stroke-width="1" fill="none" id="flow-{{$flow.ID}}-path" stroke-linecap="round"/> {{range $flow.Commits}} - + {{end}} {{end}} diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index c1d89de1958d7..9a749480f17d6 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -3263,10 +3263,15 @@ tbody.commit-list { } } +#git-graph-container #rev-list li.highlight, #git-graph-container #rev-list li.hover { - font-weight: bold; + background-color: rgba(0, 0, 0, .05); } -#git-graph-container #rev-list li.highlight { - background-color: #fcf8e9; +#git-graph-container #rev-list li.highlight.hover { + background-color: rgba(0, 0, 0, .1); +} + +#git-graph-container .color-buttons { + margin-right: 0; } diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index cbbe2346027f6..38cb539ce8fe2 100644 --- a/web_src/less/themes/theme-arc-green.less +++ b/web_src/less/themes/theme-arc-green.less @@ -1929,8 +1929,13 @@ footer .container .links > * { } } -#git-graph-container #rev-list li.highlight { - background-color: #39423e; +#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 { diff --git a/web_src/svg/material-invert-colors.svg b/web_src/svg/material-invert-colors.svg new file mode 100644 index 0000000000000..e6445ab230f10 --- /dev/null +++ b/web_src/svg/material-invert-colors.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web_src/svg/material-palette.svg b/web_src/svg/material-palette.svg new file mode 100644 index 0000000000000..df0e1756ffd0e --- /dev/null +++ b/web_src/svg/material-palette.svg @@ -0,0 +1 @@ + \ No newline at end of file From 7b2a40b42627f033b181eb57e1960e7fe7243db3 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 29 Jul 2020 20:00:52 +0100 Subject: [PATCH 10/11] as per @silverwind Signed-off-by: Andrew Thornton --- .stylelintrc | 3 -- web_src/less/_repository.less | 59 ++++++++++++++++++++++++++++++++ web_src/less/index.less | 1 - web_src/less/vendor/gitGraph.css | 15 -------- 4 files changed, 59 insertions(+), 19 deletions(-) delete mode 100644 web_src/less/vendor/gitGraph.css diff --git a/.stylelintrc b/.stylelintrc index 102b90f1fdaf1..fcc33edeff988 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,8 +1,5 @@ extends: stylelint-config-standard -ignoreFiles: - - web_src/less/vendor/**/* - rules: at-rule-empty-line-before: null block-closing-brace-empty-line-before: null diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 9a749480f17d6..466855285bc52 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -3083,10 +3083,69 @@ tbody.commit-list { } #git-graph-container { + float: left; display: block; + overflow-x: auto; + width: 100%; + .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%; +} + +#graph-raw-list { + margin: 0; } #git-graph-container.monochrome #rel-container .flow-group { diff --git a/web_src/less/index.less b/web_src/less/index.less index 33bd41e6f6ca7..e001a1a43ad09 100644 --- a/web_src/less/index.less +++ b/web_src/less/index.less @@ -1,5 +1,4 @@ @import "~font-awesome/css/font-awesome.css"; -@import "./vendor/gitGraph.css"; @import "_svg"; @import "_tribute"; diff --git a/web_src/less/vendor/gitGraph.css b/web_src/less/vendor/gitGraph.css deleted file mode 100644 index bb7e708101b83..0000000000000 --- a/web_src/less/vendor/gitGraph.css +++ /dev/null @@ -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;} From bdfb94e62cfbf38872ef481204118b06de825c76 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Thu, 30 Jul 2020 21:46:36 +0100 Subject: [PATCH 11/11] As per @silverwind Signed-off-by: Andrew Thornton --- web_src/less/_repository.less | 253 --------------------------- web_src/less/features/gitgraph.less | 256 ++++++++++++++++++++++++++++ web_src/less/index.less | 2 + 3 files changed, 258 insertions(+), 253 deletions(-) create mode 100644 web_src/less/features/gitgraph.less diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 466855285bc52..ebb7683868b04 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -3081,256 +3081,3 @@ tbody.commit-list { } } } - -#git-graph-container { - float: left; - display: block; - overflow-x: auto; - width: 100%; - - .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%; -} - -#graph-raw-list { - margin: 0; -} - -#git-graph-container.monochrome #rel-container .flow-group { - stroke: grey; - fill: grey; -} - -#git-graph-container.monochrome #rel-container .flow-group.highlight { - stroke: black; - fill: black; -} - -#git-graph-container:not(.monochrome) #rel-container .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; - } -} - -#git-graph-container: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; - } -} - -#git-graph-container #rev-list li.highlight, -#git-graph-container #rev-list li.hover { - background-color: rgba(0, 0, 0, .05); -} - -#git-graph-container #rev-list li.highlight.hover { - background-color: rgba(0, 0, 0, .1); -} - -#git-graph-container .color-buttons { - margin-right: 0; -} diff --git a/web_src/less/features/gitgraph.less b/web_src/less/features/gitgraph.less new file mode 100644 index 0000000000000..8a9c4239a7987 --- /dev/null +++ b/web_src/less/features/gitgraph.less @@ -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; + } + } + } +} diff --git a/web_src/less/index.less b/web_src/less/index.less index e001a1a43ad09..63aac5ba4c999 100644 --- a/web_src/less/index.less +++ b/web_src/less/index.less @@ -1,5 +1,7 @@ @import "~font-awesome/css/font-awesome.css"; +@import "./features/gitgraph.less"; + @import "_svg"; @import "_tribute"; @import "_base";