feat: add language statistics to repo page #208

Changed files
+81 -134
appview
config
pages
templates
repo
settings
knotserver
types
+14 -4
knotserver/routes.go
···
return
}
-
languageFileCount := make(map[string]int)
+
languageFileCount := make(map[string]types.RepoLanguageFile)
err = recurseEntireTree(r.Context(), gr, func(absPath string) {
lang, safe := enry.GetLanguageByExtension(absPath)
···
content, _ := gr.FileContentN(absPath, 1024)
if !safe {
lang = enry.GetLanguage(absPath, content)
+
if len(lang) == 0 {
+
lang = "Other"
+
}
} else {
lang, _ = enry.GetLanguageByContent(absPath, content)
if len(lang) == 0 {
-
return
+
lang = "Other"
}
}
}
+
color := enry.GetColor(lang)
+
v, ok := languageFileCount[lang]
if ok {
-
languageFileCount[lang] = v + 1
+
v.Count += 1
+
languageFileCount[lang] = v
} else {
-
languageFileCount[lang] = 1
+
languageFileCount[lang] = types.RepoLanguageFile{
+
Name: lang,
+
Color: color,
+
Count: 1,
+
}
}
}, "")
if err != nil {
+13 -1
types/repo.go
···
Status ForkStatus `json:"status"`
}
+
type RepoLanguageFile struct {
+
Name string
+
Count int
+
Color string
+
}
+
+
type RepoLanguageDetails struct {
+
Name string
+
Percentage float32
+
Color string
+
}
+
type RepoLanguageResponse struct {
// Language: Percentage
-
Languages map[string]int `json:"languages"`
+
Languages map[string]RepoLanguageFile `json:"languages"`
}
+31 -23
appview/pages/templates/layouts/repobase.html
···
{{ define "title" }}{{ .RepoInfo.FullName }}{{ end }}
{{ define "content" }}
-
<section id="repo-header" class="mb-4 py-2 px-6 dark:text-white">
-
{{ if .RepoInfo.Source }}
-
<p class="text-sm">
-
<div class="flex items-center">
-
{{ i "git-fork" "w-3 h-3 mr-1"}}
-
forked from
-
{{ $sourceOwner := didOrHandle .RepoInfo.Source.Did .RepoInfo.SourceHandle }}
-
<a class="ml-1 underline" href="/{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}">{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}</a>
-
</div>
-
</p>
-
{{ end }}
-
<div class="text-lg flex items-center justify-between">
-
<div>
-
<a href="/{{ .RepoInfo.OwnerWithAt }}">{{ .RepoInfo.OwnerWithAt }}</a>
-
<span class="select-none">/</span>
-
<a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a>
-
</div>
+
<section id="repo-header" class="mb-4 py-2 px-6 dark:text-white">
+
{{ if .RepoInfo.Source }}
+
<p class="text-sm">
+
<div class="flex items-center">
+
{{ i "git-fork" "w-3 h-3 mr-1"}}
+
forked from
+
{{ $sourceOwner := didOrHandle .RepoInfo.Source.Did .RepoInfo.SourceHandle }}
+
<a class="ml-1 underline" href="/{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}">{{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}</a>
+
</div>
+
</p>
+
{{ end }}
+
<div class="text-lg flex items-center justify-between">
+
<div>
+
<a href="/{{ .RepoInfo.OwnerWithAt }}">{{ .RepoInfo.OwnerWithAt }}</a>
+
<span class="select-none">/</span>
+
<a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a>
+
</div>
-
{{ template "repo/fragments/repoActions" .RepoInfo }}
-
</div>
-
{{ template "repo/fragments/repoDescription" . }}
-
</section>
-
<section class="min-h-screen flex flex-col drop-shadow-sm">
+
{{ template "repo/fragments/repoActions" .RepoInfo }}
+
</div>
+
{{ template "repo/fragments/repoDescription" . }}
+
</section>
+
+
<section
+
class="min-h-screen w-full flex flex-col drop-shadow-sm"
+
>
<nav class="w-full pl-4 overflow-auto">
<div class="flex z-60">
{{ $activeTabStyles := "-mb-px bg-white dark:bg-gray-800" }}
···
{{ end }}
</div>
</nav>
+
<div
+
class="bg-white dark:bg-gray-800 rounded-t overflow-hidden relative w-full drop-shadow-sm"
+
>
+
{{ block "repoLanguages" . }}{{ end }}
+
</div>
<section
-
class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white"
+
class="bg-white dark:bg-gray-800 p-6 rounded-b relative w-full drop-shadow-sm dark:text-white"
>
{{ block "repoContent" . }}{{ end }}
</section>
+2 -2
appview/pages/templates/settings.html
···
<form
hx-delete="/settings/emails"
hx-confirm="Are you sure you wish to delete the email '{{ .Address }}'?"
-
hx-indicator="#delete-email-{{ .Address }}-spinner"
+
hx-indicator="#delete-email-{{ $index }}-spinner"
>
<input type="hidden" name="email" value="{{ .Address }}">
<button
···
>
{{ i "trash-2" "w-5 h-5" }}
<span class="hidden md:inline">delete</span>
-
<span id="delete-email-{{ .Address }}-spinner" class="group">
+
<span id="delete-email-{{ $index }}-spinner" class="group">
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
</span>
</button>
-52
appview/repo/repo_util.go
···
"context"
"crypto/rand"
"fmt"
-
"log"
"math/big"
"github.com/go-git/go-git/v5/plumbing/object"
-
"tangled.sh/tangled.sh/core/appview/db"
-
"tangled.sh/tangled.sh/core/crypto"
-
"tangled.sh/tangled.sh/core/types"
)
func uniqueEmails(commits []*object.Commit) []string {
···
return emailToDidOrHandle
}
-
func verifiedObjectCommits(r *Repo, emailToDid map[string]string, commits []*object.Commit) (map[string]bool, error) {
-
ndCommits := []types.NiceDiff{}
-
for _, commit := range commits {
-
ndCommits = append(ndCommits, types.ObjectCommitToNiceDiff(commit))
-
}
-
return verifiedCommits(r, emailToDid, ndCommits)
-
}
-
-
func verifiedCommits(r *Repo, emailToDid map[string]string, ndCommits []types.NiceDiff) (map[string]bool, error) {
-
hashToVerified := make(map[string]bool)
-
-
didPubkeyCache := make(map[string][]db.PublicKey)
-
-
for _, commit := range ndCommits {
-
c := commit.Commit
-
-
committerEmail := c.Committer.Email
-
if did, exists := emailToDid[committerEmail]; exists {
-
// check if we've already fetched public keys for this did
-
pubKeys, ok := didPubkeyCache[did]
-
if !ok {
-
// fetch and cache public keys
-
keys, err := db.GetPublicKeysForDid(r.db, did)
-
if err != nil {
-
log.Printf("failed to fetch pubkey for %s: %v", committerEmail, err)
-
continue
-
}
-
pubKeys = keys
-
didPubkeyCache[did] = pubKeys
-
}
-
-
verified := false
-
-
// try to verify with any associated pubkeys
-
for _, pk := range pubKeys {
-
if _, ok := crypto.VerifyCommitSignature(pk.Key, commit); ok {
-
verified = true
-
break
-
}
-
}
-
-
hashToVerified[c.This] = verified
-
}
-
}
-
-
return hashToVerified, nil
-
}
-
func randomString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
result := make([]byte, n)
-30
types/diff.go
···
return files
}
-
-
// ObjectCommitToNiceDiff is a compatibility function to convert a
-
// commit object into a NiceDiff structure.
-
func ObjectCommitToNiceDiff(c *object.Commit) NiceDiff {
-
var niceDiff NiceDiff
-
-
// set commit information
-
niceDiff.Commit.Message = c.Message
-
niceDiff.Commit.Author = c.Author
-
niceDiff.Commit.This = c.Hash.String()
-
niceDiff.Commit.Committer = c.Committer
-
niceDiff.Commit.Tree = c.TreeHash.String()
-
niceDiff.Commit.PGPSignature = c.PGPSignature
-
-
changeId, ok := c.ExtraHeaders["change-id"]
-
if ok {
-
niceDiff.Commit.ChangedId = string(changeId)
-
}
-
-
// set parent hash if available
-
if len(c.ParentHashes) > 0 {
-
niceDiff.Commit.Parent = c.ParentHashes[0].String()
-
}
-
-
// XXX: Stats and Diff fields are typically populated
-
// after fetching the actual diff information, which isn't
-
// directly available in the commit object itself.
-
-
return niceDiff
-
}
+13 -15
appview/pages/templates/repo/log.html
···
{{ define "extrameta" }}
{{ $title := printf "commits &middot; %s" .RepoInfo.FullName }}
{{ $url := printf "https://tangled.sh/%s/commits" .RepoInfo.FullName }}
-
+
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
{{ end }}
{{ define "repoContent" }}
<section id="commit-table" class="overflow-x-auto">
<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">
-
commits
+
commits
</h2>
<!-- desktop view (hidden on small screens) -->
···
{{ end }}
</td>
<td class="py-3 align-top font-mono flex items-center">
-
{{ $verified := false }}
-
{{ $verified = index $.VerifiedCommits $commit.Hash.String }}
+
{{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }}
{{ $hashStyle := "text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900" }}
{{ if $verified }}
{{ $hashStyle = "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-2 rounded" }}
···
{{ end }}
</a>
<div class="{{ if not $verified }} ml-6 {{ end }}inline-flex">
-
<button class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
+
<button class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
title="Copy SHA"
onclick="navigator.clipboard.writeText('{{ $commit.Hash.String }}'); this.innerHTML=`{{ i "copy-check" "w-4 h-4" }}`; setTimeout(() => this.innerHTML=`{{ i "copy" "w-4 h-4" }}`, 1500)">
{{ i "copy" "w-4 h-4" }}
···
<div>
<div class="flex items-center justify-start">
<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 }}
+
{{ if gt (len $messageParts) 1 }}
<button class="ml-2 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 }}
-
+
{{ if index $.TagMap $commit.Hash.String }}
{{ range $tag := index $.TagMap $commit.Hash.String }}
<span class="ml-2 text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 inline-flex items-center">
···
</span>
{{ end }}
{{ end }}
-
+
</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 class="flex items-center justify-between">
<div class="flex-1">
<div class="inline">
-
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}"
+
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}"
class="inline no-underline hover:underline dark:text-white">
{{ index $messageParts 0 }}
</a>
{{ if gt (len $messageParts) 1 }}
-
<button
+
<button
class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600 ml-2"
hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')">
{{ i "ellipsis" "w-3 h-3" }}
···
</p>
{{ end }}
</div>
-
<a href="/{{ $.RepoInfo.FullName }}/tree/{{ $commit.Hash.String }}"
-
class="p-1 mr-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
+
<a href="/{{ $.RepoInfo.FullName }}/tree/{{ $commit.Hash.String }}"
+
class="p-1 mr-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
title="Browse repository at this commit">
{{ i "folder-code" "w-4 h-4" }}
</a>
···
</div>
<div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center">
-
{{ $verified := false }}
-
{{ $verified = index $.VerifiedCommits $commit.Hash.String }}
+
{{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }}
{{ $hashStyle := "text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900" }}
{{ if $verified }}
{{ $hashStyle = "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-2 rounded" }}
+2 -1
appview/config/config.go
···
}
type ResendConfig struct {
-
ApiKey string `env:"API_KEY"`
+
ApiKey string `env:"API_KEY"`
+
SentFrom string `env:"SENT_FROM, default=noreply@notifs.tangled.sh"`
}
type CamoConfig struct {
+1 -1
appview/settings/settings.go
···
return email.Email{
APIKey: s.Config.Resend.ApiKey,
-
From: "noreply@notifs.tangled.sh",
+
From: s.Config.Resend.SentFrom,
To: emailAddr,
Subject: "Verify your Tangled email",
Text: `Click the link below (or copy and paste it into your browser) to verify your email address.
+1 -1
appview/repo/repo.go
···
return
}
-
emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Author.Email}, true)
+
emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true)
if err != nil {
log.Println("failed to get email to did mapping:", err)
}
+2 -2
go.mod
···
github.com/go-git/go-git/v5 v5.14.0
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.4.0
+
github.com/gorilla/websocket v1.5.3
github.com/hiddeco/sshsig v0.2.0
github.com/ipfs/go-cid v0.5.0
github.com/lestrrat-go/jwx/v2 v2.1.6
···
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
-
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
···
replace github.com/bluekeyes/go-gitdiff => tangled.sh/oppi.li/go-gitdiff v0.8.2
-
replace github.com/alecthomas/chroma/v2 => github.com/oppiliappan/chroma/v2 v2.17.0
+
replace github.com/alecthomas/chroma/v2 => github.com/oppiliappan/chroma/v2 v2.19.0
// from bluesky-social/indigo
replace github.com/gocql/gocql => github.com/scylladb/gocql v1.14.4
+2 -2
go.sum
···
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
-
github.com/oppiliappan/chroma/v2 v2.17.0 h1:Qi8qnCvhCn8VxwD+BGpt7n5BdLX32/2kRBlT7hAR5Ko=
-
github.com/oppiliappan/chroma/v2 v2.17.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
+
github.com/oppiliappan/chroma/v2 v2.19.0 h1:PN7/pb+6JRKCva30NPTtRJMlrOyzgpPpIroNzy4ekHU=
+
github.com/oppiliappan/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/oppiliappan/go-git/v5 v5.17.0 h1:CuJnpcIDxr0oiNaSHMconovSWnowHznVDG+AhjGuSEo=
github.com/oppiliappan/go-git/v5 v5.17.0/go.mod h1:q/FE8C3SPMoRN7LoH9vRFiBzidAOBWJPS1CqVS8DN+w=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=