mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-20 00:50:10 +02:00
Merge 30b1928c068b9ed58668cea85310ae1aefe7972d into ce089f498bce32305b2d9e8c6adfd8cb7c82f88f
This commit is contained in:
commit
30cdccf882
@ -1140,9 +1140,16 @@ func GetUsersBySource(ctx context.Context, s *auth.Source) ([]*User, error) {
|
|||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CoAuthorUser represents a co-author parsed from a commit trailer, with optional Gitea user.
|
||||||
|
type CoAuthorUser struct {
|
||||||
|
GiteaUser *User
|
||||||
|
TrailerSignature *git.Signature
|
||||||
|
}
|
||||||
|
|
||||||
// UserCommit represents a commit with validation of user.
|
// UserCommit represents a commit with validation of user.
|
||||||
type UserCommit struct { //revive:disable-line:exported
|
type UserCommit struct { //revive:disable-line:exported
|
||||||
User *User
|
User *User
|
||||||
|
CoAuthors []*CoAuthorUser
|
||||||
*git.Commit
|
*git.Commit
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1158,6 +1165,27 @@ func ValidateCommitWithEmail(ctx context.Context, c *git.Commit) *User {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CoAuthorsFromCommit resolves co-author signatures from a commit into CoAuthorUser values.
|
||||||
|
func CoAuthorsFromCommit(ctx context.Context, c *git.Commit) ([]*CoAuthorUser, error) {
|
||||||
|
sigs := c.CoAuthorSignatures()
|
||||||
|
if len(sigs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
emails := make([]string, len(sigs))
|
||||||
|
for i, sig := range sigs {
|
||||||
|
emails[i] = sig.Email
|
||||||
|
}
|
||||||
|
emailUserMap, err := GetUsersByEmails(ctx, emails)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
coAuthors := make([]*CoAuthorUser, len(sigs))
|
||||||
|
for i, sig := range sigs {
|
||||||
|
coAuthors[i] = &CoAuthorUser{GiteaUser: emailUserMap.GetByEmail(sig.Email), TrailerSignature: sig}
|
||||||
|
}
|
||||||
|
return coAuthors, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users.
|
// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users.
|
||||||
func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([]*UserCommit, error) {
|
func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([]*UserCommit, error) {
|
||||||
var (
|
var (
|
||||||
@ -1168,6 +1196,9 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([
|
|||||||
if c.Author != nil {
|
if c.Author != nil {
|
||||||
emailSet.Add(c.Author.Email)
|
emailSet.Add(c.Author.Email)
|
||||||
}
|
}
|
||||||
|
for _, sig := range c.CoAuthorSignatures() {
|
||||||
|
emailSet.Add(sig.Email)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emailUserMap, err := GetUsersByEmails(ctx, emailSet.Values())
|
emailUserMap, err := GetUsersByEmails(ctx, emailSet.Values())
|
||||||
@ -1177,9 +1208,21 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([
|
|||||||
|
|
||||||
for _, c := range oldCommits {
|
for _, c := range oldCommits {
|
||||||
user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
|
user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
|
||||||
|
coAuthorSigs := c.CoAuthorSignatures()
|
||||||
|
var coAuthors []*CoAuthorUser
|
||||||
|
if len(coAuthorSigs) > 0 {
|
||||||
|
coAuthors = make([]*CoAuthorUser, 0, len(coAuthorSigs))
|
||||||
|
for _, sig := range coAuthorSigs {
|
||||||
|
coAuthors = append(coAuthors, &CoAuthorUser{
|
||||||
|
GiteaUser: emailUserMap.GetByEmail(sig.Email),
|
||||||
|
TrailerSignature: sig,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
newCommits = append(newCommits, &UserCommit{
|
newCommits = append(newCommits, &UserCommit{
|
||||||
User: user,
|
User: user,
|
||||||
Commit: c,
|
CoAuthors: coAuthors,
|
||||||
|
Commit: c,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return newCommits, nil
|
return newCommits, nil
|
||||||
|
|||||||
@ -17,10 +17,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CommitMessage struct {
|
type CommitMessage struct {
|
||||||
MessageRaw string
|
MessageRaw string
|
||||||
messageUTF8 *string
|
messageUTF8 *string
|
||||||
messageTitle *string
|
messageTitle *string
|
||||||
messageBody *string
|
messageBody *string
|
||||||
|
messageCoAuthors *[]*Signature
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit represents a git commit.
|
// Commit represents a git commit.
|
||||||
@ -68,6 +69,113 @@ func (c *CommitMessage) MessageBody() string {
|
|||||||
return *c.messageBody
|
return *c.messageBody
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cutTrailerPrefix matches "<token>:" case-insensitively at the start of line and
|
||||||
|
// returns the trailing value plus whether a match was found. `git interpret-trailers`
|
||||||
|
// matches trailer tokens case-insensitively, so e.g. "Co-Authored-By:" is valid.
|
||||||
|
func cutTrailerPrefix(line, token string) (string, bool) {
|
||||||
|
if len(line) < len(token)+1 || line[len(token)] != ':' {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(line[:len(token)], token) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return line[len(token)+1:], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTrailerLine(line string) bool {
|
||||||
|
token, rest, ok := strings.Cut(line, ":")
|
||||||
|
if !ok || strings.TrimSpace(rest) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range token {
|
||||||
|
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return token != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoAuthorSignatures parses "Co-authored-by:" and "Co-committed-by:" trailers
|
||||||
|
// from the trailing block of the commit message and returns deduplicated
|
||||||
|
// Signature values. Only the last paragraph of the body is scanned so that
|
||||||
|
// quoted or in-body occurrences (e.g. inside a revert/cherry-pick description)
|
||||||
|
// are not misinterpreted as trailers. The trailing paragraph must contain only
|
||||||
|
// trailer-shaped lines.
|
||||||
|
// Token matching is case-insensitive to match git's behaviour.
|
||||||
|
func (c *CommitMessage) CoAuthorSignatures() []*Signature {
|
||||||
|
if c.messageCoAuthors != nil {
|
||||||
|
return *c.messageCoAuthors
|
||||||
|
}
|
||||||
|
var sigs []*Signature
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
body := strings.TrimRight(c.MessageBody(), "\r\n")
|
||||||
|
if idx := strings.LastIndex(body, "\r\n\r\n"); idx >= 0 {
|
||||||
|
body = body[idx+4:]
|
||||||
|
} else if idx := strings.LastIndex(body, "\n\n"); idx >= 0 {
|
||||||
|
body = body[idx+2:]
|
||||||
|
}
|
||||||
|
lines := strings.Split(body, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
lines[i] = strings.TrimRight(line, "\r")
|
||||||
|
if !isTrailerLine(lines[i]) {
|
||||||
|
c.messageCoAuthors = &sigs
|
||||||
|
return sigs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
rest, ok := cutTrailerPrefix(line, "Co-authored-by")
|
||||||
|
if !ok {
|
||||||
|
rest, ok = cutTrailerPrefix(line, "Co-committed-by")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rest = strings.TrimSpace(rest)
|
||||||
|
name, emailWithBracket, ok := strings.Cut(rest, " <")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
email, _, ok := strings.Cut(emailWithBracket, ">")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
email = strings.ToLower(strings.TrimSpace(email))
|
||||||
|
if _, exists := seen[email]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[email] = struct{}{}
|
||||||
|
sigs = append(sigs, &Signature{Name: strings.TrimSpace(name), Email: email})
|
||||||
|
}
|
||||||
|
c.messageCoAuthors = &sigs
|
||||||
|
return sigs
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoAuthorSignatures returns the commit's co-author trailers with the commit's
|
||||||
|
// own author and committer emails filtered out, so a contributor who copies
|
||||||
|
// themselves into a Co-authored-by line is not duplicated in the avatar stack.
|
||||||
|
func (c *Commit) CoAuthorSignatures() []*Signature {
|
||||||
|
raw := c.CommitMessage.CoAuthorSignatures()
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
exclude := make(map[string]struct{}, 2)
|
||||||
|
if c.Author != nil {
|
||||||
|
exclude[strings.ToLower(strings.TrimSpace(c.Author.Email))] = struct{}{}
|
||||||
|
}
|
||||||
|
if c.Committer != nil {
|
||||||
|
exclude[strings.ToLower(strings.TrimSpace(c.Committer.Email))] = struct{}{}
|
||||||
|
}
|
||||||
|
out := make([]*Signature, 0, len(raw))
|
||||||
|
for _, sig := range raw {
|
||||||
|
if _, skip := exclude[sig.Email]; skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, sig)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// ParentID returns oid of n-th parent (0-based index).
|
// ParentID returns oid of n-th parent (0-based index).
|
||||||
// It returns nil if no such parent exists.
|
// It returns nil if no such parent exists.
|
||||||
func (c *Commit) ParentID(n int) (ObjectID, error) {
|
func (c *Commit) ParentID(n int) (ObjectID, error) {
|
||||||
|
|||||||
@ -208,3 +208,92 @@ func Test_GetCommitBranchStart(t *testing.T) {
|
|||||||
assert.NotEmpty(t, startCommitID)
|
assert.NotEmpty(t, startCommitID)
|
||||||
assert.Equal(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID)
|
assert.Equal(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCoAuthorSignatures(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
body string
|
||||||
|
want []Signature
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
body: "title",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single co-author",
|
||||||
|
body: "title\n\nbody text\n\nCo-authored-by: Jane <jane@example.com>",
|
||||||
|
want: []Signature{{Name: "Jane", Email: "jane@example.com"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case insensitive token",
|
||||||
|
body: "title\n\nCo-Authored-By: Jane <Jane@Example.com>\nco-committed-by: Bob <bob@example.com>",
|
||||||
|
want: []Signature{
|
||||||
|
{Name: "Jane", Email: "jane@example.com"},
|
||||||
|
{Name: "Bob", Email: "bob@example.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dedup by lowercased email",
|
||||||
|
body: "title\n\nCo-authored-by: Jane <jane@example.com>\nCo-authored-by: Janey <JANE@example.com>",
|
||||||
|
want: []Signature{{Name: "Jane", Email: "jane@example.com"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "in-body trailer ignored, only last paragraph counts",
|
||||||
|
body: "title\n\nCo-authored-by: Mallory <mallory@example.com>\n\nactual body explaining revert\n\nCo-authored-by: Jane <jane@example.com>",
|
||||||
|
want: []Signature{{Name: "Jane", Email: "jane@example.com"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "body text in trailing paragraph rejects co-author line",
|
||||||
|
body: "title\n\nbody text\nCo-authored-by: Jane <jane@example.com>",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing brackets is ignored",
|
||||||
|
body: "title\n\nCo-authored-by: Jane jane@example.com",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CRLF line endings",
|
||||||
|
body: "title\r\n\r\nCo-authored-by: Jane <jane@example.com>\r\nCo-committed-by: Bob <bob@example.com>",
|
||||||
|
want: []Signature{
|
||||||
|
{Name: "Jane", Email: "jane@example.com"},
|
||||||
|
{Name: "Bob", Email: "bob@example.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-trailer line",
|
||||||
|
body: "title\n\nSigned-off-by: Jane <jane@example.com>",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cm := CommitMessage{MessageRaw: tc.body}
|
||||||
|
got := cm.CoAuthorSignatures()
|
||||||
|
assert.Len(t, got, len(tc.want))
|
||||||
|
for i, w := range tc.want {
|
||||||
|
if i >= len(got) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
assert.Equal(t, w.Name, got[i].Name, "name[%d]", i)
|
||||||
|
assert.Equal(t, w.Email, got[i].Email, "email[%d]", i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommitCoAuthorSignaturesFiltersAuthorAndCommitter(t *testing.T) {
|
||||||
|
c := &Commit{
|
||||||
|
Author: &Signature{Name: "Jane", Email: "jane@example.com"},
|
||||||
|
Committer: &Signature{Name: "Bob", Email: "bob@example.com"},
|
||||||
|
CommitMessage: CommitMessage{MessageRaw: "title\n\n" +
|
||||||
|
"Co-authored-by: Jane Self <jane@example.com>\n" +
|
||||||
|
"Co-authored-by: Bob Self <BOB@example.com>\n" +
|
||||||
|
"Co-authored-by: Carol <carol@example.com>"},
|
||||||
|
}
|
||||||
|
got := c.CoAuthorSignatures()
|
||||||
|
if assert.Len(t, got, 1) {
|
||||||
|
assert.Equal(t, "carol@example.com", got[0].Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -29,9 +29,16 @@ type PushCommit struct {
|
|||||||
AuthorName string
|
AuthorName string
|
||||||
CommitterEmail string
|
CommitterEmail string
|
||||||
CommitterName string
|
CommitterName string
|
||||||
|
CoAuthors []*PushCommitCoAuthor `json:",omitempty"`
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PushCommitCoAuthor represents a co-author in a push commit payload.
|
||||||
|
type PushCommitCoAuthor struct {
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
// PushCommits represents list of commits in a push operation.
|
// PushCommits represents list of commits in a push operation.
|
||||||
type PushCommits struct {
|
type PushCommits struct {
|
||||||
Commits []*PushCommit
|
Commits []*PushCommit
|
||||||
@ -148,6 +155,17 @@ func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pushCommitCoAuthorsFromSignatures(sigs []*git.Signature) []*PushCommitCoAuthor {
|
||||||
|
if len(sigs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
coAuthors := make([]*PushCommitCoAuthor, len(sigs))
|
||||||
|
for i, sig := range sigs {
|
||||||
|
coAuthors[i] = &PushCommitCoAuthor{Name: sig.Name, Email: sig.Email}
|
||||||
|
}
|
||||||
|
return coAuthors
|
||||||
|
}
|
||||||
|
|
||||||
// CommitToPushCommit transforms a git.Commit to PushCommit type.
|
// CommitToPushCommit transforms a git.Commit to PushCommit type.
|
||||||
func CommitToPushCommit(commit *git.Commit) *PushCommit {
|
func CommitToPushCommit(commit *git.Commit) *PushCommit {
|
||||||
return &PushCommit{
|
return &PushCommit{
|
||||||
@ -157,10 +175,34 @@ 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: pushCommitCoAuthorsFromSignatures(commit.CoAuthorSignatures()),
|
||||||
Timestamp: commit.Author.When,
|
Timestamp: commit.Author.When,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthorSignature returns the push commit author as a git signature.
|
||||||
|
func (pc *PushCommit) AuthorSignature() *git.Signature {
|
||||||
|
return &git.Signature{
|
||||||
|
Email: pc.AuthorEmail,
|
||||||
|
Name: pc.AuthorName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoAuthorUsers returns co-authors in the template view shape.
|
||||||
|
func (pc *PushCommit) CoAuthorUsers() []*user_model.CoAuthorUser {
|
||||||
|
if len(pc.CoAuthors) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
coAuthors := make([]*user_model.CoAuthorUser, len(pc.CoAuthors))
|
||||||
|
for i, coAuthor := range pc.CoAuthors {
|
||||||
|
coAuthors[i] = &user_model.CoAuthorUser{TrailerSignature: &git.Signature{
|
||||||
|
Name: coAuthor.Name,
|
||||||
|
Email: coAuthor.Email,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
return coAuthors
|
||||||
|
}
|
||||||
|
|
||||||
// GitToPushCommits transforms a list of git.Commits to PushCommits type.
|
// GitToPushCommits transforms a list of git.Commits to PushCommits type.
|
||||||
func GitToPushCommits(gitCommits []*git.Commit) *PushCommits {
|
func GitToPushCommits(gitCommits []*git.Commit) *PushCommits {
|
||||||
commits := make([]*PushCommit, 0, len(gitCommits))
|
commits := make([]*PushCommit, 0, len(gitCommits))
|
||||||
|
|||||||
@ -145,14 +145,23 @@ 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, &git.Signature{Email: "example@example.com", Name: "John Doe"}, pushCommit.AuthorSignature())
|
||||||
|
if assert.Len(t, pushCommit.CoAuthorUsers(), 1) {
|
||||||
|
assert.Equal(t, &git.Signature{Email: "jane@example.com", Name: "Jane Doe"}, pushCommit.CoAuthorUsers()[0].TrailerSignature)
|
||||||
|
assert.Nil(t, pushCommit.CoAuthorUsers()[0].GiteaUser)
|
||||||
|
}
|
||||||
assert.Equal(t, now, pushCommit.Timestamp)
|
assert.Equal(t, now, pushCommit.Timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2589,6 +2589,11 @@
|
|||||||
"repo.diff.review.reject": "Request changes",
|
"repo.diff.review.reject": "Request changes",
|
||||||
"repo.diff.review.self_approve": "Pull request authors can't approve their own pull request",
|
"repo.diff.review.self_approve": "Pull request authors can't approve their own pull request",
|
||||||
"repo.diff.committed_by": "committed by",
|
"repo.diff.committed_by": "committed by",
|
||||||
|
"repo.diff.coauthored_by": "co-authored by",
|
||||||
|
"repo.commits.coauthor_and": "and",
|
||||||
|
"repo.commits.coauthor_people": "%d people",
|
||||||
|
"repo.commits.coauthor_others_1": "%d other",
|
||||||
|
"repo.commits.coauthor_others_n": "%d others",
|
||||||
"repo.diff.protected": "Protected",
|
"repo.diff.protected": "Protected",
|
||||||
"repo.diff.image.side_by_side": "Side by Side",
|
"repo.diff.image.side_by_side": "Side by Side",
|
||||||
"repo.diff.image.swipe": "Swipe",
|
"repo.diff.image.swipe": "Swipe",
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -382,6 +382,11 @@ func Diff(ctx *context.Context) {
|
|||||||
verification := asymkey_service.ParseCommitWithSignature(ctx, commit)
|
verification := asymkey_service.ParseCommitWithSignature(ctx, commit)
|
||||||
ctx.Data["Verification"] = verification
|
ctx.Data["Verification"] = verification
|
||||||
ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, commit)
|
ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, commit)
|
||||||
|
if coAuthors, err := user_model.CoAuthorsFromCommit(ctx, commit); err != nil {
|
||||||
|
log.Error("CoAuthorsFromCommit: %v", err)
|
||||||
|
} else {
|
||||||
|
ctx.Data["CoAuthors"] = coAuthors
|
||||||
|
}
|
||||||
ctx.Data["Parents"] = parents
|
ctx.Data["Parents"] = parents
|
||||||
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
|
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
|
||||||
|
|
||||||
|
|||||||
@ -135,6 +135,12 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
|
|||||||
ctx.Data["LatestCommitVerification"] = verification
|
ctx.Data["LatestCommitVerification"] = verification
|
||||||
ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
|
ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
|
||||||
|
|
||||||
|
if coAuthors, err := user_model.CoAuthorsFromCommit(ctx, latestCommit); err != nil {
|
||||||
|
log.Error("CoAuthorsFromCommit: %v", err)
|
||||||
|
} else {
|
||||||
|
ctx.Data["LatestCommitCoAuthors"] = coAuthors
|
||||||
|
}
|
||||||
|
|
||||||
statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
|
statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetLatestCommitStatus: %v", err)
|
log.Error("GetLatestCommitStatus: %v", err)
|
||||||
|
|||||||
@ -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,12 +104,45 @@ 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()
|
||||||
|
if len(coAuthorSigs) > 0 {
|
||||||
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +279,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}}
|
||||||
|
|||||||
35
templates/repo/commit_coauthor_avatar_stack.tmpl
Normal file
35
templates/repo/commit_coauthor_avatar_stack.tmpl
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{{if .CoAuthors}}
|
||||||
|
{{- $additionalClasses := .AdditionalClasses -}}
|
||||||
|
{{- $coCount := len .CoAuthors -}}
|
||||||
|
{{- $maxCo := 9 -}}
|
||||||
|
{{- $visibleCo := .CoAuthors -}}
|
||||||
|
{{- $overflow := 0 -}}
|
||||||
|
{{- if gt $coCount $maxCo -}}
|
||||||
|
{{- $visibleCo = slice .CoAuthors 0 $maxCo -}}
|
||||||
|
{{- $overflow = Eval $coCount "-" $maxCo -}}
|
||||||
|
{{- end -}}
|
||||||
|
<span class="coauthor-avatar-stack-wrapper {{$additionalClasses}}">
|
||||||
|
<span class="coauthor-avatar-stack">
|
||||||
|
{{- if .AuthorUser -}}
|
||||||
|
<a href="{{.AuthorUser.HomeLink}}" data-tooltip-content="{{.AuthorUser.GetDisplayName}}">{{- ctx.AvatarUtils.Avatar .AuthorUser 20 -}}</a>
|
||||||
|
{{- else -}}
|
||||||
|
{{- ctx.AvatarUtils.AvatarByEmail .AuthorSignature.Email .AuthorSignature.Name 20 -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- range $visibleCo -}}
|
||||||
|
{{- if .GiteaUser -}}
|
||||||
|
<a href="{{.GiteaUser.HomeLink}}" data-tooltip-content="{{.GiteaUser.GetDisplayName}}">{{- ctx.AvatarUtils.Avatar .GiteaUser 20 -}}</a>
|
||||||
|
{{- else -}}
|
||||||
|
{{- ctx.AvatarUtils.AvatarByEmail .TrailerSignature.Email .TrailerSignature.Name 20 -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
{{- if gt $overflow 0 -}}
|
||||||
|
{{- $overflowLabel := ctx.Locale.TrN $overflow "repo.commits.coauthor_others_1" "repo.commits.coauthor_others_n" $overflow -}}
|
||||||
|
<span class="coauthor-overflow-chip" role="img" aria-label="{{$overflowLabel}}" data-tooltip-content="{{$overflowLabel}}">+{{$overflow}}</span>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
{{else if .AuthorUser}}
|
||||||
|
{{- ctx.AvatarUtils.Avatar .AuthorUser 20 .AdditionalClasses -}}
|
||||||
|
{{else}}
|
||||||
|
{{- ctx.AvatarUtils.AvatarByEmail .AuthorSignature.Email .AuthorSignature.Name 20 .AdditionalClasses -}}
|
||||||
|
{{end}}
|
||||||
44
templates/repo/commit_coauthor_avatars.tmpl
Normal file
44
templates/repo/commit_coauthor_avatars.tmpl
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{{/*
|
||||||
|
Renders the author/co-author avatar stack and name text for a commit.
|
||||||
|
Args (via dict):
|
||||||
|
AuthorUser - *user_model.User (may be nil if no Gitea account)
|
||||||
|
AuthorSignature - *git.Signature (always set: name + email from git)
|
||||||
|
CoAuthors - []*user_model.CoAuthorUser
|
||||||
|
fields: GiteaUser (*User, may be nil), TrailerSignature (*git.Signature)
|
||||||
|
|
||||||
|
Avatar stack is capped at 10 children (author + up to 9 co-authors); any
|
||||||
|
remainder is collapsed into a trailing "+N" chip. The .coauthor-avatar-stack
|
||||||
|
CSS rules expect at most 10 children to fan out cleanly on hover.
|
||||||
|
*/}}
|
||||||
|
<span class="author-wrapper">
|
||||||
|
{{- if .CoAuthors -}}
|
||||||
|
{{- $coCount := len .CoAuthors -}}
|
||||||
|
{{template "repo/commit_coauthor_avatar_stack" dict "AuthorUser" .AuthorUser "AuthorSignature" .AuthorSignature "CoAuthors" .CoAuthors}}
|
||||||
|
{{- if .AuthorUser -}}
|
||||||
|
{{- .AuthorUser.GetShortDisplayNameLinkHTML -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{.AuthorSignature.Name}}
|
||||||
|
{{- end -}}
|
||||||
|
{{" "}}{{ctx.Locale.Tr "repo.commits.coauthor_and"}}{{" "}}
|
||||||
|
{{- if eq $coCount 1 -}}
|
||||||
|
{{- with index .CoAuthors 0 -}}
|
||||||
|
{{- if .GiteaUser -}}
|
||||||
|
<a class="muted" href="{{.GiteaUser.HomeLink}}">{{.GiteaUser.GetDisplayName}}</a>
|
||||||
|
{{- else -}}
|
||||||
|
{{.TrailerSignature.Name}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $peopleCount := Eval $coCount "+" 1 -}}
|
||||||
|
{{- ctx.Locale.Tr "repo.commits.coauthor_people" $peopleCount -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- if .AuthorUser -}}
|
||||||
|
{{- template "repo/commit_coauthor_avatar_stack" dict "AuthorUser" .AuthorUser "AuthorSignature" .AuthorSignature "AdditionalClasses" "tw-mr-1" -}}
|
||||||
|
{{- .AuthorUser.GetShortDisplayNameLinkHTML -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- template "repo/commit_coauthor_avatar_stack" dict "AuthorSignature" .AuthorSignature "AdditionalClasses" "tw-mr-1" -}}
|
||||||
|
{{.AuthorSignature.Name}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
@ -155,6 +155,21 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if .CoAuthors}}
|
||||||
|
<div class="flex-text-inline">
|
||||||
|
<span class="tw-text-text-light">{{ctx.Locale.Tr "repo.diff.coauthored_by"}}</span>
|
||||||
|
{{range .CoAuthors}}
|
||||||
|
{{if .GiteaUser}}
|
||||||
|
{{ctx.AvatarUtils.Avatar .GiteaUser 20}}
|
||||||
|
<a href="{{.GiteaUser.HomeLink}}"><strong>{{.GiteaUser.GetDisplayName}}</strong></a>
|
||||||
|
{{else}}
|
||||||
|
{{ctx.AvatarUtils.AvatarByEmail .TrailerSignature.Email .TrailerSignature.Name 20}}
|
||||||
|
<strong>{{.TrailerSignature.Name}}</strong>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if .Verification}}
|
{{if .Verification}}
|
||||||
{{template "repo/commit_sign_badge" dict "CommitSignVerification" .Verification}}
|
{{template "repo/commit_sign_badge" dict "CommitSignVerification" .Verification}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
<table class="ui very basic table unstackable" id="commits-table">
|
<table class="ui very basic table unstackable" id="commits-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="three wide">{{ctx.Locale.Tr "repo.commits.author"}}</th>
|
<th class="four wide">{{ctx.Locale.Tr "repo.commits.author"}}</th>
|
||||||
<th class="two wide sha">{{StringUtils.ToUpper $.Repository.ObjectFormatName}}</th>
|
<th class="two wide sha">{{StringUtils.ToUpper $.Repository.ObjectFormatName}}</th>
|
||||||
<th class="eight wide message">{{ctx.Locale.Tr "repo.commits.message"}}</th>
|
<th class="seven wide message">{{ctx.Locale.Tr "repo.commits.message"}}</th>
|
||||||
<th class="two wide tw-text-right">{{ctx.Locale.Tr "repo.commits.date"}}</th>
|
<th class="two wide tw-text-right">{{ctx.Locale.Tr "repo.commits.date"}}</th>
|
||||||
<th class="one wide"></th>
|
<th class="one wide"></th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -14,15 +14,7 @@
|
|||||||
{{range $commit := .Commits}}
|
{{range $commit := .Commits}}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="author">
|
<td class="author">
|
||||||
<span class="author-wrapper">
|
{{template "repo/commit_coauthor_avatars" dict "AuthorUser" .User "AuthorSignature" .Author "CoAuthors" .CoAuthors}}
|
||||||
{{- if .User -}}
|
|
||||||
{{- ctx.AvatarUtils.Avatar .User 20 "tw-mr-2" -}}
|
|
||||||
{{- .User.GetShortDisplayNameLinkHTML -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{- ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20 "tw-mr-2" -}}
|
|
||||||
{{- .Author.Name -}}
|
|
||||||
{{- end -}}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="sha">
|
<td class="sha">
|
||||||
{{$commitBaseLink := ""}}
|
{{$commitBaseLink := ""}}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -2,15 +2,7 @@
|
|||||||
{{if not .LatestCommit}}
|
{{if not .LatestCommit}}
|
||||||
…
|
…
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="author-wrapper">
|
{{template "repo/commit_coauthor_avatars" dict "AuthorUser" .LatestCommitUser "AuthorSignature" .LatestCommit.Author "CoAuthors" .LatestCommitCoAuthors}}
|
||||||
{{- if .LatestCommitUser -}}
|
|
||||||
{{- ctx.AvatarUtils.Avatar .LatestCommitUser 20 "tw-mr-2" -}}
|
|
||||||
<strong>{{.LatestCommitUser.GetShortDisplayNameLinkHTML}}</strong>
|
|
||||||
{{- else if .LatestCommit.Author -}}
|
|
||||||
{{- ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 20 "tw-mr-2" -}}
|
|
||||||
<strong>{{.LatestCommit.Author.Name}}</strong>
|
|
||||||
{{- end -}}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}}
|
{{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}}
|
||||||
|
|
||||||
|
|||||||
@ -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" .AuthorSignature "CoAuthors" .CoAuthorUsers}}
|
||||||
<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}}
|
||||||
|
|||||||
@ -1403,11 +1403,77 @@ tbody.commit-list {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.author-wrapper {
|
.author-wrapper {
|
||||||
max-width: 180px;
|
max-width: 240px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coauthor-avatar-stack-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coauthor-avatar-stack {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* each direct child of the stack is either <a> (linked user) or <img> (no account) */
|
||||||
|
.coauthor-avatar-stack > * {
|
||||||
|
margin-left: -12px;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coauthor-avatar-stack > *:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coauthor-avatar-stack .avatar {
|
||||||
|
border: 2px solid var(--color-body);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coauthor-overflow-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
border: 2px solid var(--color-body);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-secondary);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coauthor-avatar-stack > *:nth-child(1) { z-index: 20; }
|
||||||
|
.coauthor-avatar-stack > *:nth-child(2) { z-index: 19; }
|
||||||
|
.coauthor-avatar-stack > *:nth-child(3) { z-index: 18; }
|
||||||
|
.coauthor-avatar-stack > *:nth-child(4) { z-index: 17; }
|
||||||
|
.coauthor-avatar-stack > *:nth-child(5) { z-index: 16; }
|
||||||
|
.coauthor-avatar-stack > *:nth-child(6) { z-index: 15; }
|
||||||
|
.coauthor-avatar-stack > *:nth-child(7) { z-index: 14; }
|
||||||
|
.coauthor-avatar-stack > *:nth-child(8) { z-index: 13; }
|
||||||
|
.coauthor-avatar-stack > *:nth-child(9) { z-index: 12; }
|
||||||
|
.coauthor-avatar-stack > *:nth-child(10) { z-index: 11; }
|
||||||
|
|
||||||
|
.coauthor-avatar-stack:hover > *:nth-child(2) { transform: translateX(12px); }
|
||||||
|
.coauthor-avatar-stack:hover > *:nth-child(3) { transform: translateX(24px); }
|
||||||
|
.coauthor-avatar-stack:hover > *:nth-child(4) { transform: translateX(36px); }
|
||||||
|
.coauthor-avatar-stack:hover > *:nth-child(5) { transform: translateX(48px); }
|
||||||
|
.coauthor-avatar-stack:hover > *:nth-child(6) { transform: translateX(60px); }
|
||||||
|
.coauthor-avatar-stack:hover > *:nth-child(7) { transform: translateX(72px); }
|
||||||
|
.coauthor-avatar-stack:hover > *:nth-child(8) { transform: translateX(84px); }
|
||||||
|
.coauthor-avatar-stack:hover > *:nth-child(9) { transform: translateX(96px); }
|
||||||
|
.coauthor-avatar-stack:hover > *:nth-child(10) { transform: translateX(108px); }
|
||||||
|
|
||||||
.latest-commit .message-wrapper {
|
.latest-commit .message-wrapper {
|
||||||
max-width: calc(100% - 2.5rem);
|
max-width: calc(100% - 2.5rem);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user