Multiple GitGraph improvements: Exclude PR heads, Add branch/PR links, Show only certain branches, (#12766)
* 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>
This commit is contained in:
		
							parent
							
								
									d4e0b28655
								
							
						
					
					
						commit
						c05a8abc76
					
				| @ -37,6 +37,12 @@ func (p *Pagination) AddParam(ctx *Context, paramKey string, ctxKey string) { | |||||||
| 	p.urlParams = append(p.urlParams, urlParam) | 	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
 | // GetParams returns the configured URL params
 | ||||||
| func (p *Pagination) GetParams() template.URL { | func (p *Pagination) GetParams() template.URL { | ||||||
| 	return template.URL(strings.Join(p.urlParams, "&")) | 	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
 | // BranchNameSubURL sub-URL for the BranchName field
 | ||||||
| func (r *Repository) BranchNameSubURL() string { | func (r *Repository) BranchNameSubURL() string { | ||||||
| 	switch { | 	switch { | ||||||
|  | |||||||
| @ -262,8 +262,19 @@ func CommitChangesWithArgs(repoPath string, args []string, opts CommitChangesOpt | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AllCommitsCount returns count of all commits in repository
 | // AllCommitsCount returns count of all commits in repository
 | ||||||
| func AllCommitsCount(repoPath string) (int64, error) { | func AllCommitsCount(repoPath string, hidePRRefs bool, files ...string) (int64, error) { | ||||||
| 	stdout, err := NewCommand("rev-list", "--all", "--count").RunInDir(repoPath) | 	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 { | 	if err != nil { | ||||||
| 		return 0, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
| @ -271,7 +282,8 @@ func AllCommitsCount(repoPath string) (int64, error) { | |||||||
| 	return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) | 	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 := NewCommand("rev-list", "--count") | ||||||
| 	cmd.AddArguments(revision...) | 	cmd.AddArguments(revision...) | ||||||
| 	if len(relpath) > 0 { | 	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.
 | // CommitsCount returns number of total commits of until given revision.
 | ||||||
| func CommitsCount(repoPath, revision string) (int64, error) { | func CommitsCount(repoPath string, revision ...string) (int64, error) { | ||||||
| 	return commitsCount(repoPath, []string{revision}, []string{}) | 	return CommitsCountFiles(repoPath, revision, []string{}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // CommitsCount returns number of total commits of until current revision.
 | // CommitsCount returns number of total commits of until current revision.
 | ||||||
|  | |||||||
| @ -4,6 +4,8 @@ | |||||||
| 
 | 
 | ||||||
| package git | package git | ||||||
| 
 | 
 | ||||||
|  | import "strings" | ||||||
|  | 
 | ||||||
| // Reference represents a Git ref.
 | // Reference represents a Git ref.
 | ||||||
| type Reference struct { | type Reference struct { | ||||||
| 	Name   string | 	Name   string | ||||||
| @ -16,3 +18,44 @@ type Reference struct { | |||||||
| func (ref *Reference) Commit() (*Commit, error) { | func (ref *Reference) Commit() (*Commit, error) { | ||||||
| 	return ref.repo.getCommit(ref.Object) | 	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
 | // GetAllCommitsCount returns count of all commits in repository
 | ||||||
| func (repo *Repository) GetAllCommitsCount() (int64, error) { | func (repo *Repository) GetAllCommitsCount() (int64, error) { | ||||||
| 	return AllCommitsCount(repo.Path) | 	return AllCommitsCount(repo.Path, false) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (repo *Repository) parsePrettyFormatLogToList(logs []byte) (*list.List, error) { | 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
 | // FileCommitsCount return the number of files at a revison
 | ||||||
| func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) { | 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
 | // 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
 | // CommitsCountBetween return numbers of commits between two commits
 | ||||||
| func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) { | 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") { | 	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.
 | 		// 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...
 | 		// 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 | 	return count, err | ||||||
|  | |||||||
| @ -17,23 +17,42 @@ import ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // GetCommitGraph return a list of commit (GraphItems) from all branches
 | // GetCommitGraph return a list of commit (GraphItems) from all branches
 | ||||||
| func GetCommitGraph(r *git.Repository, page int, maxAllowedColors int) (*Graph, error) { | func GetCommitGraph(r *git.Repository, page int, maxAllowedColors int, hidePRRefs bool, branches, files []string) (*Graph, error) { | ||||||
| 	format := "DATA:%d|%H|%ad|%an|%ae|%h|%s" | 	format := "DATA:%D|%H|%ad|%h|%s" | ||||||
| 
 | 
 | ||||||
| 	if page == 0 { | 	if page == 0 { | ||||||
| 		page = 1 | 		page = 1 | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	graphCmd := git.NewCommand("log") | 	args := make([]string, 0, 12+len(branches)+len(files)) | ||||||
| 	graphCmd.AddArguments("--graph", | 
 | ||||||
| 		"--date-order", | 	args = append(args, "--graph", "--date-order", "--decorate=full") | ||||||
| 		"--all", | 
 | ||||||
|  | 	if hidePRRefs { | ||||||
|  | 		args = append(args, "--exclude=refs/pull/*") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(branches) == 0 { | ||||||
|  | 		args = append(args, "--all") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	args = append(args, | ||||||
| 		"-C", | 		"-C", | ||||||
| 		"-M", | 		"-M", | ||||||
| 		fmt.Sprintf("-n %d", setting.UI.GraphMaxCommitNum*page), | 		fmt.Sprintf("-n %d", setting.UI.GraphMaxCommitNum*page), | ||||||
| 		"--date=iso", | 		"--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() | 	graph := NewGraph() | ||||||
| 
 | 
 | ||||||
| 	stderr := new(strings.Builder) | 	stderr := new(strings.Builder) | ||||||
|  | |||||||
| @ -7,6 +7,10 @@ package gitgraph | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // NewGraph creates a basic graph
 | // NewGraph creates a basic graph
 | ||||||
| @ -77,6 +81,48 @@ func (graph *Graph) AddCommit(row, column int, flowID int64, data []byte) error | |||||||
| 	return nil | 	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
 | // NewFlow creates a new flow
 | ||||||
| func NewFlow(flowID int64, color, row, column int) *Flow { | func NewFlow(flowID int64, color, row, column int) *Flow { | ||||||
| 	return &Flow{ | 	return &Flow{ | ||||||
| @ -142,40 +188,58 @@ var RelationCommit = &Commit{ | |||||||
| 
 | 
 | ||||||
| // NewCommit creates a new commit from a provided line
 | // NewCommit creates a new commit from a provided line
 | ||||||
| func NewCommit(row, column int, line []byte) (*Commit, error) { | func NewCommit(row, column int, line []byte) (*Commit, error) { | ||||||
| 	data := bytes.SplitN(line, []byte("|"), 7) | 	data := bytes.SplitN(line, []byte("|"), 5) | ||||||
| 	if len(data) < 7 { | 	if len(data) < 5 { | ||||||
| 		return nil, fmt.Errorf("malformed data section on line %d with commit: %s", row, string(line)) | 		return nil, fmt.Errorf("malformed data section on line %d with commit: %s", row, string(line)) | ||||||
| 	} | 	} | ||||||
| 	return &Commit{ | 	return &Commit{ | ||||||
| 		Row:    row, | 		Row:    row, | ||||||
| 		Column: column, | 		Column: column, | ||||||
| 		// 0 matches git log --pretty=format:%d => ref names, like the --decorate option of git-log(1)
 | 		// 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
 | 		// 1 matches git log --pretty=format:%H => commit hash
 | ||||||
| 		Rev: string(data[1]), | 		Rev: string(data[1]), | ||||||
| 		// 2 matches git log --pretty=format:%ad => author date (format respects --date= option)
 | 		// 2 matches git log --pretty=format:%ad => author date (format respects --date= option)
 | ||||||
| 		Date: string(data[2]), | 		Date: string(data[2]), | ||||||
| 		// 3 matches git log --pretty=format:%an => author name
 | 		// 3 matches git log --pretty=format:%h => abbreviated commit hash
 | ||||||
| 		Author: string(data[3]), | 		ShortRev: string(data[3]), | ||||||
| 		// 4 matches git log --pretty=format:%ae => author email
 | 		// 4 matches git log --pretty=format:%s => subject
 | ||||||
| 		AuthorEmail: string(data[4]), | 		Subject: 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 | 	}, 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
 | // Commit represents a commit at co-ordinate X, Y with the data
 | ||||||
| type Commit struct { | type Commit struct { | ||||||
|  | 	Commit       *git.Commit | ||||||
|  | 	User         *models.User | ||||||
|  | 	Verification *models.CommitVerification | ||||||
|  | 	Status       *models.CommitStatus | ||||||
| 	Flow         int64 | 	Flow         int64 | ||||||
| 	Row          int | 	Row          int | ||||||
| 	Column       int | 	Column       int | ||||||
| 	Branch      string | 	Refs         []git.Reference | ||||||
| 	Rev          string | 	Rev          string | ||||||
| 	Date         string | 	Date         string | ||||||
| 	Author      string |  | ||||||
| 	AuthorEmail string |  | ||||||
| 	ShortRev     string | 	ShortRev     string | ||||||
| 	Subject      string | 	Subject      string | ||||||
| } | } | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ func BenchmarkGetCommitGraph(b *testing.B) { | |||||||
| 	defer currentRepo.Close() | 	defer currentRepo.Close() | ||||||
| 
 | 
 | ||||||
| 	for i := 0; i < b.N; i++ { | 	for i := 0; i < b.N; i++ { | ||||||
| 		graph, err := GetCommitGraph(currentRepo, 1, 0) | 		graph, err := GetCommitGraph(currentRepo, 1, 0, false, nil, nil) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			b.Error("Could get commit graph") | 			b.Error("Could get commit graph") | ||||||
| 		} | 		} | ||||||
| @ -34,7 +34,7 @@ func BenchmarkGetCommitGraph(b *testing.B) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func BenchmarkParseCommitString(b *testing.B) { | func BenchmarkParseCommitString(b *testing.B) { | ||||||
| 	testString := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Kjell Kvinge|kjell@kvinge.biz|4e61bac|Add route for graph" | 	testString := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|4e61bac|Add route for graph" | ||||||
| 
 | 
 | ||||||
| 	parser := &Parser{} | 	parser := &Parser{} | ||||||
| 	parser.Reset() | 	parser.Reset() | ||||||
| @ -44,7 +44,7 @@ func BenchmarkParseCommitString(b *testing.B) { | |||||||
| 		if err := parser.AddLineToGraph(graph, 0, []byte(testString)); err != nil { | 		if err := parser.AddLineToGraph(graph, 0, []byte(testString)); err != nil { | ||||||
| 			b.Error("could not parse teststring") | 			b.Error("could not parse teststring") | ||||||
| 		} | 		} | ||||||
| 		if graph.Flows[1].Commits[0].Author != "Kjell Kvinge" { | 		if graph.Flows[1].Commits[0].Rev != "4e61bacab44e9b4730e44a6615d04098dd3a8eaf" { | ||||||
| 			b.Error("Did not get expected data") | 			b.Error("Did not get expected data") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -244,7 +244,7 @@ func TestParseGlyphs(t *testing.T) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestCommitStringParsing(t *testing.T) { | func TestCommitStringParsing(t *testing.T) { | ||||||
| 	dataFirstPart := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Author|user@mail.something|4e61bac|" | 	dataFirstPart := "* DATA:|4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|4e61bac|" | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		shouldPass    bool | 		shouldPass    bool | ||||||
| 		testName      string | 		testName      string | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ import ( | |||||||
| 	"mime" | 	"mime" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | 	"reflect" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"runtime" | 	"runtime" | ||||||
| 	"strings" | 	"strings" | ||||||
| @ -310,6 +311,26 @@ func NewFuncMap() []template.FuncMap { | |||||||
| 				"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond), | 				"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 { | 		"contain": func(s []int64, id int64) bool { | ||||||
| 			for i := 0; i < len(s); i++ { | 			for i := 0; i < len(s); i++ { | ||||||
| 				if s[i] == id { | 				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 | stored_lfs = Stored with Git LFS | ||||||
| symbolic_link = Symbolic link | symbolic_link = Symbolic link | ||||||
| commit_graph = Commit Graph | commit_graph = Commit Graph | ||||||
|  | commit_graph.select = Select branches | ||||||
|  | commit_graph.hide_pr_refs = Hide Pull Requests | ||||||
| commit_graph.monochrome = Mono | commit_graph.monochrome = Mono | ||||||
| commit_graph.color = Color | commit_graph.color = Color | ||||||
| blame = Blame | blame = Blame | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ import ( | |||||||
| const ( | const ( | ||||||
| 	tplCommits    base.TplName = "repo/commits" | 	tplCommits    base.TplName = "repo/commits" | ||||||
| 	tplGraph      base.TplName = "repo/graph" | 	tplGraph      base.TplName = "repo/graph" | ||||||
|  | 	tplGraphDiv   base.TplName = "repo/graph/div" | ||||||
| 	tplCommitPage base.TplName = "repo/commit_page" | 	tplCommitPage base.TplName = "repo/commit_page" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -88,6 +89,7 @@ func Commits(ctx *context.Context) { | |||||||
| 
 | 
 | ||||||
| // Graph render commit graph - show commits from all branches.
 | // Graph render commit graph - show commits from all branches.
 | ||||||
| func Graph(ctx *context.Context) { | func Graph(ctx *context.Context) { | ||||||
|  | 	ctx.Data["Title"] = ctx.Tr("repo.commit_graph") | ||||||
| 	ctx.Data["PageIsCommits"] = true | 	ctx.Data["PageIsCommits"] = true | ||||||
| 	ctx.Data["PageIsViewCode"] = true | 	ctx.Data["PageIsViewCode"] = true | ||||||
| 	mode := strings.ToLower(ctx.QueryTrim("mode")) | 	mode := strings.ToLower(ctx.QueryTrim("mode")) | ||||||
| @ -95,6 +97,18 @@ func Graph(ctx *context.Context) { | |||||||
| 		mode = "color" | 		mode = "color" | ||||||
| 	} | 	} | ||||||
| 	ctx.Data["Mode"] = mode | 	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() | 	commitsCount, err := ctx.Repo.GetCommitsCount() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -102,28 +116,60 @@ func Graph(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	allCommitsCount, err := ctx.Repo.GitRepo.GetAllCommitsCount() | 	graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(hidePRRefs, realBranches, files) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.ServerError("GetAllCommitsCount", err) | 		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 | 			return | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	page := ctx.QueryInt("page") | 	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 { | 	if err != nil { | ||||||
| 		ctx.ServerError("GetCommitGraph", err) | 		ctx.ServerError("GetCommitGraph", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if err := graph.LoadAndProcessCommits(ctx.Repo.Repository, ctx.Repo.GitRepo); err != nil { | ||||||
|  | 		ctx.ServerError("LoadAndProcessCommits", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	ctx.Data["Graph"] = graph | 	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["Username"] = ctx.Repo.Owner.Name | ||||||
| 	ctx.Data["Reponame"] = ctx.Repo.Repository.Name | 	ctx.Data["Reponame"] = ctx.Repo.Repository.Name | ||||||
| 	ctx.Data["CommitCount"] = commitsCount | 	ctx.Data["CommitCount"] = commitsCount | ||||||
| 	ctx.Data["Branch"] = ctx.Repo.BranchName | 	ctx.Data["Branch"] = ctx.Repo.BranchName | ||||||
| 	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, "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 | 	ctx.Data["Page"] = paginator | ||||||
|  | 	if ctx.QueryBool("div-only") { | ||||||
|  | 		ctx.HTML(200, tplGraphDiv) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	ctx.HTML(200, tplGraph) | 	ctx.HTML(200, tplGraph) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,60 +3,61 @@ | |||||||
| 	{{template "repo/header" .}} | 	{{template "repo/header" .}} | ||||||
| 	<div class="ui container"> | 	<div class="ui container"> | ||||||
| 		<div id="git-graph-container" class="ui segment{{if eq .Mode "monochrome"}} monochrome{{end}}"> | 		<div id="git-graph-container" class="ui segment{{if eq .Mode "monochrome"}} monochrome{{end}}"> | ||||||
| 			<h2 class="ui header dividing">{{.i18n.Tr "repo.commit_graph"}} | 			<h2 class="ui header dividing"> | ||||||
| 				<div class="ui right"> | 				{{.i18n.Tr "repo.commit_graph"}} | ||||||
| 				<div class="ui icon buttons tiny color-buttons"> | 				<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> | 					<div class="ui multiple selection search dropdown" id="flow-select-refs-dropdown"> | ||||||
| 						<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> | 						<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> | 							</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> | 				</div> | ||||||
| 			</h2> | 			</h2> | ||||||
| 			<div class="ui dividing"></div> | 			<div class="ui dividing"></div> | ||||||
| 			<div id="rel-container"> | 			<div class="ui segment loading hide" id="loading-indicator"></div> | ||||||
| 				<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"> | 			{{ template "repo/graph/svgcontainer" .}} | ||||||
| 					{{range $flowid, $flow := .Graph.Flows}} | 			{{ template "repo/graph/commits" .}} | ||||||
| 						<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> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| {{template "base/paginate" .}} | <div id="pagination"> | ||||||
|  | 	{{template "base/paginate" .}} | ||||||
|  | </div> | ||||||
| {{template "base/footer" .}} | {{template "base/footer" .}} | ||||||
|  | |||||||
							
								
								
									
										80
									
								
								templates/repo/graph/commits.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								templates/repo/graph/commits.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||||
							
								
								
									
										7
									
								
								templates/repo/graph/div.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								templates/repo/graph/div.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | <div> | ||||||
|  | 	{{template "repo/graph/svgcontainer" .}} | ||||||
|  | 	{{template "repo/graph/commits" .}} | ||||||
|  | 	<div id="pagination"> | ||||||
|  | 		{{template "base/paginate" .}} | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
							
								
								
									
										24
									
								
								templates/repo/graph/svgcontainer.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								templates/repo/graph/svgcontainer.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -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); |       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) => { |   $('#git-graph-container').on('mouseenter', '#rev-list li', (e) => { | ||||||
|     const flow = $(e.currentTarget).data('flow'); |     const flow = $(e.currentTarget).data('flow'); | ||||||
|     if (flow === 0) return; |     if (flow === 0) return; | ||||||
|  | |||||||
| @ -1391,6 +1391,10 @@ table th[data-sortt-desc] { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .dropdown .ui.label { | ||||||
|  |   margin-left: 0 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .ui.dropdown .menu .item { | .ui.dropdown .menu .item { | ||||||
|   border-radius: 0; |   border-radius: 0; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1462,6 +1462,7 @@ | |||||||
| 
 | 
 | ||||||
|   #commits-table td.sha .sha.label, |   #commits-table td.sha .sha.label, | ||||||
|   #repo-files-table .sha.label, |   #repo-files-table .sha.label, | ||||||
|  |   #rev-list .sha.label, | ||||||
|   .timeline-item.commits-list .singular-commit .sha.label { |   .timeline-item.commits-list .singular-commit .sha.label { | ||||||
|     border: 1px solid #bbbbbb; |     border: 1px solid #bbbbbb; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,8 +1,21 @@ | |||||||
| #git-graph-container { | #git-graph-container { | ||||||
|   float: left; |   float: left; | ||||||
|   display: block; |   display: block; | ||||||
|   overflow-x: auto; |   overflow-x: scroll; | ||||||
|   width: 100%; |   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 { |   .color-buttons { | ||||||
|     margin-right: 0; |     margin-right: 0; | ||||||
| @ -12,11 +25,49 @@ | |||||||
|     padding-bottom: 10px; |     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 { |   li { | ||||||
|     list-style-type: none; |     list-style-type: none; | ||||||
|     height: 20px; |     height: 24px; | ||||||
|     line-height: 20px; |     line-height: 24px; | ||||||
|     white-space: nowrap; |     white-space: nowrap; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
| 
 | 
 | ||||||
|     .node-relation { |     .node-relation { | ||||||
|       font-family: "Bitstream Vera Sans Mono", "Courier", monospace; |       font-family: "Bitstream Vera Sans Mono", "Courier", monospace; | ||||||
| @ -31,10 +82,6 @@ | |||||||
|       font-size: 80%; |       font-size: 80%; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     a { |  | ||||||
|       color: #000000; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     a:hover { |     a:hover { | ||||||
|       text-decoration: underline; |       text-decoration: underline; | ||||||
|     } |     } | ||||||
| @ -59,16 +106,39 @@ | |||||||
| 
 | 
 | ||||||
|   #rev-list { |   #rev-list { | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     padding: 0 5px; |     padding: 0; | ||||||
|     min-width: 95%; |     width: 100%; | ||||||
| 
 | 
 | ||||||
|     li.highlight, |     li.highlight.hover { | ||||||
|     li.hover { |  | ||||||
|       background-color: rgba(0, 0, 0, .05); |       background-color: rgba(0, 0, 0, .05); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     li.highlight.hover { |     .tags a.button { | ||||||
|       background-color: rgba(0, 0, 0, .1); |       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) { | .settings .key.list .item:not(:first-child) { | ||||||
|   border-top: 1px solid var(--color-secondary); |   border-top: 1px solid var(--color-secondary); | ||||||
| } | } | ||||||
| @ -1608,6 +1604,12 @@ a.blob-excerpt:hover { | |||||||
|   color: #dbdbdb; |   color: #dbdbdb; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .ui.active.label { | ||||||
|  |   background: #393d4a; | ||||||
|  |   border-color: #393d4a; | ||||||
|  |   color: #dbdbdb; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| a.ui.label:hover, | a.ui.label:hover, | ||||||
| a.ui.labels .label:hover { | a.ui.labels .label:hover { | ||||||
|   background-color: #505667 !important; |   background-color: #505667 !important; | ||||||
| @ -1617,6 +1619,7 @@ a.ui.labels .label:hover { | |||||||
| .sha.label, | .sha.label, | ||||||
| .repository #repo-files-table .sha.label, | .repository #repo-files-table .sha.label, | ||||||
| .repository #commits-table td.sha .sha.label, | .repository #commits-table td.sha .sha.label, | ||||||
|  | #rev-list .sha.label, | ||||||
| .repository .timeline-item.commits-list .singular-commit .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 { | .repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label { | ||||||
|   border-color: #505667; |   border-color: #505667; | ||||||
| @ -1624,6 +1627,7 @@ a.ui.labels .label:hover { | |||||||
| 
 | 
 | ||||||
| .sha.label.isSigned .detail.icon, | .sha.label.isSigned .detail.icon, | ||||||
| .repository #commits-table td.sha .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 #repo-files-table .sha.label.isSigned .detail.icon, | ||||||
| .repository .timeline-item.commits-list .singular-commit .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 { | .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); |   color: var(--color-secondary-dark-6); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #git-graph-container li a { |  | ||||||
|   color: #c79575; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #git-graph-container li .author { |  | ||||||
|   color: #c79575; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ui.header .sub.header { | .ui.header .sub.header { | ||||||
|   color: var(--color-secondary-dark-6); |   color: var(--color-secondary-dark-6); | ||||||
| } | } | ||||||
| @ -1970,6 +1966,10 @@ a.ui.labels .label:hover { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .ui.loading.segment:before { | ||||||
|  |   background: #353945; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .ui.popup { | .ui.popup { | ||||||
|   background-color: #383c4a; |   background-color: #383c4a; | ||||||
|   color: var(--color-secondary-dark-6); |   color: var(--color-secondary-dark-6); | ||||||
| @ -2053,6 +2053,10 @@ img[src$="/img/matrix.svg"] { | |||||||
|   filter: invert(80%); |   filter: invert(80%); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #git-graph-container li .time { | ||||||
|  |   color: #6a737d; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #git-graph-container.monochrome #rel-container .flow-group { | #git-graph-container.monochrome #rel-container .flow-group { | ||||||
|   stroke: dimgrey; |   stroke: dimgrey; | ||||||
|   fill: 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 { | #git-graph-container #rev-list li.highlight.hover { | ||||||
|   background-color: rgba(255, 255, 255, .1); |   background-color: rgba(255, 255, 255, .1); | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user