mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-10 05:20:28 +00:00
Parse `Co-authored-by:` trailers from commit messages and surface contributors as an avatar stack across the commit page, commits list, PR commits tab, latest-commit row, blame, graph, and dashboard feed. - Up to 10 visible 20px avatars, GitHub-style overlap (6px first stride, 4px between subsequent), `+N` chip for the rest. - Label: 1 → name; 2 → `<a> and <b>`; 3+ → `<N> people` opens a Tippy popup with all participants. - Names and avatars link to the repo's commits-by-author search; fall back to profile or `mailto:`. - Trailer parsing uses `net/mail.ParseAddress`, scans only the trailing paragraph, filters out the commit's own author/committer. - Drops the non-standard `Co-committed-by:` emission on squash merge and web edits. Devtest: `/devtest/coauthor-avatars`. Fixes #25521 ---- <img width="353" height="277" alt="image" src="https://github.com/user-attachments/assets/72092ceb-97ca-4b09-9557-0b72d3c5458e" /> <img width="533" height="328" src="https://github.com/user-attachments/assets/11d0c8f8-8b3f-4f2e-9993-879f1c06bcc5" /> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Giteabot <teabot@gitea.io>
65 lines
2.1 KiB
Go
65 lines
2.1 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package gituser
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
|
|
"gitea.dev/models/user"
|
|
"gitea.dev/modules/container"
|
|
"gitea.dev/modules/git"
|
|
)
|
|
|
|
// CommitParticipant is one participant of a commit (its author or a co-author):
|
|
// a git identity, optionally matched to a Gitea user.
|
|
type CommitParticipant struct {
|
|
GitIdentity *git.CommitIdentity // git identity (name/email), never nil
|
|
GiteaUser *user.User // matched Gitea user, nil if unmatched
|
|
}
|
|
|
|
// UserCommit represents a commit with matched of database "author" user.
|
|
type UserCommit struct {
|
|
GitCommit *git.Commit
|
|
AuthorUser *user.User
|
|
AvatarStackData *AvatarStackData
|
|
}
|
|
|
|
func RepoCommitSearchByEmailLink(repoLink string, ref git.RefName) string {
|
|
if curRefWebLinkPath := ref.RefWebLinkPath(); curRefWebLinkPath != "" {
|
|
return repoLink + "/commits/" + curRefWebLinkPath + "/search?q=" + url.QueryEscape("author:") + "{email}"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetUserCommitsByGitCommits checks if authors' e-mails of commits are corresponding to users.
|
|
func GetUserCommitsByGitCommits(ctx context.Context, gitCommits []*git.Commit, repoLink string, currentRef git.RefName) ([]*UserCommit, error) {
|
|
userCommits := make([]*UserCommit, 0, len(gitCommits))
|
|
emailSet := make(container.Set[string])
|
|
for _, c := range gitCommits {
|
|
emailSet.Add(c.Author.Email)
|
|
emailSet.Add(c.Committer.Email)
|
|
for _, p := range c.AllParticipantIdentities() {
|
|
emailSet.Add(p.Email)
|
|
}
|
|
}
|
|
|
|
emailUserMap, err := user.GetUsersByEmails(ctx, emailSet.Values())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
searchByEmailLink := RepoCommitSearchByEmailLink(repoLink, currentRef)
|
|
for _, c := range gitCommits {
|
|
uc := &UserCommit{
|
|
AuthorUser: emailUserMap.GetByEmail(c.Author.Email), // FIXME: why GetUserCommitsByGitCommits uses "Author", but ParseCommitsWithSignature uses "Committer"?
|
|
GitCommit: c,
|
|
AvatarStackData: BuildAvatarStackData(ctx, c.AllParticipantIdentities(), emailUserMap),
|
|
}
|
|
uc.AvatarStackData.SearchByEmailLink = searchByEmailLink
|
|
userCommits = append(userCommits, uc)
|
|
}
|
|
return userCommits, nil
|
|
}
|