0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-11 11:25:42 +02:00

feat(repo): display co-author stacks beside commit SHAs

Show co-author avatar stacks on commit SHA surfaces including commits list rows, graph, blame, and dashboard feeds, with repository-level data propagation and tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-05-07 21:53:01 +02:00
parent ac82797cd8
commit 10461b2f4e
9 changed files with 60 additions and 37 deletions

View File

@ -29,6 +29,7 @@ type PushCommit struct {
AuthorName string
CommitterEmail string
CommitterName string
CoAuthors []*git.Signature
Timestamp time.Time
}
@ -157,6 +158,7 @@ func CommitToPushCommit(commit *git.Commit) *PushCommit {
AuthorName: commit.Author.Name,
CommitterEmail: commit.Committer.Email,
CommitterName: commit.Committer.Name,
CoAuthors: commit.CoAuthorSignatures(),
Timestamp: commit.Author.When,
}
}

View File

@ -145,14 +145,18 @@ func TestCommitToPushCommit(t *testing.T) {
ID: sha1,
Author: sig,
Committer: sig,
CommitMessage: git.CommitMessage{MessageRaw: "Commit Message"},
CommitMessage: git.CommitMessage{MessageRaw: "Commit Message\n\nCo-authored-by: Jane Doe <jane@example.com>"},
})
assert.Equal(t, hexString, pushCommit.Sha1)
assert.Equal(t, "Commit Message", pushCommit.Message)
assert.Equal(t, "Commit Message\n\nCo-authored-by: Jane Doe <jane@example.com>", pushCommit.Message)
assert.Equal(t, "example@example.com", pushCommit.AuthorEmail)
assert.Equal(t, "John Doe", pushCommit.AuthorName)
assert.Equal(t, "example@example.com", pushCommit.CommitterEmail)
assert.Equal(t, "John Doe", pushCommit.CommitterName)
if assert.Len(t, pushCommit.CoAuthors, 1) {
assert.Equal(t, "jane@example.com", pushCommit.CoAuthors[0].Email)
assert.Equal(t, "Jane Doe", pushCommit.CoAuthors[0].Name)
}
assert.Equal(t, now, pushCommit.Timestamp)
}

View File

@ -29,12 +29,14 @@ import (
type blameRow struct {
RowNumber int
Avatar template.HTML
PreviousSha string
PreviousShaURL string
CommitURL string
CommitMessage string
CommitSince template.HTML
AuthorUser *user_model.User
CoAuthors []*user_model.CoAuthorUser
Author *git.Signature
Code template.HTML
EscapeStatus *charset.EscapeStatus
@ -221,13 +223,10 @@ func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) ma
return commitNames
}
func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) {
if commit.User != nil {
br.Avatar = avatarUtils.Avatar(commit.User, 18)
} else {
br.Avatar = avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18)
}
func renderBlameFillFirstBlameRow(repoLink string, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) {
br.AuthorUser = commit.User
br.CoAuthors = commit.CoAuthors
br.Author = commit.Author
br.PreviousSha = part.PreviousSha
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
@ -243,7 +242,6 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa
buf := &bytes.Buffer{}
rows := make([]*blameRow, 0)
avatarUtils := templates.NewAvatarUtils(ctx)
rowNumber := 0 // will be 1-based
for _, part := range blameParts {
for partLineIdx, line := range part.Lines {
@ -258,7 +256,7 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa
}
if partLineIdx == 0 {
renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, avatarUtils, part, commitNames[part.Sha], br)
renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, part, commitNames[part.Sha], br)
}
}
}

View File

