diff --git a/modules/context/pagination.go b/modules/context/pagination.go index 9a6ad0b5c4..a6638f4086 100644 --- a/modules/context/pagination.go +++ b/modules/context/pagination.go @@ -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, "&")) diff --git a/modules/context/repo.go b/modules/context/repo.go index f34b05d1d0..0ef644b522 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -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 { diff --git a/modules/git/commit.go b/modules/git/commit.go index 87278af9c7..6425345ea8 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -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. diff --git a/modules/git/ref.go b/modules/git/ref.go index 67b56ac999..2a2798b18f 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -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 "" +} diff --git a/modules/git/repo.go b/modules/git/repo.go index 644ff09284..ae370d3da9 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -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) { diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 1f123c97fb..ee3b05447b 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -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 diff --git a/modules/gitgraph/graph.go b/modules/gitgraph/graph.go index 257e4f3af0..8505678639 100644 --- a/modules/gitgraph/graph.go +++ b/modules/gitgraph/graph.go @@ -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) diff --git a/modules/gitgraph/graph_models.go b/modules/gitgraph/graph_models.go index ea6ba96084..ba168ab19d 100644 --- a/modules/gitgraph/graph_models.go +++ b/modules/gitgraph/graph_models.go @@ -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 diff --git a/modules/gitgraph/graph_test.go b/modules/gitgraph/graph_test.go index ca9d653cee..c2726a731a 100644 --- a/modules/gitgraph/graph_test.go +++ b/modules/gitgraph/graph_test.go @@ -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 diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 03ec80f99c..e4107dfa9a 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -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 { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 815d69c518..b1447f3108 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 diff --git a/routers/repo/commit.go b/routers/repo/commit.go index d9547cc51d..5bb26ffe41 100644 --- a/routers/repo/commit.go +++ b/routers/repo/commit.go @@ -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) } diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl index 7d2ecb5a91..fa027adc03 100644 --- a/templates/repo/graph.tmpl +++ b/templates/repo/graph.tmpl @@ -3,60 +3,61 @@ {{template "repo/header" .}}