mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-21 04:30:24 +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:
parent
ac82797cd8
commit
10461b2f4e
@ -29,6 +29,7 @@ type PushCommit struct {
|
|||||||
AuthorName string
|
AuthorName string
|
||||||
CommitterEmail string
|
CommitterEmail string
|
||||||
CommitterName string
|
CommitterName string
|
||||||
|
CoAuthors []*git.Signature
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,6 +158,7 @@ func CommitToPushCommit(commit *git.Commit) *PushCommit {
|
|||||||
AuthorName: commit.Author.Name,
|
AuthorName: commit.Author.Name,
|
||||||
CommitterEmail: commit.Committer.Email,
|
CommitterEmail: commit.Committer.Email,
|
||||||
CommitterName: commit.Committer.Name,
|
CommitterName: commit.Committer.Name,
|
||||||
|
CoAuthors: commit.CoAuthorSignatures(),
|
||||||
Timestamp: commit.Author.When,
|
Timestamp: commit.Author.When,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,14 +145,18 @@ func TestCommitToPushCommit(t *testing.T) {
|
|||||||
ID: sha1,
|
ID: sha1,
|
||||||
Author: sig,
|
Author: sig,
|
||||||
Committer: 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, 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, "example@example.com", pushCommit.AuthorEmail)
|
||||||
assert.Equal(t, "John Doe", pushCommit.AuthorName)
|
assert.Equal(t, "John Doe", pushCommit.AuthorName)
|
||||||
assert.Equal(t, "example@example.com", pushCommit.CommitterEmail)
|
assert.Equal(t, "example@example.com", pushCommit.CommitterEmail)
|
||||||
assert.Equal(t, "John Doe", pushCommit.CommitterName)
|
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)
|
assert.Equal(t, now, pushCommit.Timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,12 +29,14 @@ import (
|
|||||||
type blameRow struct {
|
type blameRow struct {
|
||||||
RowNumber int
|
RowNumber int
|
||||||
|
|
||||||
Avatar template.HTML
|
|
||||||
PreviousSha string
|
PreviousSha string
|
||||||
PreviousShaURL string
|
PreviousShaURL string
|
||||||
CommitURL string
|
CommitURL string
|
||||||
CommitMessage string
|
CommitMessage string
|
||||||
CommitSince template.HTML
|
CommitSince template.HTML
|
||||||
|
AuthorUser *user_model.User
|
||||||
|
CoAuthors []*user_model.CoAuthorUser
|
||||||
|
Author *git.Signature
|
||||||
|
|
||||||
Code template.HTML
|
Code template.HTML
|
||||||
EscapeStatus *charset.EscapeStatus
|
EscapeStatus *charset.EscapeStatus
|
||||||
@ -221,13 +223,10 @@ func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) ma
|
|||||||
return commitNames
|
return commitNames
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) {
|
func renderBlameFillFirstBlameRow(repoLink string, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) {
|
||||||
if commit.User != nil {
|
br.AuthorUser = commit.User
|
||||||
br.Avatar = avatarUtils.Avatar(commit.User, 18)
|
br.CoAuthors = commit.CoAuthors
|
||||||
} else {
|
br.Author = commit.Author
|
||||||
br.Avatar = avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18)
|
|
||||||
}
|
|
||||||
|
|
||||||
br.PreviousSha = part.PreviousSha
|
br.PreviousSha = part.PreviousSha
|
||||||
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
|
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))
|
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{}
|
buf := &bytes.Buffer{}
|
||||||
rows := make([]*blameRow, 0)
|
rows := make([]*blameRow, 0)
|
||||||
avatarUtils := templates.NewAvatarUtils(ctx)
|
|
||||||
rowNumber := 0 // will be 1-based
|
rowNumber := 0 // will be 1-based
|
||||||
for _, part := range blameParts {
|
for _, part := range blameParts {
|
||||||
for partLineIdx, line := range part.Lines {
|
for partLineIdx, line := range part.Lines {
|
||||||
@ -258,7 +256,7 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if partLineIdx == 0 {
|
if partLineIdx == 0 {
|
||||||
renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, avatarUtils, part, commitNames[part.Sha], br)
|
renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, part, commitNames[part.Sha], br)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,9 +93,7 @@ func (graph *Graph) AddCommit(row, column int, flowID int64, data []byte) error
|
|||||||
// before finally retrieving the latest status
|
// before finally retrieving the latest status
|
||||||
func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_model.Repository, gitRepo *git.Repository) error {
|
func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_model.Repository, gitRepo *git.Repository) error {
|
||||||
var err error
|
var err error
|
||||||
var ok bool
|
emailSet := map[string]struct{}{}
|
||||||
|
|
||||||
emails := map[string]*user_model.User{}
|
|
||||||
keyMap := map[string]bool{}
|
keyMap := map[string]bool{}
|
||||||
|
|
||||||
for _, c := range graph.Commits {
|
for _, c := range graph.Commits {
|
||||||
@ -106,13 +104,44 @@ func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GetCommit: %s Error: %w", c.Rev, err)
|
return fmt.Errorf("GetCommit: %s Error: %w", c.Rev, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Commit.Author != nil {
|
if c.Commit.Author != nil {
|
||||||
email := c.Commit.Author.Email
|
emailSet[c.Commit.Author.Email] = struct{}{}
|
||||||
if c.User, ok = emails[email]; !ok {
|
}
|
||||||
c.User, _ = user_model.GetUserByEmail(ctx, email)
|
for _, sig := range c.Commit.CoAuthorSignatures() {
|
||||||
emails[email] = c.User
|
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)
|
c.Verification = asymkey_service.ParseCommitWithSignature(ctx, c.Commit)
|
||||||
@ -248,6 +277,7 @@ func newRefsFromRefNames(refNames []byte) []git.Reference {
|
|||||||
type Commit struct {
|
type Commit struct {
|
||||||
Commit *git.Commit
|
Commit *git.Commit
|
||||||
User *user_model.User
|
User *user_model.User
|
||||||
|
CoAuthors []*user_model.CoAuthorUser
|
||||||
Verification *asymkey_model.CommitVerification
|
Verification *asymkey_model.CommitVerification
|
||||||
Status *git_model.CommitStatus
|
Status *git_model.CommitStatus
|
||||||
Flow int64
|
Flow int64
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
<div class="blame-info">
|
<div class="blame-info">
|
||||||
<div class="blame-data">
|
<div class="blame-data">
|
||||||
<div class="blame-avatar">
|
<div class="blame-avatar">
|
||||||
{{$row.Avatar}}
|
{{template "repo/commit_coauthor_avatar_stack" dict "AuthorUser" $row.AuthorUser "AuthorSignature" $row.Author "CoAuthors" $row.CoAuthors}}
|
||||||
</div>
|
</div>
|
||||||
<div class="blame-message muted-links" title="{{$row.CommitMessage}}">
|
<div class="blame-message muted-links" title="{{$row.CommitMessage}}">
|
||||||
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $row.CommitMessage $row.CommitURL $.Repository}}
|
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $row.CommitMessage $row.CommitURL $.Repository}}
|
||||||
|
|||||||
@ -64,10 +64,10 @@ so this template should be kept as small as possible, DO NOT put large component
|
|||||||
{{- if $verified -}}
|
{{- if $verified -}}
|
||||||
{{- if and $signingUser $signingUser.ID -}}
|
{{- if and $signingUser $signingUser.ID -}}
|
||||||
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock"}}</span>
|
<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 -}}
|
{{- else -}}
|
||||||
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock-cog"}}</span>
|
<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 -}}
|
{{- end -}}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-unlock"}}</span>
|
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-unlock"}}</span>
|
||||||
|
|||||||
@ -5,11 +5,7 @@
|
|||||||
{{$index = Eval $index "+" 1}}
|
{{$index = Eval $index "+" 1}}
|
||||||
<div class="flex-text-block" id="{{$tag}}">{{/*singular-commit*/}}
|
<div class="flex-text-block" id="{{$tag}}">{{/*singular-commit*/}}
|
||||||
<span class="badge badge-commit">{{svg "octicon-git-commit"}}</span>
|
<span class="badge badge-commit">{{svg "octicon-git-commit"}}</span>
|
||||||
{{if .User}}
|
{{template "repo/commit_coauthor_avatar_stack" dict "AuthorUser" .User "AuthorSignature" .Author "CoAuthors" .CoAuthors}}
|
||||||
<a class="avatar" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}}</a>
|
|
||||||
{{else}}
|
|
||||||
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}}
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}}
|
{{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}}
|
||||||
{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}}
|
{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}}
|
||||||
|
|||||||
@ -41,14 +41,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="flex-text-inline tw-text-12">
|
<span class="flex-text-inline tw-text-12">
|
||||||
{{if $commit.User}}
|
{{template "repo/commit_coauthor_avatars" dict "AuthorUser" $commit.User "AuthorSignature" $commit.Commit.Author "CoAuthors" $commit.CoAuthors}}
|
||||||
{{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}}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="time flex-text-inline">{{DateUtils.FullTime $commit.Date}}</span>
|
<span class="time flex-text-inline">{{DateUtils.FullTime $commit.Date}}</span>
|
||||||
|
|||||||
@ -91,7 +91,7 @@
|
|||||||
{{range $pushCommit := $push.Commits}}
|
{{range $pushCommit := $push.Commits}}
|
||||||
{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
|
{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
|
||||||
<div class="flex-text-block">
|
<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>
|
<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
|
||||||
<span class="tw-inline-block tw-truncate">
|
<span class="tw-inline-block tw-truncate">
|
||||||
{{ctx.RenderUtils.RenderCommitMessage $pushCommit.Message $repo}}
|
{{ctx.RenderUtils.RenderCommitMessage $pushCommit.Message $repo}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user