appview/pages: show co-authors and committers in commit messages #863

merged
opened by oppi.li targeting master from op/rkowwwsvpoqx

in smaller views of a commit, such as logs, only the profile pictures of coauthors are shown, and the lead author's identity is suffixed with a "+ N" to indicate co-authors.

in larger views of a commit, such as the commit view, the authors and the committer are explicitly laid out, DIDs are resolved where possible and shown alongside profile pictures.

Signed-off-by: oppiliappan me@oppi.li

Changed files
+116 -52
appview
types
+1 -1
appview/pages/funcmap.go
···
}
return pairs, nil
},
-
"append": func(s []string, values ...string) []string {
+
"append": func(s []any, values ...any) []any {
s = append(s, values...)
return s
},
+34 -9
appview/pages/templates/repo/commit.html
···
</div>
<div class="flex flex-wrap items-center space-x-2">
-
<p class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-300">
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
-
-
{{ if $did }}
-
{{ template "user/fragments/picHandleLink" $did }}
-
{{ else }}
-
<a href="mailto:{{ $commit.Author.Email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $commit.Author.Name }}</a>
-
{{ end }}
+
<p class="flex flex-wrap items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
+
{{ template "attribution" . }}
<span class="px-1 select-none before:content-['\00B7']"></span>
-
{{ template "repo/fragments/time" $commit.Committer.When }}
+
{{ template "repo/fragments/time" $commit.Committer.When }}
<span class="px-1 select-none before:content-['\00B7']"></span>
<a href="/{{ $repo }}/commit/{{ $commit.This }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ slice $commit.This 0 8 }}</a>
···
</section>
{{end}}
+
{{ define "attribution" }}
+
{{ $commit := .Diff.Commit }}
+
{{ $showCommitter := true }}
+
{{ if eq $commit.Author.Email $commit.Committer.Email }}
+
{{ $showCommitter = false }}
+
{{ end }}
+
+
{{ if $showCommitter }}
+
authored by {{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid) }}
+
{{ range $commit.CoAuthors }}
+
{{ template "attributedUser" (list .Email .Name $.EmailToDid) }}
+
{{ end }}
+
and committed by {{ template "attributedUser" (list $commit.Committer.Email $commit.Committer.Name $.EmailToDid) }}
+
{{ else }}
+
{{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid )}}
+
{{ end }}
+
{{ end }}
+
+
{{ define "attributedUser" }}
+
{{ $email := index . 0 }}
+
{{ $name := index . 1 }}
+
{{ $map := index . 2 }}
+
{{ $did := index $map $email }}
+
+
{{ if $did }}
+
{{ template "user/fragments/picHandleLink" $did }}
+
{{ else }}
+
<a href="mailto:{{ $email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $name }}</a>
+
{{ end }}
+
{{ end }}
+
{{ define "topbarLayout" }}
<header class="col-span-full" style="z-index: 20;">
{{ template "layouts/fragments/topbar" . }}
+30 -8
appview/pages/templates/repo/index.html
···
{{ end }}
<div class="flex items-center justify-between pb-5">
{{ block "branchSelector" . }}{{ end }}
-
<div class="flex md:hidden items-center gap-2">
+
<div class="flex md:hidden items-center gap-3">
<a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold">
{{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }}
</a>
···
{{ define "branchSelector" }}
<div class="flex gap-2 items-center justify-between w-full">
-
<div class="flex gap-2 items-center">
+
<div class="flex gap-2 items-stretch">
<select
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)"
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
···
<span
class="mx-1 before:content-['·'] before:select-none"
></span>
-
<span>
-
{{ $did := index $.EmailToDid .Author.Email }}
-
<a href="{{ if $did }}/{{ resolve $did }}{{ else }}mailto:{{ .Author.Email }}{{ end }}"
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
-
>{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ .Author.Name }}{{ end }}</a>
-
</span>
+
{{ template "attribution" (list . $.EmailToDid) }}
<div class="inline-block px-1 select-none after:content-['·']"></div>
{{ template "repo/fragments/time" .Committer.When }}
···
</div>
{{ end }}
+
{{ define "attribution" }}
+
{{ $commit := index . 0 }}
+
{{ $map := index . 1 }}
+
<span class="flex items-center">
+
{{ $author := index $map $commit.Author.Email }}
+
{{ $coauthors := $commit.CoAuthors }}
+
{{ $all := list }}
+
+
{{ if $author }}
+
{{ $all = append $all $author }}
+
{{ end }}
+
{{ range $coauthors }}
+
{{ $co := index $map .Email }}
+
{{ if $co }}
+
{{ $all = append $all $co }}
+
{{ end }}
+
{{ end }}
+
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
+
class="no-underline hover:underline">
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
+
</a>
+
</span>
+
{{ end }}
+
{{ define "branchList" }}
{{ if gt (len .BranchesTrunc) 0 }}
<div id="branches" class="md:col-span-1 px-2 py-4 border-t border-gray-200 dark:border-gray-700">
+40 -23
appview/pages/templates/repo/log.html
···
<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
{{ $grid := "grid grid-cols-14 gap-4" }}
<div class="{{ $grid }}">
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2">Author</div>
+
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Author</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-1"></div>
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2 justify-self-end">Date</div>
</div>
{{ range $index, $commit := .Commits }}
{{ $messageParts := splitN $commit.Message "\n\n" 2 }}
<div class="{{ $grid }} py-3">
-
<div class="align-top truncate col-span-2">
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
-
{{ if $did }}
-
{{ template "user/fragments/picHandleLink" $did }}
-
{{ else }}
-
<a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a>
-
{{ end }}
+
<div class="align-top col-span-3">
+
{{ template "attribution" (list $commit $.EmailToDid) }}
</div>
<div class="align-top font-mono flex items-start col-span-3">
{{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }}
···
<div class="align-top col-span-6">
<div>
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a>
+
{{ if gt (len $messageParts) 1 }}
<button class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded" hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')">{{ i "ellipsis" "w-3 h-3" }}</button>
{{ end }}
···
</span>
{{ end }}
{{ end }}
+
+
<!-- ci status -->
+
<span class="text-xs">
+
{{ $pipeline := index $.Pipelines .Hash.String }}
+
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
+
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
+
{{ end }}
+
</span>
</div>
{{ if gt (len $messageParts) 1 }}
<p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p>
{{ end }}
</div>
-
<div class="align-top col-span-1">
-
<!-- ci status -->
-
{{ $pipeline := index $.Pipelines .Hash.String }}
-
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
-
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
-
{{ end }}
-
</div>
<div class="align-top justify-self-end text-gray-500 dark:text-gray-400 col-span-2">{{ template "repo/fragments/shortTimeAgo" $commit.Committer.When }}</div>
</div>
{{ end }}
···
</a>
</span>
<span class="mx-2 before:content-['·'] before:select-none"></span>
-
<span>
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
-
<a href="{{ if $did }}/{{ $did }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline">
-
{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ $commit.Author.Name }}{{ end }}
-
</a>
-
</span>
+
{{ template "attribution" (list $commit $.EmailToDid) }}
<div class="inline-block px-1 select-none after:content-['·']"></div>
<span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span>
···
{{ end }}
+
{{ define "attribution" }}
+
{{ $commit := index . 0 }}
+
{{ $map := index . 1 }}
+
<span class="flex items-center gap-1">
+
{{ $author := index $map $commit.Author.Email }}
+
{{ $coauthors := $commit.CoAuthors }}
+
{{ $all := list }}
+
+
{{ if $author }}
+
{{ $all = append $all $author }}
+
{{ end }}
+
{{ range $coauthors }}
+
{{ $co := index $map .Email }}
+
{{ if $co }}
+
{{ $all = append $all $co }}
+
{{ end }}
+
{{ end }}
+
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
+
class="no-underline hover:underline">
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
+
</a>
+
</span>
+
{{ end }}
+
{{ define "repoAfter" }}
{{ $commits_len := len .Commits }}
<div class="flex justify-end mt-4 gap-2">
+10 -10
appview/repo/repo_util.go
···
package repo
import (
+
"maps"
"slices"
"sort"
"strings"
···
func uniqueEmails(commits []types.Commit) []string {
emails := make(map[string]struct{})
for _, commit := range commits {
-
if commit.Author.Email != "" {
-
emails[commit.Author.Email] = struct{}{}
+
emails[commit.Author.Email] = struct{}{}
+
emails[commit.Committer.Email] = struct{}{}
+
for _, c := range commit.CoAuthors() {
+
emails[c.Email] = struct{}{}
}
-
if commit.Committer.Email != "" {
-
emails[commit.Committer.Email] = struct{}{}
-
}
-
}
-
var uniqueEmails []string
-
for email := range emails {
-
uniqueEmails = append(uniqueEmails, email)
}
-
return uniqueEmails
+
+
// delete empty emails if any, from the set
+
delete(emails, "")
+
+
return slices.Collect(maps.Keys(emails))
}
func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) {
+1 -1
types/commit.go
···
coAuthorRegex = regexp.MustCompile(`(?im)^Co-authored-by:\s*(.+?)\s*<([^>]+)>`)
)
-
func (commit *Commit) CoAuthors() []object.Signature {
+
func (commit Commit) CoAuthors() []object.Signature {
var coAuthors []object.Signature
matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)