@ -93,9 +93,7 @@ func (graph *Graph) AddCommit(row, column int, flowID int64, data []byte) error
// before finally retrieving the latest status
func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_model.Repository, gitRepo *git.Repository) error {
var err error
var ok bool
emails := map[string]*user_model.User{}
emailSet := map[string]struct{}{}
keyMap := map[string]bool{}
for _, c := range graph.Commits {
@ -106,13 +104,44 @@ func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_
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, _ = user_model.GetUserByEmail(ctx, email)
emails[email] = c.User
emailSet[c.Commit.Author.Email] = struct{}{}
}
for _, sig := range c.Commit.CoAuthorSignatures() {
emailSet[sig.Email] = struct{}{}
}
}
allEmails := make([]string, 0, len(emailSet))
for email := range emailSet {
allEmails = append(allEmails, email)
}
var emailUserMap *user_model.EmailUserMap
if len(allEmails) > 0 {
emailUserMap, err = user_model.GetUsersByEmails(ctx, allEmails)
if err != nil {
log.Error("GetUsersByEmails: %v", err)
}
}
for _, c := range graph.Commits {
if c.Commit == nil {
continue
}
if c.Commit.Author != nil && emailUserMap != nil {
c.User = emailUserMap.GetByEmail(c.Commit.Author.Email)
}
coAuthorSigs := c.Commit.CoAuthorSignatures()
c.CoAuthors = make([]*user_model.CoAuthorUser, 0, len(coAuthorSigs))
for _, sig := range coAuthorSigs {
var giteaUser *user_model.User
if emailUserMap != nil {
giteaUser = emailUserMap.GetByEmail(sig.Email)
}
c.CoAuthors = append(c.CoAuthors, &user_model.CoAuthorUser{
GiteaUser: giteaUser,
TrailerSignature: sig,
})
}
c.Verification = asymkey_service.ParseCommitWithSignature(ctx, c.Commit)
@ -248,6 +277,7 @@ func newRefsFromRefNames(refNames []byte) []git.Reference {
type Commit struct {
Commit *git.Commit
User *user_model.User
CoAuthors []*user_model.CoAuthorUser
Verification *asymkey_model.CommitVerification
Status *git_model.CommitStatus
Flow int64

View File

@ -43,7 +43,7 @@
<div class="blame-info">
<div class="blame-data">
<div class="blame-avatar">
{{$row.Avatar}}
{{template "repo/commit_coauthor_avatar_stack" dict "AuthorUser" $row.AuthorUser "AuthorSignature" $row.Author "CoAuthors" $row.CoAuthors}}
</div>
<div class="blame-message muted-links" title="{{$row.CommitMessage}}">
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $row.CommitMessage $row.CommitURL $.Repository}}

View File

@ -64,10 +64,10 @@ so this template should be kept as small as possible, DO NOT put large component
{{- if $verified -}}
{{- if and $signingUser $signingUser.ID -}}
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock"}}</span>
<span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.Avatar $signingUser 16}}</span>
<span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.Avatar $signingUser 20}}</span>
{{- else -}}
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock-cog"}}</span>
<span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 16}}</span>
<span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 20}}</span>
{{- end -}}
{{- else -}}
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-unlock"}}</span>

View File

@ -5,11 +5,7 @@
{{$index = Eval $index "+" 1}}
<div class="flex-text-block" id="{{$tag}}">{{/*singular-commit*/}}
<span class="badge badge-commit">{{svg "octicon-git-commit"}}</span>
{{if .User}}
<a class="avatar" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}}</a>
{{else}}
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}}
{{end}}
{{template "repo/commit_coauthor_avatar_stack" dict "AuthorUser" .User "AuthorSignature" .Author "CoAuthors" .CoAuthors}}
{{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}}
{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}}

View File

@ -41,14 +41,7 @@
</span>
<span class="flex-text-inline tw-text-12">
{{if $commit.User}}
{{ctx.AvatarUtils.Avatar $commit.User 18}}
{{$commit.User.GetShortDisplayNameLinkHTML}}
{{else}}
{{$gitUserName := $commit.Commit.Author.Name}}
{{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $gitUserName 18}}
{{$gitUserName}}
{{end}}
{{template "repo/commit_coauthor_avatars" dict "AuthorUser" $commit.User "AuthorSignature" $commit.Commit.Author "CoAuthors" $commit.CoAuthors}}
</span>
<span class="time flex-text-inline">{{DateUtils.FullTime $commit.Date}}</span>

View File

@ -91,7 +91,7 @@
{{range $pushCommit := $push.Commits}}
{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
<div class="flex-text-block">
<img loading="lazy" alt class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
{{template "repo/commit_coauthor_avatar_stack" dict "AuthorSignature" (dict "Email" .AuthorEmail "Name" .AuthorName) "CoAuthorSignatures" .CoAuthors}}
<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
<span class="tw-inline-block tw-truncate">
{{ctx.RenderUtils.RenderCommitMessage $pushCommit.Message $repo}}