mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-01 02:30:22 +00:00
43390e802c
Show only author and co-authors without committer, and deduplicate the same user with multiple email addresses. The commit list "Author" column should not show the committer. This is somewhat misleading, and arguably showing it on the individual commit page is sufficient and consistent with other forges. The same user with multiple email addresses often happens when DEFAULT_KEEP_EMAIL_PRIVATE is enabled and Gitea does not use an actual email address by default for edits. Showing the same avatar and name twice is not helpful then. Ref #37594 Fix #38488 --- Before <img width="4088" height="2138" alt="before" src="https://github.com/user-attachments/assets/a9e919f4-72d0-495f-a0b5-ec094f148f5a" /> After <img width="4124" height="2142" alt="after" src="https://github.com/user-attachments/assets/7212e527-ab83-440a-a8a4-95b5a79b3580" /> --------- Co-authored-by: bircni <bircni@icloud.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
280 lines
6.9 KiB
Go
280 lines
6.9 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package gitgraph
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
asymkey_model "gitea.dev/models/asymkey"
|
|
"gitea.dev/models/db"
|
|
git_model "gitea.dev/models/git"
|
|
"gitea.dev/models/gituser"
|
|
repo_model "gitea.dev/models/repo"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/container"
|
|
"gitea.dev/modules/git"
|
|
"gitea.dev/modules/log"
|
|
asymkey_service "gitea.dev/services/asymkey"
|
|
)
|
|
|
|
// NewGraph creates a basic graph
|
|
func NewGraph() *Graph {
|
|
graph := &Graph{}
|
|
graph.relationCommit = &Commit{
|
|
Row: -1,
|
|
Column: -1,
|
|
}
|
|
graph.Flows = map[int64]*Flow{}
|
|
return graph
|
|
}
|
|
|
|
// Graph represents a collection of flows
|
|
type Graph struct {
|
|
Flows map[int64]*Flow
|
|
Commits []*Commit
|
|
MinRow int
|
|
MinColumn int
|
|
MaxRow int
|
|
MaxColumn int
|
|
relationCommit *Commit
|
|
}
|
|
|
|
// Width returns the width of the graph
|
|
func (graph *Graph) Width() int {
|
|
return graph.MaxColumn - graph.MinColumn + 1
|
|
}
|
|
|
|
// Height returns the height of the graph
|
|
func (graph *Graph) Height() int {
|
|
return graph.MaxRow - graph.MinRow + 1
|
|
}
|
|
|
|
// AddGlyph adds glyph to flows
|
|
func (graph *Graph) AddGlyph(row, column int, flowID int64, color int, glyph byte) {
|
|
flow, ok := graph.Flows[flowID]
|
|
if !ok {
|
|
flow = NewFlow(flowID, color, row, column)
|
|
graph.Flows[flowID] = flow
|
|
}
|
|
flow.AddGlyph(row, column, glyph)
|
|
|
|
if row < graph.MinRow {
|
|
graph.MinRow = row
|
|
}
|
|
if row > graph.MaxRow {
|
|
graph.MaxRow = row
|
|
}
|
|
if column < graph.MinColumn {
|
|
graph.MinColumn = column
|
|
}
|
|
if column > graph.MaxColumn {
|
|
graph.MaxColumn = column
|
|
}
|
|
}
|
|
|
|
// AddCommit adds a commit at row, column on flowID with the provided data
|
|
func (graph *Graph) AddCommit(row, column int, flowID int64, data []byte) error {
|
|
commit, err := NewCommit(row, column, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
commit.Flow = flowID
|
|
graph.Commits = append(graph.Commits, commit)
|
|
|
|
graph.Flows[flowID].Commits = append(graph.Flows[flowID].Commits, commit)
|
|
return nil
|
|
}
|
|
|
|
// 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(ctx context.Context, repository *repo_model.Repository, gitRepo *git.Repository) error {
|
|
var err error
|
|
emailSet := make(container.Set[string])
|
|
keyMap := map[string]bool{}
|
|
|
|
for _, c := range graph.Commits {
|
|
if len(c.Rev) == 0 {
|
|
continue
|
|
}
|
|
c.Commit, err = gitRepo.GetCommit(ctx, c.Rev)
|
|
if err != nil {
|
|
return fmt.Errorf("GetCommit: %s Error: %w", c.Rev, err)
|
|
}
|
|
if c.Commit.Author != nil {
|
|
emailSet.Add(c.Commit.Author.Email)
|
|
}
|
|
for _, sig := range c.Commit.AllAuthorIdentities() {
|
|
emailSet.Add(sig.Email)
|
|
}
|
|
}
|
|
|
|
emailUserMap, err := user_model.GetUsersByEmails(ctx, emailSet.Values())
|
|
if err != nil {
|
|
log.Error("GetUsersByEmails: %v", err)
|
|
}
|
|
|
|
for _, c := range graph.Commits {
|
|
if c.Commit == nil {
|
|
continue
|
|
}
|
|
|
|
c.User = emailUserMap.GetByEmail(c.Commit.Author.Email)
|
|
c.AvatarStackData = gituser.BuildAvatarStackData(ctx, c.Commit.AllAuthorIdentities(), emailUserMap)
|
|
|
|
c.Verification = asymkey_service.ParseCommitWithSignature(ctx, c.Commit)
|
|
|
|
_ = asymkey_model.CalculateTrustStatus(c.Verification, repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
|
|
return repo_model.IsOwnerMemberCollaborator(ctx, repository, user.ID)
|
|
}, &keyMap)
|
|
|
|
statuses, err := git_model.GetLatestCommitStatus(ctx, repository.ID, c.Commit.ID.String(), db.ListOptionsAll)
|
|
if err != nil {
|
|
log.Error("GetLatestCommitStatus: %v", err)
|
|
} else {
|
|
c.Status = git_model.CalcCommitStatus(statuses)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NewFlow creates a new flow
|
|
func NewFlow(flowID int64, color, row, column int) *Flow {
|
|
return &Flow{
|
|
ID: flowID,
|
|
ColorNumber: color,
|
|
MinRow: row,
|
|
MinColumn: column,
|
|
MaxRow: row,
|
|
MaxColumn: column,
|
|
}
|
|
}
|
|
|
|
// Flow represents a series of glyphs
|
|
type Flow struct {
|
|
ID int64
|
|
ColorNumber int
|
|
Glyphs []Glyph
|
|
Commits []*Commit
|
|
MinRow int
|
|
MinColumn int
|
|
MaxRow int
|
|
MaxColumn int
|
|
}
|
|
|
|
// Color16 wraps the color numbers around mod 16
|
|
func (flow *Flow) Color16() int {
|
|
return flow.ColorNumber % 16
|
|
}
|
|
|
|
// AddGlyph adds glyph at row and column
|
|
func (flow *Flow) AddGlyph(row, column int, glyph byte) {
|
|
if row < flow.MinRow {
|
|
flow.MinRow = row
|
|
}
|
|
if row > flow.MaxRow {
|
|
flow.MaxRow = row
|
|
}
|
|
if column < flow.MinColumn {
|
|
flow.MinColumn = column
|
|
}
|
|
if column > flow.MaxColumn {
|
|
flow.MaxColumn = column
|
|
}
|
|
|
|
flow.Glyphs = append(flow.Glyphs, Glyph{
|
|
row,
|
|
column,
|
|
glyph,
|
|
})
|
|
}
|
|
|
|
// Glyph represents a coordinate and glyph
|
|
type Glyph struct {
|
|
Row int
|
|
Column int
|
|
Glyph byte
|
|
}
|
|
|
|
// RelationCommit represents an empty relation commit
|
|
var RelationCommit = &Commit{
|
|
Row: -1,
|
|
}
|
|
|
|
func parseGitTime(timeStr string) time.Time {
|
|
t, err := time.Parse(time.RFC3339, timeStr)
|
|
if err != nil {
|
|
return time.Unix(0, 0)
|
|
}
|
|
return t
|
|
}
|
|
|
|
// NewCommit creates a new commit from a provided line
|
|
func NewCommit(row, column int, line []byte) (*Commit, error) {
|
|
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)
|
|
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: parseGitTime(string(data[2])),
|
|
// 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 after, ok := strings.CutPrefix(refName, "tag: "); ok {
|
|
refName = after
|
|
} else {
|
|
refName = strings.TrimPrefix(refName, "HEAD -> ")
|
|
}
|
|
refs = append(refs, git.Reference{
|
|
Name: refName,
|
|
})
|
|
}
|
|
return refs
|
|
}
|
|
|
|
// Commit represents a commit at coordinate X, Y with the data
|
|
type Commit struct {
|
|
Commit *git.Commit
|
|
User *user_model.User // author
|
|
AvatarStackData *gituser.AvatarStackData
|
|
Verification *asymkey_model.CommitVerification
|
|
Status *git_model.CommitStatus
|
|
Flow int64
|
|
Row int
|
|
Column int
|
|
Refs []git.Reference
|
|
Rev string
|
|
Date time.Time // author date from "%ad"
|
|
ShortRev string
|
|
Subject string
|
|
}
|
|
|
|
// OnlyRelation returns whether this a relation only commit
|
|
func (c *Commit) OnlyRelation() bool {
|
|
return c.Row == -1
|
|
}
|