+2
-2
.air/knotserver.toml
+2
-2
.air/knotserver.toml
···
1
1
[build]
2
-
cmd = 'go build -ldflags "-X tangled.sh/tangled.sh/core/knotserver.version=$(git describe --tags --long)" -o .bin/knot ./cmd/knotserver/main.go'
3
-
bin = ".bin/knot"
2
+
cmd = 'go build -ldflags "-X tangled.sh/tangled.sh/core/knotserver.version=$(git describe --tags --long)" -o .bin/knot ./cmd/knot/'
3
+
bin = ".bin/knot server"
4
4
root = "."
5
5
6
6
exclude_regex = [""]
+3
-3
appview/db/pubkeys.go
+3
-3
appview/db/pubkeys.go
···
15
15
16
16
func DeletePublicKey(e Execer, did, name, key string) error {
17
17
_, err := e.Exec(`
18
-
delete from public_keys
18
+
delete from public_keys
19
19
where did = ? and name = ? and key = ?`,
20
20
did, name, key)
21
21
return err
···
23
23
24
24
func DeletePublicKeyByRkey(e Execer, did, rkey string) error {
25
25
_, err := e.Exec(`
26
-
delete from public_keys
26
+
delete from public_keys
27
27
where did = ? and rkey = ?`,
28
28
did, rkey)
29
29
return err
···
75
75
return keys, nil
76
76
}
77
77
78
-
func GetPublicKeys(e Execer, did string) ([]PublicKey, error) {
78
+
func GetPublicKeysForDid(e Execer, did string) ([]PublicKey, error) {
79
79
var keys []PublicKey
80
80
81
81
rows, err := e.Query(`select did, key, name, rkey, created from public_keys where did = ?`, did)
+3
appview/pages/pages.go
+3
appview/pages/pages.go
···
414
414
HTMLReadme template.HTML
415
415
Raw bool
416
416
EmailToDidOrHandle map[string]string
417
+
VerifiedCommits map[string]bool
417
418
Languages *types.RepoLanguageResponse
418
419
types.RepoIndexResponse
419
420
}
···
452
453
types.RepoLogResponse
453
454
Active string
454
455
EmailToDidOrHandle map[string]string
456
+
VerifiedCommits map[string]bool
455
457
}
456
458
457
459
func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
···
464
466
RepoInfo repoinfo.RepoInfo
465
467
Active string
466
468
EmailToDidOrHandle map[string]string
469
+
Verified bool
467
470
468
471
types.RepoCommitResponse
469
472
}
+11
-2
appview/pages/templates/repo/commit.html
+11
-2
appview/pages/templates/repo/commit.html
···
3
3
{{ define "extrameta" }}
4
4
{{ $title := printf "commit %s · %s" .Diff.Commit.This .RepoInfo.FullName }}
5
5
{{ $url := printf "https://tangled.sh/%s/commit/%s" .RepoInfo.FullName .Diff.Commit.This }}
6
-
6
+
7
7
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
8
8
{{ end }}
9
9
···
24
24
</div>
25
25
</div>
26
26
27
-
<div class="flex items-center">
27
+
<div class="flex items-center space-x-2">
28
28
<p class="text-sm text-gray-500 dark:text-gray-300">
29
29
{{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }}
30
30
···
45
45
<a href="/{{ $repo }}/commit/{{ $commit.Parent }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ slice $commit.Parent 0 8 }}</a>
46
46
{{ end }}
47
47
</p>
48
+
49
+
{{ if .Verified }}
50
+
<span class="text-sm bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-2 py-1 rounded">
51
+
<div class="flex items-center gap-2">
52
+
{{ i "shield-check" "w-4 h-4" }}
53
+
verified
54
+
</div>
55
+
</span>
56
+
{{ end }}
48
57
</div>
49
58
50
59
</section>
+16
-8
appview/pages/templates/repo/index.html
+16
-8
appview/pages/templates/repo/index.html
···
222
222
</div>
223
223
</div>
224
224
225
-
<div class="text-xs text-gray-500 dark:text-gray-400">
226
-
<span class="font-mono">
227
-
<a
228
-
href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}"
229
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
230
-
>{{ slice .Hash.String 0 8 }}</a
231
-
></span
232
-
>
225
+
<div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center">
226
+
{{ $verified := false }}
227
+
{{ $verified = index $.VerifiedCommits .Hash.String }}
228
+
{{ $hashStyle := "text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900" }}
229
+
{{ if $verified }}
230
+
{{ $hashStyle = "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-2 rounded" }}
231
+
{{ end }}
232
+
<span class="font-mono">
233
+
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}"
234
+
class="no-underline hover:underline {{ $hashStyle }} px-2 py-1 rounded flex items-center gap-2">
235
+
{{ slice .Hash.String 0 8 }}
236
+
{{ if $verified }}
237
+
{{ i "shield-check" "w-3 h-3" }}
238
+
{{ end }}
239
+
</a>
240
+
</span>
233
241
<span
234
242
class="mx-2 before:content-['·'] before:select-none"
235
243
></span>
+26
-6
appview/pages/templates/repo/log.html
+26
-6
appview/pages/templates/repo/log.html
···
35
35
<a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a>
36
36
{{ end }}
37
37
</td>
38
-
<td class=" py-3 align-top font-mono flex items-end">
39
-
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ slice $commit.Hash.String 0 8 }}</a>
40
-
<div class="inline-flex">
38
+
<td class="py-3 align-top font-mono flex items-center">
39
+
{{ $verified := false }}
40
+
{{ $verified = index $.VerifiedCommits $commit.Hash.String }}
41
+
{{ $hashStyle := "text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900" }}
42
+
{{ if $verified }}
43
+
{{ $hashStyle = "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-2 rounded" }}
44
+
{{ end }}
45
+
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="no-underline hover:underline {{ $hashStyle }} px-2 py-1/2 rounded flex items-center gap-2">
46
+
{{ slice $commit.Hash.String 0 8 }}
47
+
{{ if $verified }}
48
+
{{ i "shield-check" "w-4 h-4" }}
49
+
{{ end }}
50
+
</a>
51
+
<div class="{{ if not $verified }} ml-6 {{ end }}inline-flex">
41
52
<button class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
42
53
title="Copy SHA"
43
54
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)">
···
80
91
<!-- mobile view (visible only on small screens) -->
81
92
<div class="md:hidden">
82
93
{{ range $index, $commit := .Commits }}
83
-
<div class="relative p-2 {{ if ne $index (sub (len $.Commits) 1) }}border-b border-gray-200 dark:border-gray-700{{ end }}">
94
+
<div class="relative p-2 mb-2 {{ if ne $index (sub (len $.Commits) 1) }}border-b border-gray-200 dark:border-gray-700{{ end }}">
84
95
<div id="commit-message">
85
96
{{ $messageParts := splitN $commit.Message "\n\n" 2 }}
86
97
<div class="text-base cursor-pointer">
···
125
136
</div>
126
137
</div>
127
138
128
-
<div class="text-xs text-gray-500 dark:text-gray-400">
139
+
<div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center">
140
+
{{ $verified := false }}
141
+
{{ $verified = index $.VerifiedCommits $commit.Hash.String }}
142
+
{{ $hashStyle := "text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900" }}
143
+
{{ if $verified }}
144
+
{{ $hashStyle = "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 px-2 rounded" }}
145
+
{{ end }}
129
146
<span class="font-mono">
130
147
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}"
131
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline">
148
+
class="no-underline hover:underline {{ $hashStyle }} px-2 py-1 rounded flex items-center gap-2">
132
149
{{ slice $commit.Hash.String 0 8 }}
150
+
{{ if $verified }}
151
+
{{ i "shield-check" "w-3 h-3" }}
152
+
{{ end }}
133
153
</a>
134
154
</span>
135
155
<span class="mx-2 before:content-['·'] before:select-none"></span>
+36
-3
appview/repo/repo.go
+36
-3
appview/repo/repo.go
···
138
138
branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))]
139
139
140
140
emails := uniqueEmails(commitsTrunc)
141
+
emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true)
142
+
if err != nil {
143
+
log.Println("failed to get email to did map", err)
144
+
}
145
+
146
+
vc, err := verifiedObjectCommits(rp, emailToDidMap, commitsTrunc)
147
+
if err != nil {
148
+
log.Println(err)
149
+
}
141
150
142
151
user := rp.oauth.GetUser(r)
143
152
repoInfo := f.RepoInfo(user)
···
178
187
TagsTrunc: tagsTrunc,
179
188
ForkInfo: forkInfo,
180
189
BranchesTrunc: branchesTrunc,
181
-
EmailToDidOrHandle: EmailToDidOrHandle(rp, emails),
190
+
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
191
+
VerifiedCommits: vc,
182
192
Languages: repoLanguages,
183
193
})
184
194
return
···
293
303
}
294
304
295
305
user := rp.oauth.GetUser(r)
306
+
307
+
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(repolog.Commits), true)
308
+
if err != nil {
309
+
log.Println("failed to fetch email to did mapping", err)
310
+
}
311
+
312
+
vc, err := verifiedObjectCommits(rp, emailToDidMap, repolog.Commits)
313
+
if err != nil {
314
+
log.Println(err)
315
+
}
316
+
296
317
rp.pages.RepoLog(w, pages.RepoLogParams{
297
318
LoggedInUser: user,
298
319
TagMap: tagMap,
299
320
RepoInfo: f.RepoInfo(user),
300
321
RepoLogResponse: *repolog,
301
-
EmailToDidOrHandle: EmailToDidOrHandle(rp, uniqueEmails(repolog.Commits)),
322
+
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
323
+
VerifiedCommits: vc,
302
324
})
303
325
return
304
326
}
···
438
460
return
439
461
}
440
462
463
+
emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Author.Email}, true)
464
+
if err != nil {
465
+
log.Println("failed to get email to did mapping:", err)
466
+
}
467
+
468
+
vc, err := verifiedCommits(rp, emailToDidMap, []types.NiceDiff{*result.Diff})
469
+
if err != nil {
470
+
log.Println(err)
471
+
}
472
+
441
473
user := rp.oauth.GetUser(r)
442
474
rp.pages.RepoCommit(w, pages.RepoCommitParams{
443
475
LoggedInUser: user,
444
476
RepoInfo: f.RepoInfo(user),
445
477
RepoCommitResponse: result,
446
-
EmailToDidOrHandle: EmailToDidOrHandle(rp, []string{result.Diff.Commit.Author.Email}),
478
+
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
479
+
Verified: vc[result.Diff.Commit.This],
447
480
})
448
481
return
449
482
}
+56
-6
appview/repo/repo_util.go
+56
-6
appview/repo/repo_util.go
···
9
9
10
10
"github.com/go-git/go-git/v5/plumbing/object"
11
11
"tangled.sh/tangled.sh/core/appview/db"
12
+
"tangled.sh/tangled.sh/core/crypto"
13
+
"tangled.sh/tangled.sh/core/types"
12
14
)
13
15
14
16
func uniqueEmails(commits []*object.Commit) []string {
···
56
58
return
57
59
}
58
60
59
-
func EmailToDidOrHandle(r *Repo, emails []string) map[string]string {
60
-
emailToDid, err := db.GetEmailToDid(r.db, emails, true) // only get verified emails for mapping
61
-
if err != nil {
62
-
log.Printf("error fetching dids for emails: %v", err)
61
+
// emailToDidOrHandle takes an emailToDidMap from db.GetEmailToDid
62
+
// and resolves all dids to handles and returns a new map[string]string
63
+
func emailToDidOrHandle(r *Repo, emailToDidMap map[string]string) map[string]string {
64
+
if emailToDidMap == nil {
63
65
return nil
64
66
}
65
67
66
68
var dids []string
67
-
for _, v := range emailToDid {
69
+
for _, v := range emailToDidMap {
68
70
dids = append(dids, v)
69
71
}
70
72
resolvedIdents := r.idResolver.ResolveIdents(context.Background(), dids)
···
80
82
81
83
// Create map of email to didOrHandle for commit display
82
84
emailToDidOrHandle := make(map[string]string)
83
-
for email, did := range emailToDid {
85
+
for email, did := range emailToDidMap {
84
86
if didOrHandle, ok := didHandleMap[did]; ok {
85
87
emailToDidOrHandle[email] = didOrHandle
86
88
}
87
89
}
88
90
89
91
return emailToDidOrHandle
92
+
}
93
+
94
+
func verifiedObjectCommits(r *Repo, emailToDid map[string]string, commits []*object.Commit) (map[string]bool, error) {
95
+
ndCommits := []types.NiceDiff{}
96
+
for _, commit := range commits {
97
+
ndCommits = append(ndCommits, types.ObjectCommitToNiceDiff(commit))
98
+
}
99
+
return verifiedCommits(r, emailToDid, ndCommits)
100
+
}
101
+
102
+
func verifiedCommits(r *Repo, emailToDid map[string]string, ndCommits []types.NiceDiff) (map[string]bool, error) {
103
+
hashToVerified := make(map[string]bool)
104
+
105
+
didPubkeyCache := make(map[string][]db.PublicKey)
106
+
107
+
for _, commit := range ndCommits {
108
+
c := commit.Commit
109
+
110
+
committerEmail := c.Committer.Email
111
+
if did, exists := emailToDid[committerEmail]; exists {
112
+
// check if we've already fetched public keys for this did
113
+
pubKeys, ok := didPubkeyCache[did]
114
+
if !ok {
115
+
// fetch and cache public keys
116
+
keys, err := db.GetPublicKeysForDid(r.db, did)
117
+
if err != nil {
118
+
log.Printf("failed to fetch pubkey for %s: %v", committerEmail, err)
119
+
continue
120
+
}
121
+
pubKeys = keys
122
+
didPubkeyCache[did] = pubKeys
123
+
}
124
+
125
+
verified := false
126
+
127
+
// try to verify with any associated pubkeys
128
+
for _, pk := range pubKeys {
129
+
if _, ok := crypto.VerifyCommitSignature(pk.Key, commit); ok {
130
+
verified = true
131
+
break
132
+
}
133
+
}
134
+
135
+
hashToVerified[c.This] = verified
136
+
}
137
+
}
138
+
139
+
return hashToVerified, nil
90
140
}
91
141
92
142
func randomString(n int) string {
+1
-1
appview/settings/settings.go
+1
-1
appview/settings/settings.go
+1
-1
appview/state/state.go
+1
-1
appview/state/state.go
+30
types/diff.go
+30
types/diff.go
···
77
77
78
78
return files
79
79
}
80
+
81
+
// ObjectCommitToNiceDiff is a compatibility function to convert a
82
+
// commit object into a NiceDiff structure.
83
+
func ObjectCommitToNiceDiff(c *object.Commit) NiceDiff {
84
+
var niceDiff NiceDiff
85
+
86
+
// set commit information
87
+
niceDiff.Commit.Message = c.Message
88
+
niceDiff.Commit.Author = c.Author
89
+
niceDiff.Commit.This = c.Hash.String()
90
+
niceDiff.Commit.Committer = c.Committer
91
+
niceDiff.Commit.Tree = c.TreeHash.String()
92
+
niceDiff.Commit.PGPSignature = c.PGPSignature
93
+
94
+
changeId, ok := c.ExtraHeaders["change-id"]
95
+
if ok {
96
+
niceDiff.Commit.ChangedId = string(changeId)
97
+
}
98
+
99
+
// set parent hash if available
100
+
if len(c.ParentHashes) > 0 {
101
+
niceDiff.Commit.Parent = c.ParentHashes[0].String()
102
+
}
103
+
104
+
// XXX: Stats and Diff fields are typically populated
105
+
// after fetching the actual diff information, which isn't
106
+
// directly available in the commit object itself.
107
+
108
+
return niceDiff
109
+
}