* Multiple GitGraph improvements. Add backend support for excluding PRs, selecting branches and files. Fix #10327 Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @silverwind Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @silverwind Signed-off-by: Andrew Thornton <art27@cantab.net> * Only show refs in dropdown we display on the graph Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @silverwind Signed-off-by: Andrew Thornton <art27@cantab.net> * use flexbox for ui header Signed-off-by: Andrew Thornton <art27@cantab.net> * Move Hide Pull Request button to the dropdown Signed-off-by: Andrew Thornton <art27@cantab.net> * Add SHA and user pictures Signed-off-by: Andrew Thornton <art27@cantab.net> * fix test Signed-off-by: Andrew Thornton <art27@cantab.net> * fix test 2 Signed-off-by: Andrew Thornton <art27@cantab.net> * fixes * async * more tweaks * use tabs in tmpl Signed-off-by: Andrew Thornton <art27@cantab.net> * remove commented thing Signed-off-by: Andrew Thornton <art27@cantab.net> * fix linting Signed-off-by: Andrew Thornton <art27@cantab.net> * Update web_src/js/features/gitgraph.js Co-authored-by: silverwind <me@silverwind.io> * graph tweaks * more tweaks * add title Signed-off-by: Andrew Thornton <art27@cantab.net> * fix loading indicator z-index and position Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: Lauris BH <lauris@nix.lv>tags/v1.15.0-dev
@@ -37,6 +37,12 @@ func (p *Pagination) AddParam(ctx *Context, paramKey string, ctxKey string) { | |||
p.urlParams = append(p.urlParams, urlParam) | |||
} | |||
// AddParamString adds a string parameter directly | |||
func (p *Pagination) AddParamString(key string, value string) { | |||
urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value)) | |||
p.urlParams = append(p.urlParams, urlParam) | |||
} | |||
// GetParams returns the configured URL params | |||
func (p *Pagination) GetParams() template.URL { | |||
return template.URL(strings.Join(p.urlParams, "&")) | |||
@@ -171,6 +171,18 @@ func (r *Repository) GetCommitsCount() (int64, error) { | |||
}) | |||
} | |||
// GetCommitGraphsCount returns cached commit count for current view | |||
func (r *Repository) GetCommitGraphsCount(hidePRRefs bool, branches []string, files []string) (int64, error) { | |||
cacheKey := fmt.Sprintf("commits-count-%d-graph-%t-%s-%s", r.Repository.ID, hidePRRefs, branches, files) | |||
return cache.GetInt64(cacheKey, func() (int64, error) { | |||
if len(branches) == 0 { | |||
return git.AllCommitsCount(r.Repository.RepoPath(), hidePRRefs, files...) | |||
} | |||
return git.CommitsCountFiles(r.Repository.RepoPath(), branches, files) | |||
}) | |||
} | |||
// BranchNameSubURL sub-URL for the BranchName field | |||
func (r *Repository) BranchNameSubURL() string { | |||
switch { | |||
@@ -262,8 +262,19 @@ func CommitChangesWithArgs(repoPath string, args []string, opts CommitChangesOpt | |||
} | |||
// AllCommitsCount returns count of all commits in repository | |||
func AllCommitsCount(repoPath string) (int64, error) { | |||
stdout, err := NewCommand("rev-list", "--all", "--count").RunInDir(repoPath) | |||
func AllCommitsCount(repoPath string, hidePRRefs bool, files ...string) (int64, error) { | |||
args := []string{"--all", "--count"} | |||
if hidePRRefs { | |||
args = append([]string{"--exclude=refs/pull/*"}, args...) | |||
} | |||
cmd := NewCommand("rev-list") | |||
cmd.AddArguments(args...) | |||
if len(files) > 0 { | |||
cmd.AddArguments("--") | |||
cmd.AddArguments(files...) | |||
} | |||
stdout, err := cmd.RunInDir(repoPath) | |||
if err != nil { | |||
return 0, err | |||
} | |||
@@ -271,7 +282,8 @@ func AllCommitsCount(repoPath string) (int64, error) { | |||
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) | |||
} | |||
func commitsCount(repoPath string, revision, relpath []string) (int64, error) { | |||
// CommitsCountFiles returns number of total commits of until given revision. | |||
func CommitsCountFiles(repoPath string, revision, relpath []string) (int64, error) { | |||
cmd := NewCommand("rev-list", "--count") | |||
cmd.AddArguments(revision...) | |||
if len(relpath) > 0 { | |||
@@ -288,8 +300,8 @@ func commitsCount(repoPath string, revision, relpath []string) (int64, error) { | |||
} | |||
// CommitsCount returns number of total commits of until given revision. | |||
func CommitsCount(repoPath, revision string) (int64, error) { | |||
return commitsCount(repoPath, []string{revision}, []string{}) | |||
func CommitsCount(repoPath string, revision ...string) (int64, error) { | |||
return CommitsCountFiles(repoPath, revision, []string{}) | |||
} | |||
// CommitsCount returns number of total commits of until current revision. | |||
@@ -4,6 +4,8 @@ | |||
package git | |||
import "strings" | |||
// Reference represents a Git ref. | |||
type Reference struct { | |||
Name string | |||
@@ -16,3 +18,44 @@ type Reference struct { | |||
func (ref *Reference) Commit() (*Commit, error) { | |||
return ref.repo.getCommit(ref.Object) | |||
} | |||
// ShortName returns the short name of the reference | |||
func (ref *Reference) ShortName() string { | |||
if ref == nil { | |||
return "" | |||
} | |||
if strings.HasPrefix(ref.Name, "refs/heads/") { | |||
return ref.Name[11:] | |||
} | |||
if strings.HasPrefix(ref.Name, "refs/tags/") { | |||
return ref.Name[10:] | |||
} | |||
if strings.HasPrefix(ref.Name, "refs/remotes/") { | |||
return ref.Name[13:] | |||
} | |||
if strings.HasPrefix(ref.Name, "refs/pull/") && strings.IndexByte(ref.Name[10:], '/') > -1 { | |||
return ref.Name[10 : strings.IndexByte(ref.Name[10:], '/')+10] | |||
} | |||
return ref.Name | |||
} | |||
// RefGroup returns the group type of the reference | |||
func (ref *Reference) RefGroup() string { | |||
if ref == nil { | |||
return "" | |||
} | |||
if strings.HasPrefix(ref.Name, "refs/heads/") { | |||
return "heads" | |||
} | |||
if strings.HasPrefix(ref.Name, "refs/tags/") { | |||
return "tags" | |||
} | |||
if strings.HasPrefix(ref.Name, "refs/remotes/") { | |||
return "remotes" | |||
} | |||
if strings.HasPrefix(ref.Name, "refs/pull/") && strings.IndexByte(ref.Name[10:], '/') > -1 { | |||
return "pull" | |||
} | |||
return "" | |||
} |
@@ -49,7 +49,7 @@ const prettyLogFormat = `--pretty=format:%H` | |||
// GetAllCommitsCount returns count of all commits in repository | |||
func (repo *Repository) GetAllCommitsCount() (int64, error) { | |||
return AllCommitsCount(repo.Path) | |||
return AllCommitsCount(repo.Path, false) | |||
} | |||
func (repo *Repository) parsePrettyFormatLogToList(logs []byte) (*list.List, error) { | |||
@@ -318,7 +318,7 @@ func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bo | |||
// FileCommitsCount return the number of files at a revison | |||
func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) { | |||
return commitsCount(repo.Path, []string{revision}, []string{file}) | |||
return CommitsCountFiles(repo.Path, []string{revision}, []string{file}) | |||
} | |||
// CommitsByFileAndRange return the commits according revison file and the page | |||
@@ -413,11 +413,11 @@ func (repo *Repository) CommitsBetweenIDs(last, before string) (*list.List, erro | |||
// CommitsCountBetween return numbers of commits between two commits | |||
func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) { | |||
count, err := commitsCount(repo.Path, []string{start + "..." + end}, []string{}) | |||
count, err := CommitsCountFiles(repo.Path, []string{start + "..." + end}, []string{}) | |||
if err != nil && strings.Contains(err.Error(), "no merge base") { | |||
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. | |||
// previously it would return the results of git rev-list before last so let's try that... | |||
return commitsCount(repo.Path, []string{start, end}, []string{}) | |||
return CommitsCountFiles(repo.Path, []string{start, end}, []string{}) | |||
} | |||
return count, err | |||
@@ -17,23 +17,42 @@ import ( | |||
) | |||
// 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" | |||
func GetCommitGraph(r *git.Repository, page int, maxAllowedColors int, hidePRRefs bool, branches, files []string) (*Graph, error) { | |||
format := "DATA:%D|%H|%ad|%h|%s" | |||
if page == 0 { | |||
page = 1 | |||
} | |||
graphCmd := git.NewCommand("log") | |||
graphCmd.AddArguments("--graph", | |||
"--date-order", | |||
"--all", | |||
args := make([]string, 0, 12+len(branches)+len(files)) | |||
args = append(args, "--graph", "--date-order", "--decorate=full") | |||
if hidePRRefs { | |||
args = append(args, "--exclude=refs/pull/*") | |||
} | |||
if len(branches) == 0 { | |||
args = append(args, "--all") | |||
} | |||
args = append(args, | |||
"-C", | |||
"-M", | |||
fmt.Sprintf("-n %d", setting.UI.GraphMaxCommitNum*page), | |||
"--date=iso", | |||
fmt.Sprintf("--pretty=format:%s", format), | |||
) | |||
fmt.Sprintf("--pretty=format:%s", format)) | |||
if len(branches) > 0 { | |||
args = append(args, branches...) | |||
} | |||
args = append(args, "--") | |||
if len(files) > 0 { | |||
args = append(args, files...) | |||
} | |||
graphCmd := git.NewCommand("log") | |||
graphCmd.AddArguments(args...) | |||
graph := NewGraph() | |||
stderr := new(strings.Builder) | |||
@@ -7,6 +7,10 @@ package gitgraph | |||
import ( | |||
"bytes" | |||
"fmt" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/git" | |||
"code.gitea.io/gitea/modules/log" | |||
) | |||
// NewGraph creates a basic graph | |||
@@ -77,6 +81,48 @@ func (graph *Graph) AddCommit(row, column int, flowID int64, data []byte) error | |||
return nil | |||
} | |||
// LoadAndProcessCommits will load the git.Commits for each commit in the graph, | |||
// the associate the commit with the user author, and check the commit verification | |||
// before finally retrieving the latest status | |||
func (graph *Graph) LoadAndProcessCommits(repository *models.Repository, gitRepo *git.Repository) error { | |||
var err error | |||
var ok bool | |||
emails := map[string]*models.User{} | |||
keyMap := map[string]bool{} | |||
for _, c := range graph.Commits { | |||
if len(c.Rev) == 0 { | |||
continue | |||
} | |||
c.Commit, err = gitRepo.GetCommit(c.Rev) | |||
if err != nil { | |||
return fmt.Errorf("GetCommit: %s Error: %w", c.Rev, err) | |||
} | |||
if c.Commit.Author != nil { | |||
email := c.Commit.Author.Email | |||
if c.User, ok = emails[email]; !ok { | |||
c.User, _ = models.GetUserByEmail(email) | |||
emails[email] = c.User | |||
} | |||
} | |||
c.Verification = models.ParseCommitWithSignature(c.Commit) | |||
_ = models.CalculateTrustStatus(c.Verification, repository, &keyMap) | |||
statuses, err := models.GetLatestCommitStatus(repository, c.Commit.ID.String(), 0) | |||
if err != nil { | |||
log.Error("GetLatestCommitStatus: %v", err) | |||
} else { | |||
c.Status = models.CalcCommitStatus(statuses) | |||
} | |||
} | |||
return nil | |||
} | |||
// NewFlow creates a new flow | |||
func NewFlow(flowID int64, color, row, column int) *Flow { | |||
return &Flow{ | |||
@@ -142,42 +188,60 @@ var RelationCommit = &Commit{ | |||
// 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 { | |||
data := bytes.SplitN(line, []byte("|"), 5) | |||
if len(data) < 5 { | |||
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]), | |||
Refs: newRefsFromRefNames(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]), | |||
// 3 matches git log --pretty=format:%h => abbreviated commit hash | |||
ShortRev: string(data[3]), | |||
// 4 matches git log --pretty=format:%s => subject | |||
Subject: string(data[4]), | |||
}, nil | |||
} | |||
func newRefsFromRefNames(refNames []byte) []git.Reference { | |||
refBytes := bytes.Split(refNames, []byte{',', ' '}) | |||
refs := make([]git.Reference, 0, len(refBytes)) | |||
for _, refNameBytes := range refBytes { | |||
if len(refNameBytes) == 0 { | |||
continue | |||
} | |||
refName := string(refNameBytes) | |||
if refName[0:5] == "tag: " { | |||
refName = refName[5:] | |||
} else if refName[0:8] == "HEAD -> " { | |||
refName = refName[8:] | |||
} | |||
refs = append(refs, git.Reference{ | |||
Name: refName, | |||
}) | |||
} | |||
return refs | |||
} | |||
// 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 | |||
Commit *git.Commit | |||
User *models.User | |||
Verification *models.CommitVerification | |||
Status *models.CommitStatus | |||
Flow int64 | |||
Row int | |||
Column int | |||
Refs []git.Reference | |||
Rev string | |||
Date string | |||
ShortRev string | |||
Subject string | |||
} | |||
// OnlyRelation returns whether this a relation only commit | |||
@@ -22,7 +22,7 @@ func BenchmarkGetCommitGraph(b *testing.B) { | |||
defer currentRepo.Close() | |||
for i := 0; i < b.N; i++ { | |||
graph, err := GetCommitGraph(currentRepo, 1, 0) | |||
graph, err := GetCommitGraph(currentRepo, 1, 0, false, nil, nil) | |||
if err != nil { | |||
b.Error("Could get commit graph") | |||
} | |||
@@ -34,7 +34,7 @@ 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" | |||
testString := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|4e61bac|Add route for graph" | |||
parser := &Parser{} | |||
parser.Reset() | |||
@@ -44,7 +44,7 @@ func BenchmarkParseCommitString(b *testing.B) { | |||
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" { | |||
if graph.Flows[1].Commits[0].Rev != "4e61bacab44e9b4730e44a6615d04098dd3a8eaf" { | |||
b.Error("Did not get expected data") | |||
} | |||
} | |||
@@ -244,7 +244,7 @@ func TestParseGlyphs(t *testing.T) { | |||
} | |||
func TestCommitStringParsing(t *testing.T) { | |||
dataFirstPart := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Author|user@mail.something|4e61bac|" | |||
dataFirstPart := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|4e61bac|" | |||
tests := []struct { | |||
shouldPass bool | |||
testName string | |||
@@ -16,6 +16,7 @@ import ( | |||
"mime" | |||
"net/url" | |||
"path/filepath" | |||
"reflect" | |||
"regexp" | |||
"runtime" | |||
"strings" | |||
@@ -310,6 +311,26 @@ func NewFuncMap() []template.FuncMap { | |||
"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond), | |||
} | |||
}, | |||
"containGeneric": func(arr interface{}, v interface{}) bool { | |||
arrV := reflect.ValueOf(arr) | |||
if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String { | |||
return strings.Contains(arr.(string), v.(string)) | |||
} | |||
if arrV.Kind() == reflect.Slice { | |||
for i := 0; i < arrV.Len(); i++ { | |||
iV := arrV.Index(i) | |||
if !iV.CanInterface() { | |||
continue | |||
} | |||
if iV.Interface() == v { | |||
return true | |||
} | |||
} | |||
} | |||
return false | |||
}, | |||
"contain": func(s []int64, id int64) bool { | |||
for i := 0; i < len(s); i++ { | |||
if s[i] == id { | |||
@@ -822,6 +822,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.select = Select branches | |||
commit_graph.hide_pr_refs = Hide Pull Requests | |||
commit_graph.monochrome = Mono | |||
commit_graph.color = Color | |||
blame = Blame | |||
@@ -23,6 +23,7 @@ import ( | |||
const ( | |||
tplCommits base.TplName = "repo/commits" | |||
tplGraph base.TplName = "repo/graph" | |||
tplGraphDiv base.TplName = "repo/graph/div" | |||
tplCommitPage base.TplName = "repo/commit_page" | |||
) | |||
@@ -88,6 +89,7 @@ func Commits(ctx *context.Context) { | |||
// Graph render commit graph - show commits from all branches. | |||
func Graph(ctx *context.Context) { | |||
ctx.Data["Title"] = ctx.Tr("repo.commit_graph") | |||
ctx.Data["PageIsCommits"] = true | |||
ctx.Data["PageIsViewCode"] = true | |||
mode := strings.ToLower(ctx.QueryTrim("mode")) | |||
@@ -95,6 +97,18 @@ func Graph(ctx *context.Context) { | |||
mode = "color" | |||
} | |||
ctx.Data["Mode"] = mode | |||
hidePRRefs := ctx.QueryBool("hide-pr-refs") | |||
ctx.Data["HidePRRefs"] = hidePRRefs | |||
branches := ctx.QueryStrings("branch") | |||
realBranches := make([]string, len(branches)) | |||
copy(realBranches, branches) | |||
for i, branch := range realBranches { | |||
if strings.HasPrefix(branch, "--") { | |||
realBranches[i] = "refs/heads/" + branch | |||
} | |||
} | |||
ctx.Data["SelectedBranches"] = realBranches | |||
files := ctx.QueryStrings("file") | |||
commitsCount, err := ctx.Repo.GetCommitsCount() | |||
if err != nil { | |||
@@ -102,28 +116,60 @@ func Graph(ctx *context.Context) { | |||
return | |||
} | |||
allCommitsCount, err := ctx.Repo.GitRepo.GetAllCommitsCount() | |||
graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(hidePRRefs, realBranches, files) | |||
if err != nil { | |||
ctx.ServerError("GetAllCommitsCount", err) | |||
return | |||
log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err) | |||
realBranches = []string{} | |||
branches = []string{} | |||
graphCommitsCount, err = ctx.Repo.GetCommitGraphsCount(hidePRRefs, realBranches, files) | |||
if err != nil { | |||
ctx.ServerError("GetCommitGraphsCount", err) | |||
return | |||
} | |||
} | |||
page := ctx.QueryInt("page") | |||
graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0) | |||
graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0, hidePRRefs, realBranches, files) | |||
if err != nil { | |||
ctx.ServerError("GetCommitGraph", err) | |||
return | |||
} | |||
if err := graph.LoadAndProcessCommits(ctx.Repo.Repository, ctx.Repo.GitRepo); err != nil { | |||
ctx.ServerError("LoadAndProcessCommits", err) | |||
return | |||
} | |||
ctx.Data["Graph"] = graph | |||
gitRefs, err := ctx.Repo.GitRepo.GetRefs() | |||
if err != nil { | |||
ctx.ServerError("GitRepo.GetRefs", err) | |||
return | |||
} | |||
ctx.Data["AllRefs"] = gitRefs | |||
ctx.Data["Username"] = ctx.Repo.Owner.Name | |||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name | |||
ctx.Data["CommitCount"] = commitsCount | |||
ctx.Data["Branch"] = ctx.Repo.BranchName | |||
paginator := context.NewPagination(int(allCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) | |||
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) | |||
paginator.AddParam(ctx, "mode", "Mode") | |||
paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs") | |||
for _, branch := range branches { | |||
paginator.AddParamString("branch", branch) | |||
} | |||
for _, file := range files { | |||
paginator.AddParamString("file", file) | |||
} | |||
ctx.Data["Page"] = paginator | |||
if ctx.QueryBool("div-only") { | |||
ctx.HTML(200, tplGraphDiv) | |||
return | |||
} | |||
ctx.HTML(200, tplGraph) | |||
} | |||
@@ -3,60 +3,61 @@ | |||
{{template "repo/header" .}} | |||
<div class="ui container"> | |||
<div id="git-graph-container" class="ui segment{{if eq .Mode "monochrome"}} monochrome{{end}}"> | |||
<h2 class="ui header dividing">{{.i18n.Tr "repo.commit_graph"}} | |||
<div class="ui right"> | |||
<div class="ui icon buttons tiny color-buttons"> | |||
<button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{.i18n.Tr "repo.commit_graph.monochrome"}}"><span class="emoji">{{svg "material-invert-colors"}}</span> {{.i18n.Tr "repo.commit_graph.monochrome"}}</button> | |||
<button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{.i18n.Tr "repo.commit_graph.color"}}"><span class="emoji">{{svg "material-palette"}}</span> {{.i18n.Tr "repo.commit_graph.color"}}</button> | |||
<h2 class="ui header dividing"> | |||
{{.i18n.Tr "repo.commit_graph"}} | |||
<div class="ui icon buttons tiny color-buttons"> | |||
<div class="ui multiple selection search dropdown" id="flow-select-refs-dropdown"> | |||
<input type="hidden" name="flow"> | |||
<i class="dropdown icon"></i> | |||
<div class="default text">{{.i18n.Tr "repo.commit_graph.select"}}</div> | |||
<div class="menu"> | |||
<div class="item" data-value="...flow-hide-pr-refs"> | |||
<span class="truncate"> | |||
{{svg "octicon-eye-closed" 16 "mr-2"}}<span title="{{.i18n.Tr "repo.commit_graph.hide_pr_refs"}}">{{.i18n.Tr "repo.commit_graph.hide_pr_refs"}}</span> | |||
</span> | |||
</div> | |||
{{range .AllRefs}} | |||
{{$refGroup := .RefGroup}} | |||
{{if eq $refGroup "pull"}} | |||
<div class="item" data-value="{{.Name}}"> | |||
<span class="truncate"> | |||
{{svg "octicon-git-pull-request" 16 "mr-2"}}<span title="{{.ShortName}}">#{{.ShortName}}</span> | |||
</span> | |||
</div> | |||
{{else if eq $refGroup "tags"}} | |||
<div class="item" data-value="{{.Name}}"> | |||
<span class="truncate"> | |||
{{svg "octicon-tag" 16 "mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span> | |||
</span> | |||
</div> | |||
{{else if eq $refGroup "remotes"}} | |||
<div class="item" data-value="{{.Name}}"> | |||
<span class="truncate"> | |||
{{svg "octicon-cross-reference" 16 "mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span> | |||
</span> | |||
</div> | |||
{{else if eq $refGroup "heads"}} | |||
<div class="item" data-value="{{.Name}}"> | |||
<span class="truncate"> | |||
{{svg "octicon-git-branch" 16 "mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span> | |||
</span> | |||
</div> | |||
{{end}} | |||
{{end}} | |||
</div> | |||
</div> | |||
<button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{.i18n.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "mr-2"}}{{.i18n.Tr "repo.commit_graph.monochrome"}}</button> | |||
<button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{.i18n.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "mr-2"}}{{.i18n.Tr "repo.commit_graph.color"}}</button> | |||
</div> | |||
</h2> | |||
<div class="ui dividing"></div> | |||
<div id="rel-container"> | |||
<svg viewbox="{{Mul .Graph.MinColumn 5}} {{Mul .Graph.MinRow 10}} {{Add (Mul .Graph.Width 5) 5}} {{Mul .Graph.Height 10}}" width="{{Add (Mul .Graph.Width 10) 10}}px"> | |||
{{range $flowid, $flow := .Graph.Flows}} | |||
<g id="flow-{{$flow.ID}}" class="flow-group flow-color-{{$flow.ColorNumber}} flow-color-16-{{$flow.Color16}}" data-flow="{{$flow.ID}}" data-color="{{$flow.ColorNumber}}"> | |||
<path d="{{range $i, $glyph := $flow.Glyphs -}} | |||
{{- if or (eq $glyph.Glyph '*') (eq $glyph.Glyph '|') -}} | |||
M {{Add (Mul $glyph.Column 5) 5}} {{Add (Mul $glyph.Row 10) 0}} v 10 {{/* */ -}} | |||
{{- else if eq $glyph.Glyph '/' -}} | |||
M {{Add (Mul $glyph.Column 5) 10}} {{Add (Mul $glyph.Row 10) 0}} l -10 10 {{/* */ -}} | |||
{{- else if eq $glyph.Glyph '\\' -}} | |||
M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 10) 0}} l 10 10 {{/* */ -}} | |||
{{- else if or (eq $glyph.Glyph '-') (eq $glyph.Glyph '.') -}} | |||
M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 10) 10}} h 5 {{/* */ -}} | |||
{{- else if eq $glyph.Glyph '_' -}} | |||
M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 10) 10}} h 10 {{/* */ -}} | |||
{{- end -}} | |||
{{- end}}" stroke-width="1" fill="none" id="flow-{{$flow.ID}}-path" stroke-linecap="round"/> | |||
{{range $flow.Commits}} | |||
<circle class="flow-commit" cx="{{Add (Mul .Column 5) 5}}" cy="{{Add (Mul .Row 10) 5}}" r="2.5" stroke="none" id="flow-commit-{{.Rev}}" data-rev="{{.Rev}}"/> | |||
{{end}} | |||
</g> | |||
{{end}} | |||
</svg> | |||
</div> | |||
<div id="rev-container"> | |||
<ul id="rev-list"> | |||
{{ range .Graph.Commits }} | |||
<li id="commit-{{.Rev}}" data-flow="{{.Flow}}"> | |||
{{ if .OnlyRelation }} | |||
<span /> | |||
{{ else }} | |||
<code id="{{.ShortRev}}"> | |||
<a href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.Rev}}">{{ .ShortRev}}</a> | |||
</code> | |||
<strong> {{.Branch}}</strong> | |||
<span>{{RenderCommitMessage .Subject $.RepoLink $.Repository.ComposeMetas}}</span> by | |||
<span class="author">{{.Author}}</span> | |||
<span class="time">{{.Date}}</span> | |||
{{ end }} | |||
</li> | |||
{{ end }} | |||
</ul> | |||
</div> | |||
<div class="ui segment loading hide" id="loading-indicator"></div> | |||
{{ template "repo/graph/svgcontainer" .}} | |||
{{ template "repo/graph/commits" .}} | |||
</div> | |||
</div> | |||
</div> | |||
{{template "base/paginate" .}} | |||
<div id="pagination"> | |||
{{template "base/paginate" .}} | |||
</div> | |||
{{template "base/footer" .}} |
@@ -0,0 +1,80 @@ | |||
<div id="rev-container"> | |||
<ul id="rev-list"> | |||
{{ range $commitI, $commit := .Graph.Commits }} | |||
<li id="commit-{{$commit.Rev}}" data-flow="{{$commit.Flow}}"> | |||
{{ if $commit.OnlyRelation }} | |||
<span /> | |||
{{ else }} | |||
<span class="sha" id="{{$commit.ShortRev}}"> | |||
{{$class := "ui sha label"}} | |||
{{if $commit.Commit.Signature}} | |||
{{$class = (printf "%s%s" $class " isSigned")}} | |||
{{if $commit.Verification.Verified}} | |||
{{if eq $commit.Verification.TrustStatus "trusted"}} | |||
{{$class = (printf "%s%s" $class " isVerified")}} | |||
{{else if eq $commit.Verification.TrustStatus "untrusted"}} | |||
{{$class = (printf "%s%s" $class " isVerifiedUntrusted")}} | |||
{{else}} | |||
{{$class = (printf "%s%s" $class " isVerifiedUnmatched")}} | |||
{{end}} | |||
{{else if $commit.Verification.Warning}} | |||
{{$class = (printf "%s%s" $class " isWarning")}} | |||
{{end}} | |||
{{end}} | |||
<a href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{$commit.Rev}}" rel="nofollow" class="{{$class}}"> | |||
<span class="shortsha">{{ShortSha $commit.Commit.ID.String}}</span> | |||
{{- if $commit.Commit.Signature -}} | |||
<span class="shortsha-pad"></span>{{template "repo/shabox_badge" dict "root" $ "verification" $commit.Verification}} | |||
{{- end -}} | |||
</a> | |||
</span> | |||
<span class="message df ac mr-2">{{RenderCommitMessage $commit.Subject $.RepoLink $.Repository.ComposeMetas}}</span> | |||
<span class="tags df ac"> | |||
{{range $commit.Refs}} | |||
{{$refGroup := .RefGroup}} | |||
{{if eq $refGroup "pull"}} | |||
{{if $.HidePRRefs}} | |||
{{if (containGeneric $.SelectedBranches .Name) }} | |||
<a class="ui labelled icon button basic tiny" href="{{$.RepoLink}}/pulls/{{.ShortName|PathEscape}}"> | |||
{{svg "octicon-git-pull-request" 16 "mr-2"}}#{{.ShortName}} | |||
</a> | |||
{{end}} | |||
{{else}} | |||
<a class="ui labelled icon button basic tiny" href="{{$.RepoLink}}/pulls/{{.ShortName|PathEscape}}"> | |||
{{svg "octicon-git-pull-request" 16 "mr-2"}}#{{.ShortName}} | |||
</a> | |||
{{end}} | |||
{{else if eq $refGroup "tags"}} | |||
<a class="ui labelled icon button basic tiny" href="{{$.RepoLink}}/src/tag/{{.ShortName|PathEscape}}"> | |||
{{svg "octicon-tag" 16 "mr-2"}}{{.ShortName}} | |||
</a> | |||
{{else if eq $refGroup "remotes"}} | |||
<a class="ui labelled icon button basic tiny" href="{{$.RepoLink}}/src/commit/{{$commit.Rev}}"> | |||
{{svg "octicon-cross-reference" 16 "mr-2"}}{{.ShortName}} | |||
</a> | |||
{{else if eq $refGroup "heads"}} | |||
<a class="ui labelled icon button basic tiny" href="{{$.RepoLink}}/src/branch/{{.ShortName|PathEscape}}"> | |||
{{svg "octicon-git-branch" 16 "mr-2"}}{{.ShortName}} | |||
</a> | |||
{{else}} | |||
<!-- Unknown ref type {{.Name}} --> | |||
{{end}} | |||
{{end}} | |||
</span> | |||
<span class="author df ac mr-2"> | |||
{{$userName := $commit.Commit.Author.Name}} | |||
{{if $commit.User}} | |||
{{if $commit.User.FullName}} | |||
{{$userName = $commit.User.FullName}} | |||
{{end}} | |||
<img class="ui avatar image" src="{{$commit.User.RelAvatarLink}}" alt=""/><a href="{{AppSubUrl}}/{{$commit.User.Name}}">{{$userName}}</a> | |||
{{else}} | |||
<img class="ui avatar image" src="{{AvatarLink $commit.Commit.Author.Email}}" alt=""/>{{$userName}} | |||
{{end}} | |||
</span> | |||
<span class="time df ac">{{$commit.Date}}</span> | |||
{{ end }} | |||
</li> | |||
{{ end }} | |||
</ul> | |||
</div> |
@@ -0,0 +1,7 @@ | |||
<div> | |||
{{template "repo/graph/svgcontainer" .}} | |||
{{template "repo/graph/commits" .}} | |||
<div id="pagination"> | |||
{{template "base/paginate" .}} | |||
</div> | |||
</div> |
@@ -0,0 +1,24 @@ | |||
<div id="rel-container"> | |||
<svg viewbox="{{Mul .Graph.MinColumn 5}} {{Mul .Graph.MinRow 12}} {{Add (Mul .Graph.Width 5) 5}} {{Mul .Graph.Height 12}}" width="{{Add (Mul .Graph.Width 10) 10}}px"> | |||
{{range $flowid, $flow := .Graph.Flows}} | |||
<g id="flow-{{$flow.ID}}" class="flow-group flow-color-{{$flow.ColorNumber}} flow-color-16-{{$flow.Color16}}" data-flow="{{$flow.ID}}" data-color="{{$flow.ColorNumber}}"> | |||
<path d="{{range $i, $glyph := $flow.Glyphs -}} | |||
{{- if or (eq $glyph.Glyph '*') (eq $glyph.Glyph '|') -}} | |||
M {{Add (Mul $glyph.Column 5) 5}} {{Add (Mul $glyph.Row 12) 0}} v 12 {{/* */ -}} | |||
{{- else if eq $glyph.Glyph '/' -}} | |||
M {{Add (Mul $glyph.Column 5) 10}} {{Add (Mul $glyph.Row 12) 0}} l -10 12 {{/* */ -}} | |||
{{- else if eq $glyph.Glyph '\\' -}} | |||
M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 12) 0}} l 10 12 {{/* */ -}} | |||
{{- else if or (eq $glyph.Glyph '-') (eq $glyph.Glyph '.') -}} | |||
M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 12) 12}} h 5 {{/* */ -}} | |||
{{- else if eq $glyph.Glyph '_' -}} | |||
M {{Add (Mul $glyph.Column 5) 0}} {{Add (Mul $glyph.Row 12) 12}} h 10 {{/* */ -}} | |||
{{- end -}} | |||
{{- end}}" stroke-width="1" fill="none" id="flow-{{$flow.ID}}-path" stroke-linecap="round"/> | |||
{{range $flow.Commits}} | |||
<circle class="flow-commit" cx="{{Add (Mul .Column 5) 5}}" cy="{{Add (Mul .Row 12) 6}}" r="2.5" stroke="none" id="flow-commit-{{.Rev}}" data-rev="{{.Rev}}"/> | |||
{{end}} | |||
</g> | |||
{{end}} | |||
</svg> | |||
</div> |
@@ -46,6 +46,57 @@ export default async function initGitGraph() { | |||
window.history.replaceState({}, '', window.location.pathname); | |||
} | |||
}); | |||
const url = new URL(window.location); | |||
const params = url.searchParams; | |||
const updateGraph = async () => { | |||
const queryString = params.toString(); | |||
const ajaxUrl = new URL(url); | |||
ajaxUrl.searchParams.set('div-only', 'true'); | |||
window.history.replaceState({}, '', queryString ? `?${queryString}` : window.location.pathname); | |||
$('#pagination').empty(); | |||
$('#rel-container').addClass('hide'); | |||
$('#rev-container').addClass('hide'); | |||
$('#loading-indicator').removeClass('hide'); | |||
const div = $(await $.ajax(String(ajaxUrl))); | |||
$('#pagination').html(div.find('#pagination').html()); | |||
$('#rel-container').html(div.find('#rel-container').html()); | |||
$('#rev-container').html(div.find('#rev-container').html()); | |||
$('#loading-indicator').addClass('hide'); | |||
$('#rel-container').removeClass('hide'); | |||
$('#rev-container').removeClass('hide'); | |||
}; | |||
const dropdownSelected = params.getAll('branch'); | |||
if (params.has('hide-pr-refs') && params.get('hide-pr-refs') === 'true') { | |||
dropdownSelected.splice(0, 0, '...flow-hide-pr-refs'); | |||
} | |||
$('#flow-select-refs-dropdown').dropdown('set selected', dropdownSelected); | |||
$('#flow-select-refs-dropdown').dropdown({ | |||
clearable: true, | |||
onRemove(toRemove) { | |||
if (toRemove === '...flow-hide-pr-refs') { | |||
params.delete('hide-pr-refs'); | |||
} else { | |||
const branches = params.getAll('branch'); | |||
params.delete('branch'); | |||
for (const branch of branches) { | |||
if (branch !== toRemove) { | |||
params.append('branch', branch); | |||
} | |||
} | |||
} | |||
updateGraph(); | |||
}, | |||
onAdd(toAdd) { | |||
if (toAdd === '...flow-hide-pr-refs') { | |||
params.set('hide-pr-refs', true); | |||
} else { | |||
params.append('branch', toAdd); | |||
} | |||
updateGraph(); | |||
}, | |||
}); | |||
$('#git-graph-container').on('mouseenter', '#rev-list li', (e) => { | |||
const flow = $(e.currentTarget).data('flow'); | |||
if (flow === 0) return; | |||
@@ -1391,6 +1391,10 @@ table th[data-sortt-desc] { | |||
} | |||
} | |||
.dropdown .ui.label { | |||
margin-left: 0 !important; | |||
} | |||
.ui.dropdown .menu .item { | |||
border-radius: 0; | |||
} | |||
@@ -1462,6 +1462,7 @@ | |||
#commits-table td.sha .sha.label, | |||
#repo-files-table .sha.label, | |||
#rev-list .sha.label, | |||
.timeline-item.commits-list .singular-commit .sha.label { | |||
border: 1px solid #bbbbbb; | |||
@@ -1,8 +1,21 @@ | |||
#git-graph-container { | |||
float: left; | |||
display: block; | |||
overflow-x: auto; | |||
overflow-x: scroll; | |||
width: 100%; | |||
min-height: 350px; | |||
> .ui.segment.loading { | |||
border: 0; | |||
z-index: 1; | |||
min-height: 246px; | |||
} | |||
h2 { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
} | |||
.color-buttons { | |||
margin-right: 0; | |||
@@ -12,11 +25,49 @@ | |||
padding-bottom: 10px; | |||
} | |||
#flow-select-refs-dropdown { | |||
border-top-right-radius: 0; | |||
border-bottom-right-radius: 0; | |||
min-width: 250px; | |||
border-right: none; | |||
.ui.label { | |||
max-width: 180px; | |||
display: inline-flex !important; | |||
align-items: center; | |||
.truncate { | |||
display: inline-block; | |||
max-width: 140px; | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
vertical-align: top; | |||
white-space: nowrap; | |||
} | |||
} | |||
.dropdown.icon { | |||
display: none; | |||
} | |||
.default.text { | |||
padding-top: 4px; | |||
padding-bottom: 4px; | |||
} | |||
input.search { | |||
position: relative; | |||
top: 1px; | |||
} | |||
} | |||
li { | |||
list-style-type: none; | |||
height: 20px; | |||
line-height: 20px; | |||
height: 24px; | |||
line-height: 24px; | |||
white-space: nowrap; | |||
display: flex; | |||
align-items: center; | |||
.node-relation { | |||
font-family: "Bitstream Vera Sans Mono", "Courier", monospace; | |||
@@ -31,10 +82,6 @@ | |||
font-size: 80%; | |||
} | |||
a { | |||
color: #000000; | |||
} | |||
a:hover { | |||
text-decoration: underline; | |||
} | |||
@@ -59,16 +106,39 @@ | |||
#rev-list { | |||
margin: 0; | |||
padding: 0 5px; | |||
min-width: 95%; | |||
padding: 0; | |||
width: 100%; | |||
li.highlight, | |||
li.hover { | |||
li.highlight.hover { | |||
background-color: rgba(0, 0, 0, .05); | |||
} | |||
li.highlight.hover { | |||
background-color: rgba(0, 0, 0, .1); | |||
.tags a.button { | |||
padding: 2px 4px; | |||
} | |||
.sha.label { | |||
padding-top: 5px; | |||
padding-bottom: 3px; | |||
} | |||
.sha.label .shortsha { | |||
padding-top: 0; | |||
} | |||
.sha.label .shortsha-pad { | |||
padding-right: 10px; | |||
} | |||
.sha.label .ui.detail.icon.button { | |||
padding-top: 3px; | |||
margin-top: -5px; | |||
padding-bottom: 1px; | |||
} | |||
.author .ui.avatar.image { | |||
width: auto; | |||
height: 18px; | |||
} | |||
} | |||
@@ -1360,10 +1360,6 @@ td.blob-hunk { | |||
} | |||
} | |||
input { | |||
background: #2e323e; | |||
} | |||
.settings .key.list .item:not(:first-child) { | |||
border-top: 1px solid var(--color-secondary); | |||
} | |||
@@ -1608,6 +1604,12 @@ a.blob-excerpt:hover { | |||
color: #dbdbdb; | |||
} | |||
.ui.active.label { | |||
background: #393d4a; | |||
border-color: #393d4a; | |||
color: #dbdbdb; | |||
} | |||
a.ui.label:hover, | |||
a.ui.labels .label:hover { | |||
background-color: #505667 !important; | |||
@@ -1617,6 +1619,7 @@ a.ui.labels .label:hover { | |||
.sha.label, | |||
.repository #repo-files-table .sha.label, | |||
.repository #commits-table td.sha .sha.label, | |||
#rev-list .sha.label, | |||
.repository .timeline-item.commits-list .singular-commit .sha.label, | |||
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label { | |||
border-color: #505667; | |||
@@ -1624,6 +1627,7 @@ a.ui.labels .label:hover { | |||
.sha.label.isSigned .detail.icon, | |||
.repository #commits-table td.sha .sha.label.isSigned .detail.icon, | |||
#rev-list .sha.label.isSigned .detail.icon, | |||
.repository #repo-files-table .sha.label.isSigned .detail.icon, | |||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned .detail.icon, | |||
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned .detail.icon { | |||
@@ -1743,14 +1747,6 @@ a.ui.labels .label:hover { | |||
color: var(--color-secondary-dark-6); | |||
} | |||
#git-graph-container li a { | |||
color: #c79575; | |||
} | |||
#git-graph-container li .author { | |||
color: #c79575; | |||
} | |||
.ui.header .sub.header { | |||
color: var(--color-secondary-dark-6); | |||
} | |||
@@ -1970,6 +1966,10 @@ a.ui.labels .label:hover { | |||
} | |||
} | |||
.ui.loading.segment:before { | |||
background: #353945; | |||
} | |||
.ui.popup { | |||
background-color: #383c4a; | |||
color: var(--color-secondary-dark-6); | |||
@@ -2053,6 +2053,10 @@ img[src$="/img/matrix.svg"] { | |||
filter: invert(80%); | |||
} | |||
#git-graph-container li .time { | |||
color: #6a737d; | |||
} | |||
#git-graph-container.monochrome #rel-container .flow-group { | |||
stroke: dimgrey; | |||
fill: dimgrey; | |||
@@ -2077,11 +2081,6 @@ img[src$="/img/matrix.svg"] { | |||
} | |||
} | |||
#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); | |||
} | |||