mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-10 13:28:24 +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>
92 lines
3.1 KiB
Go
92 lines
3.1 KiB
Go
// Copyright 2025 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package git
|
|
|
|
import (
|
|
"context"
|
|
|
|
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"
|
|
asymkey_service "gitea.dev/services/asymkey"
|
|
)
|
|
|
|
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
|
|
func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, oldCommits []*gituser.UserCommit, repoTrustModel repo_model.TrustModelType) ([]*asymkey_model.SignCommit, error) {
|
|
newCommits := make([]*asymkey_model.SignCommit, 0, len(oldCommits))
|
|
keyMap := map[string]bool{}
|
|
|
|
emails := make(container.Set[string])
|
|
for _, c := range oldCommits {
|
|
if c.GitCommit.Committer != nil {
|
|
emails.Add(c.GitCommit.Committer.Email)
|
|
}
|
|
}
|
|
|
|
emailUsers, err := user_model.GetUsersByEmails(ctx, emails.Values())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, c := range oldCommits {
|
|
committerUser := emailUsers.GetByEmail(c.GitCommit.Committer.Email) // FIXME: why GetUserCommitsByGitCommits uses "Author", but ParseCommitsWithSignature uses "Committer"?
|
|
signCommit := &asymkey_model.SignCommit{
|
|
UserCommit: c,
|
|
Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.GitCommit, committerUser),
|
|
}
|
|
|
|
isOwnerMemberCollaborator := func(user *user_model.User) (bool, error) {
|
|
return repo_model.IsOwnerMemberCollaborator(ctx, repo, user.ID)
|
|
}
|
|
|
|
_ = asymkey_model.CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap)
|
|
|
|
newCommits = append(newCommits, signCommit)
|
|
}
|
|
return newCommits, nil
|
|
}
|
|
|
|
// ConvertFromGitCommit converts git commits into SignCommitWithStatuses
|
|
func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository, currentRef git.RefName) ([]*git_model.SignCommitWithStatuses, error) {
|
|
userCommits, err := gituser.GetUserCommitsByGitCommits(ctx, commits, repo.Link(), currentRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signedCommits, err := ParseCommitsWithSignature(
|
|
ctx,
|
|
repo,
|
|
userCommits,
|
|
repo.GetTrustModel(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ParseCommitsWithStatus(ctx, signedCommits, repo)
|
|
}
|
|
|
|
// ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state
|
|
func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.SignCommit, repo *repo_model.Repository) ([]*git_model.SignCommitWithStatuses, error) {
|
|
newCommits := make([]*git_model.SignCommitWithStatuses, 0, len(oldCommits))
|
|
|
|
for _, c := range oldCommits {
|
|
commit := &git_model.SignCommitWithStatuses{
|
|
SignCommit: c,
|
|
}
|
|
statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commit.GitCommit.ID.String(), db.ListOptionsAll)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
commit.Statuses = statuses
|
|
commit.Status = git_model.CalcCommitStatus(statuses)
|
|
newCommits = append(newCommits, commit)
|
|
}
|
|
return newCommits, nil
|
|
}
|