mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-11 09:15:31 +02:00
more cleanup
This commit is contained in:
parent
10461b2f4e
commit
30b1928c06
@ -1209,12 +1209,15 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([
|
||||
for _, c := range oldCommits {
|
||||
user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
|
||||
coAuthorSigs := c.CoAuthorSignatures()
|
||||
coAuthors := make([]*CoAuthorUser, 0, len(coAuthorSigs))
|
||||
for _, sig := range coAuthorSigs {
|
||||
coAuthors = append(coAuthors, &CoAuthorUser{
|
||||
GiteaUser: emailUserMap.GetByEmail(sig.Email),
|
||||
TrailerSignature: sig,
|
||||
})
|
||||
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{
|
||||
User: user,
|
||||
|
||||
@ -69,26 +69,64 @@ func (c *CommitMessage) MessageBody() string {
|
||||
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, matching `git interpret-trailers`.
|
||||
// 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(), "\n")
|
||||
if idx := strings.LastIndex(body, "\n\n"); idx >= 0 {
|
||||
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:]
|
||||
}
|
||||
for line := range strings.SplitSeq(body, "\n") {
|
||||
var rest string
|
||||
var ok bool
|
||||
if rest, ok = strings.CutPrefix(line, "Co-authored-by:"); !ok {
|
||||
rest, ok = strings.CutPrefix(line, "Co-committed-by:")
|
||||
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
|
||||
}
|
||||
@ -113,6 +151,31 @@ func (c *CommitMessage) CoAuthorSignatures() []*Signature {
|
||||
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).
|
||||
// It returns nil if no such parent exists.
|
||||
func (c *Commit) ParentID(n int) (ObjectID, error) {
|
||||
|
||||
@ -208,3 +208,92 @@ func Test_GetCommitBranchStart(t *testing.T) {
|
||||
assert.NotEmpty(t, 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,10 +29,16 @@ type PushCommit struct {
|
||||
AuthorName string
|
||||
CommitterEmail string
|
||||
CommitterName string
|
||||
CoAuthors []*git.Signature
|
||||
CoAuthors []*PushCommitCoAuthor `json:",omitempty"`
|
||||
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.
|
||||
type PushCommits struct {
|
||||
Commits []*PushCommit
|
||||
@ -149,6 +155,17 @@ func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string {
|
||||
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.
|
||||
func CommitToPushCommit(commit *git.Commit) *PushCommit {
|
||||
return &PushCommit{
|
||||
@ -158,11 +175,34 @@ func CommitToPushCommit(commit *git.Commit) *PushCommit {
|
||||
AuthorName: commit.Author.Name,
|
||||
CommitterEmail: commit.Committer.Email,
|
||||
CommitterName: commit.Committer.Name,
|
||||
CoAuthors: commit.CoAuthorSignatures(),
|
||||
CoAuthors: pushCommitCoAuthorsFromSignatures(commit.CoAuthorSignatures()),
|
||||
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.
|
||||
func GitToPushCommits(gitCommits []*git.Commit) *PushCommits {
|
||||
commits := make([]*PushCommit, 0, len(gitCommits))
|
||||
|
||||
@ -157,6 +157,11 @@ func TestCommitToPushCommit(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -2591,6 +2591,7 @@
|
||||
"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",
|
||||
|
||||
@ -132,16 +132,18 @@ func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_
|
||||
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)
|
||||
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,
|
||||
})
|
||||
}
|
||||
c.CoAuthors = append(c.CoAuthors, &user_model.CoAuthorUser{
|
||||
GiteaUser: giteaUser,
|
||||
TrailerSignature: sig,
|
||||
})
|
||||
}
|
||||
|
||||
c.Verification = asymkey_service.ParseCommitWithSignature(ctx, c.Commit)
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
{{if or .CoAuthors .CoAuthorSignatures}}
|
||||
{{if .CoAuthors}}
|
||||
{{- $additionalClasses := .AdditionalClasses -}}
|
||||
<span class="coauthor-avatar-stack {{$additionalClasses}}">
|
||||
{{- 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 -}}
|
||||
{{- if .CoAuthors -}}
|
||||
{{- $coCount := len .CoAuthors -}}
|
||||
{{- $maxCo := 9 -}}
|
||||
{{- $visibleCo := .CoAuthors -}}
|
||||
{{- $overflow := 0 -}}
|
||||
{{- if gt $coCount $maxCo -}}
|
||||
{{- $visibleCo = slice .CoAuthors 0 $maxCo -}}
|
||||
{{- $overflow = Eval $coCount "-" $maxCo -}}
|
||||
{{- $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 -}}
|
||||
@ -22,24 +22,10 @@
|
||||
{{- ctx.AvatarUtils.AvatarByEmail .TrailerSignature.Email .TrailerSignature.Name 20 -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if gt $overflow 0 -}}
|
||||
<span class="coauthor-overflow-chip" data-tooltip-content="{{ctx.Locale.TrN $overflow "repo.commits.coauthor_others_1" "repo.commits.coauthor_others_n" $overflow}}">+{{$overflow}}</span>
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- $coCount := len .CoAuthorSignatures -}}
|
||||
{{- $maxCo := 9 -}}
|
||||
{{- $visibleCo := .CoAuthorSignatures -}}
|
||||
{{- $overflow := 0 -}}
|
||||
{{- if gt $coCount $maxCo -}}
|
||||
{{- $visibleCo = slice .CoAuthorSignatures 0 $maxCo -}}
|
||||
{{- $overflow = Eval $coCount "-" $maxCo -}}
|
||||
{{- end -}}
|
||||
{{- range $visibleCo -}}
|
||||
{{- ctx.AvatarUtils.AvatarByEmail .Email .Name 20 -}}
|
||||
{{- end -}}
|
||||
{{- if gt $overflow 0 -}}
|
||||
<span class="coauthor-overflow-chip" data-tooltip-content="{{ctx.Locale.TrN $overflow "repo.commits.coauthor_others_1" "repo.commits.coauthor_others_n" $overflow}}">+{{$overflow}}</span>
|
||||
{{- 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}}
|
||||
|
||||
@ -29,7 +29,8 @@
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- ctx.Locale.TrN $coCount "repo.commits.coauthor_others_1" "repo.commits.coauthor_others_n" $coCount -}}
|
||||
{{- $peopleCount := Eval $coCount "+" 1 -}}
|
||||
{{- ctx.Locale.Tr "repo.commits.coauthor_people" $peopleCount -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- if .AuthorUser -}}
|
||||
|
||||
@ -91,7 +91,7 @@
|
||||
{{range $pushCommit := $push.Commits}}
|
||||
{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
|
||||
<div class="flex-text-block">
|
||||
{{template "repo/commit_coauthor_avatar_stack" dict "AuthorSignature" (dict "Email" .AuthorEmail "Name" .AuthorName) "CoAuthorSignatures" .CoAuthors}}
|
||||
{{template "repo/commit_coauthor_avatar_stack" dict "AuthorSignature" .AuthorSignature "CoAuthors" .CoAuthorUsers}}
|
||||
<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
|
||||
<span class="tw-inline-block tw-truncate">
|
||||
{{ctx.RenderUtils.RenderCommitMessage $pushCommit.Message $repo}}
|
||||
|
||||
@ -1408,13 +1408,18 @@ tbody.commit-list {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.coauthor-avatar-stack {
|
||||
.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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user