feat(basin): ssh signature verification badges, left aligned layout, system mono font
10 files changed, +94 −116
- git.go modified +20 −12
- static/fonts/mono.txt deleted +0 −93
- static/fonts/mono.woff2 deleted +0 −0
- static/style.css modified +12 −8
- store.go modified +26 −0
- templates/commit.html modified +1 −0
- templates/log.html modified +2 −1
- templates/pull.html modified +1 −1
- templates/repo.html modified +1 −1
- web.go modified +31 −0
git.go modified
| @@ -4,6 +4,7 @@ import ( | ||
| 4 | 4 | "bytes" |
| 5 | 5 | "context" |
| 6 | 6 | "errors" |
| 7 | + "os" | |
| 7 | 8 | "os/exec" |
| 8 | 9 | "path" |
| 9 | 10 | "strconv" |
| @@ -17,15 +18,19 @@ type Git struct { | ||
| 17 | 18 | } |
| 18 | 19 | |
| 19 | 20 | type Commit struct { |
| 20 | - Hash string | |
| 21 | - Author string | |
| 22 | - Email string | |
| 23 | - Time time.Time | |
| 24 | - Subject string | |
| 25 | - Body string | |
| 26 | - Parents []string | |
| 21 | + Hash string | |
| 22 | + Author string | |
| 23 | + Email string | |
| 24 | + Time time.Time | |
| 25 | + Subject string | |
| 26 | + Body string | |
| 27 | + Parents []string | |
| 28 | + Signature string | |
| 29 | + Key string | |
| 27 | 30 | } |
| 28 | 31 | |
| 32 | +var signersFile string | |
| 33 | + | |
| 29 | 34 | type Entry struct { |
| 30 | 35 | Mode string |
| 31 | 36 | Type string |
| @@ -72,7 +77,10 @@ func (g Git) runEnv(env []string, stdin []byte, args ...string) ([]byte, error) | ||
| 72 | 77 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
| 73 | 78 | defer cancel() |
| 74 | 79 | cmd := exec.CommandContext(ctx, "git", append([]string{"-C", g.Dir}, args...)...) |
| 75 | - cmd.Env = append([]string{"GIT_CONFIG_NOSYSTEM=1", "HOME=/nonexistent", "LC_ALL=C.UTF-8"}, env...) | |
| 80 | + cmd.Env = append([]string{"GIT_CONFIG_NOSYSTEM=1", "HOME=/nonexistent", "LC_ALL=C.UTF-8", "PATH=" + os.Getenv("PATH")}, env...) | |
| 81 | + if signersFile != "" { | |
| 82 | + cmd.Env = append(cmd.Env, "GIT_CONFIG_COUNT=1", "GIT_CONFIG_KEY_0=gpg.ssh.allowedSignersFile", "GIT_CONFIG_VALUE_0="+signersFile) | |
| 83 | + } | |
| 76 | 84 | if stdin != nil { |
| 77 | 85 | cmd.Stdin = bytes.NewReader(stdin) |
| 78 | 86 | } |
| @@ -190,19 +198,19 @@ func (g Git) Resolve(ref string) (string, error) { | ||
| 190 | 198 | return strings.TrimSpace(string(out)), nil |
| 191 | 199 | } |
| 192 | 200 | |
| 193 | -const logFormat = "%H%x00%an%x00%ae%x00%at%x00%P%x00%s%x00%b%x00" | |
| 201 | +const logFormat = "%H%x00%an%x00%ae%x00%at%x00%P%x00%s%x00%b%x00%G?%x00%GK%x00" | |
| 194 | 202 | |
| 195 | 203 | func parseCommits(out []byte) []Commit { |
| 196 | 204 | fields := strings.Split(string(out), "\x00") |
| 197 | 205 | var commits []Commit |
| 198 | - for len(fields) >= 7 { | |
| 206 | + for len(fields) >= 9 { | |
| 199 | 207 | t, _ := strconv.ParseInt(fields[3], 10, 64) |
| 200 | - c := Commit{Hash: fields[0], Author: fields[1], Email: fields[2], Time: time.Unix(t, 0), Subject: fields[5], Body: strings.TrimSpace(fields[6])} | |
| 208 | + c := Commit{Hash: fields[0], Author: fields[1], Email: fields[2], Time: time.Unix(t, 0), Subject: fields[5], Body: strings.TrimSpace(fields[6]), Signature: fields[7], Key: fields[8]} | |
| 201 | 209 | if fields[4] != "" { |
| 202 | 210 | c.Parents = strings.Split(fields[4], " ") |
| 203 | 211 | } |
| 204 | 212 | commits = append(commits, c) |
| 205 | - fields = fields[7:] | |
| 213 | + fields = fields[9:] | |
| 206 | 214 | if len(fields) > 0 { |
| 207 | 215 | fields[0] = strings.TrimLeft(fields[0], "\n") |
| 208 | 216 | } |
static/fonts/mono.txt deleted
| @@ -1,93 +0,0 @@ | ||
| 1 | -Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) | |
| 2 | - | |
| 3 | -This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | -This license is copied below, and is also available with a FAQ at: | |
| 5 | -https://scripts.sil.org/OFL | |
| 6 | - | |
| 7 | - | |
| 8 | ------------------------------------------------------------ | |
| 9 | -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 10 | ------------------------------------------------------------ | |
| 11 | - | |
| 12 | -PREAMBLE | |
| 13 | -The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 14 | -development of collaborative font projects, to support the font creation | |
| 15 | -efforts of academic and linguistic communities, and to provide a free and | |
| 16 | -open framework in which fonts may be shared and improved in partnership | |
| 17 | -with others. | |
| 18 | - | |
| 19 | -The OFL allows the licensed fonts to be used, studied, modified and | |
| 20 | -redistributed freely as long as they are not sold by themselves. The | |
| 21 | -fonts, including any derivative works, can be bundled, embedded, | |
| 22 | -redistributed and/or sold with any software provided that any reserved | |
| 23 | -names are not used by derivative works. The fonts and derivatives, | |
| 24 | -however, cannot be released under any other type of license. The | |
| 25 | -requirement for fonts to remain under this license does not apply | |
| 26 | -to any document created using the fonts or their derivatives. | |
| 27 | - | |
| 28 | -DEFINITIONS | |
| 29 | -"Font Software" refers to the set of files released by the Copyright | |
| 30 | -Holder(s) under this license and clearly marked as such. This may | |
| 31 | -include source files, build scripts and documentation. | |
| 32 | - | |
| 33 | -"Reserved Font Name" refers to any names specified as such after the | |
| 34 | -copyright statement(s). | |
| 35 | - | |
| 36 | -"Original Version" refers to the collection of Font Software components as | |
| 37 | -distributed by the Copyright Holder(s). | |
| 38 | - | |
| 39 | -"Modified Version" refers to any derivative made by adding to, deleting, | |
| 40 | -or substituting -- in part or in whole -- any of the components of the | |
| 41 | -Original Version, by changing formats or by porting the Font Software to a | |
| 42 | -new environment. | |
| 43 | - | |
| 44 | -"Author" refers to any designer, engineer, programmer, technical | |
| 45 | -writer or other person who contributed to the Font Software. | |
| 46 | - | |
| 47 | -PERMISSION & CONDITIONS | |
| 48 | -Permission is hereby granted, free of charge, to any person obtaining | |
| 49 | -a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 50 | -redistribute, and sell modified and unmodified copies of the Font | |
| 51 | -Software, subject to the following conditions: | |
| 52 | - | |
| 53 | -1) Neither the Font Software nor any of its individual components, | |
| 54 | -in Original or Modified Versions, may be sold by itself. | |
| 55 | - | |
| 56 | -2) Original or Modified Versions of the Font Software may be bundled, | |
| 57 | -redistributed and/or sold with any software, provided that each copy | |
| 58 | -contains the above copyright notice and this license. These can be | |
| 59 | -included either as stand-alone text files, human-readable headers or | |
| 60 | -in the appropriate machine-readable metadata fields within text or | |
| 61 | -binary files as long as those fields can be easily viewed by the user. | |
| 62 | - | |
| 63 | -3) No Modified Version of the Font Software may use the Reserved Font | |
| 64 | -Name(s) unless explicit written permission is granted by the corresponding | |
| 65 | -Copyright Holder. This restriction only applies to the primary font name as | |
| 66 | -presented to the users. | |
| 67 | - | |
| 68 | -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 69 | -Software shall not be used to promote, endorse or advertise any | |
| 70 | -Modified Version, except to acknowledge the contribution(s) of the | |
| 71 | -Copyright Holder(s) and the Author(s) or with their explicit written | |
| 72 | -permission. | |
| 73 | - | |
| 74 | -5) The Font Software, modified or unmodified, in part or in whole, | |
| 75 | -must be distributed entirely under this license, and must not be | |
| 76 | -distributed under any other license. The requirement for fonts to | |
| 77 | -remain under this license does not apply to any document created | |
| 78 | -using the Font Software. | |
| 79 | - | |
| 80 | -TERMINATION | |
| 81 | -This license becomes null and void if any of the above conditions are | |
| 82 | -not met. | |
| 83 | - | |
| 84 | -DISCLAIMER | |
| 85 | -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 86 | -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 87 | -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 88 | -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 89 | -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 90 | -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 91 | -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 92 | -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 93 | -OTHER DEALINGS IN THE FONT SOFTWARE. | |
static/fonts/mono.woff2 deleted
Binary file.
static/style.css modified
| @@ -1,11 +1,10 @@ | ||
| 1 | -@font-face{font-family:Mono;font-style:normal;font-weight:400;font-display:swap;src:url(/static/fonts/mono.woff2) format('woff2')} | |
| 2 | 1 | :root{--fg:#212529;--muted:#6c757d;--line:#dee2e6;--soft:#f8f9fa;--bar:#ddd;--link:#0640e0;--primary:#0640e0;--add:#1a7f37;--del:#c0392b;--addbg:#e6ffec;--delbg:#ffebe9;--code:#f8f9fa} |
| 3 | 2 | *{box-sizing:border-box} |
| 4 | 3 | html{font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","DejaVu Sans",Arial,sans-serif;background:#fff;color:var(--fg)} |
| 5 | 4 | body{margin:0} |
| 6 | 5 | a{color:var(--link);text-decoration:none} |
| 7 | 6 | a:hover{text-decoration:underline} |
| 8 | -code,pre,.hash,.mode,.blob,.hunk,.code,textarea{font-family:Mono,ui-monospace,Menlo,Consolas,monospace;font-size:13px} | |
| 7 | +code,pre,.hash,.mode,.blob,.hunk,.code,textarea{font-family:"DejaVu Sans Mono",Menlo,Consolas,"Liberation Mono","SF Mono",monospace;font-size:13px} | |
| 9 | 8 | pre{background:var(--code);padding:.6rem .8rem;overflow-x:auto;border:1px solid var(--line)} |
| 10 | 9 | pre.plain{background:none;border:0;padding:0} |
| 11 | 10 | h1{font-size:1.6rem;font-weight:500;margin:0 0 .2rem} |
| @@ -19,7 +18,7 @@ h4 a{color:var(--fg)} | ||
| 19 | 18 | .tag{font-size:.75rem;border:1px solid var(--line);padding:0 .3rem;color:var(--muted);vertical-align:middle} |
| 20 | 19 | .error{color:var(--del);border:1px solid var(--del);padding:.4rem .6rem;background:#fff} |
| 21 | 20 | .notice{border:1px solid var(--add);padding:.4rem .6rem;background:var(--addbg)} |
| 22 | -.inner,nav.site,nav.subtabs,main{max-width:1140px;margin:0 auto;padding-left:1rem;padding-right:1rem} | |
| 21 | +.inner,nav.site,nav.subtabs,main{max-width:1140px;margin:0;padding-left:1rem;padding-right:1rem} | |
| 23 | 22 | header{margin-bottom:1rem} |
| 24 | 23 | nav.site{display:flex;gap:1rem;align-items:center;padding-top:.5rem;padding-bottom:.5rem;flex-wrap:wrap} |
| 25 | 24 | nav.site .brand{font-size:1.25rem;color:var(--fg);margin-right:.5rem} |
| @@ -30,7 +29,7 @@ nav.site .spacer{flex:1} | ||
| 30 | 29 | nav.site form{display:inline} |
| 31 | 30 | nav.site button{background:none;border:0;color:var(--muted);cursor:pointer;font:inherit;padding:0} |
| 32 | 31 | nav.subtabs{display:flex;gap:0;padding-top:.2rem;border-bottom:1px solid var(--bar);max-width:none} |
| 33 | -nav.subtabs a:first-child{margin-left:max(1rem,calc((100% - 1140px)/2 + 1rem))} | |
| 32 | +nav.subtabs a:first-child{margin-left:1rem} | |
| 34 | 33 | nav.subtabs a,nav.tabs a{color:var(--muted);padding:.3rem .9rem} |
| 35 | 34 | nav.subtabs a.active{background:#e9ecef;color:var(--fg)} |
| 36 | 35 | nav.subtabs a:hover,nav.tabs a:hover{text-decoration:none;color:var(--fg)} |
| @@ -42,7 +41,7 @@ nav.tabs a{padding:.25rem .9rem} | ||
| 42 | 41 | nav.tabs a.active{background:#fff;color:var(--fg)} |
| 43 | 42 | .repohead .desc{margin:0 0 .4rem;color:var(--muted)} |
| 44 | 43 | .repohead+main{margin-top:1rem} |
| 45 | -.repohead+main.file{margin-top:0;padding-top:1.3rem} | |
| 44 | +.repohead+main.file{margin-top:0;padding-top:.6rem} | |
| 46 | 45 | .summary{margin-top:.2rem;display:grid;grid-template-columns:4fr 1.5fr 2.6fr;gap:2rem;margin-bottom:1.5rem;align-items:start} |
| 47 | 46 | .summary .event{background:var(--soft);padding:.4rem .5rem .5rem;margin-bottom:.6rem} |
| 48 | 47 | .summary .event .row{display:flex;justify-content:space-between;gap:1rem} |
| @@ -102,7 +101,7 @@ dl.meta dd{margin:0;overflow-wrap:anywhere} | ||
| 102 | 101 | .log .event{padding:.5rem 0;border-bottom:1px solid var(--line)} |
| 103 | 102 | .event .subject{color:var(--fg)} |
| 104 | 103 | .event pre.body{margin:.4rem 0 0} |
| 105 | -.filebar{display:flex;gap:1.2rem;align-items:baseline;border-bottom:1px solid var(--bar);padding:0 1rem .25rem;margin:-1rem -1rem .6rem;flex-wrap:wrap} | |
| 104 | +.filebar{display:flex;gap:1.2rem;align-items:baseline;border-bottom:1px solid var(--bar);padding:0 1rem .25rem;margin:-.6rem -1rem 0;flex-wrap:wrap} | |
| 106 | 105 | .filebar .spacer{flex:1} |
| 107 | 106 | .filebar a.active{color:var(--fg);border-bottom:2px solid var(--fg);padding-bottom:.2rem} |
| 108 | 107 | .filebar a{color:var(--muted)} |
| @@ -126,7 +125,7 @@ table.tree .size{color:var(--muted);text-align:right} | ||
| 126 | 125 | table.tree .last{max-width:30rem;overflow:hidden;text-overflow:ellipsis;width:100%} |
| 127 | 126 | table.tree .when{text-align:right} |
| 128 | 127 | table.refs td,table.keys td{padding:.3rem 1rem .3rem 0;border-bottom:1px solid var(--line)} |
| 129 | -.code{margin:0 -1rem} | |
| 128 | +.code{margin:0 -1rem;line-height:1.45} | |
| 130 | 129 | table.blob,.code table{width:100%} |
| 131 | 130 | table.blob td,table.hunk td,.code td{padding:0 .5rem;white-space:pre;vertical-align:top} |
| 132 | 131 | td.n{text-align:right;color:var(--muted);user-select:none;width:1%} |
| @@ -136,7 +135,10 @@ td.l{white-space:pre-wrap;overflow-wrap:anywhere;width:100%} | ||
| 136 | 135 | tr:target td{background:#fff8c5} |
| 137 | 136 | .code pre,.code .chroma{background:#fff;border:0;padding:0;margin:0} |
| 138 | 137 | .code .lntable{margin-top:0} |
| 139 | -.code .lntd:first-child{width:1%;user-select:none;border-right:1px solid var(--bar)} | |
| 138 | +.code .lntd:first-child{width:1%;user-select:none;border-right:1px solid var(--bar);padding-top:.4rem} | |
| 139 | +.code .lntd:last-child{padding-top:.4rem} | |
| 140 | +.code .lnlinks{text-decoration:none} | |
| 141 | +.code .lnlinks:hover{text-decoration:underline} | |
| 140 | 142 | .code .lnt,.code .ln{color:var(--link);margin-right:0;padding:0 .5rem 0 .6rem} |
| 141 | 143 | .code .lntd a{color:var(--link);text-decoration:underline} |
| 142 | 144 | .code .lntd:last-child{padding-left:.6rem} |
| @@ -208,3 +210,5 @@ select{font:inherit;padding:.35rem .5rem;border:1px solid #ced4da;background:#ff | ||
| 208 | 210 | .mergeform input,.mergeform textarea{width:100%} |
| 209 | 211 | .mergeform .actions{gap:.4rem} |
| 210 | 212 | .panel a.btn.block{margin-top:.4rem} |
| 213 | +.verified{display:inline-block;font-size:.72rem;border:1px solid #1a7f37;color:#1a7f37;border-radius:3px;padding:0 .3rem;vertical-align:middle;margin-left:.3rem} | |
| 214 | +.verified.unknown{border-color:var(--muted);color:var(--muted)} | |
store.go modified
| @@ -336,6 +336,32 @@ func (s *Store) DeleteKey(user, id int64) error { | ||
| 336 | 336 | return err |
| 337 | 337 | } |
| 338 | 338 | |
| 339 | +func (s *Store) UserByFingerprint(fp string) (User, bool) { | |
| 340 | + var user int64 | |
| 341 | + if err := s.db.QueryRow(`select user from keys where fingerprint = ?`, fp).Scan(&user); err != nil { | |
| 342 | + return User{}, false | |
| 343 | + } | |
| 344 | + u, err := s.userByID(user) | |
| 345 | + return u, err == nil | |
| 346 | +} | |
| 347 | + | |
| 348 | +func (s *Store) AllKeys() ([]string, error) { | |
| 349 | + rows, err := s.db.Query(`select public from keys order by id`) | |
| 350 | + if err != nil { | |
| 351 | + return nil, err | |
| 352 | + } | |
| 353 | + defer rows.Close() | |
| 354 | + var keys []string | |
| 355 | + for rows.Next() { | |
| 356 | + var k string | |
| 357 | + if err := rows.Scan(&k); err != nil { | |
| 358 | + return nil, err | |
| 359 | + } | |
| 360 | + keys = append(keys, k) | |
| 361 | + } | |
| 362 | + return keys, rows.Err() | |
| 363 | +} | |
| 364 | + | |
| 339 | 365 | func (s *Store) UserByKey(public string) (User, error) { |
| 340 | 366 | pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) |
| 341 | 367 | if err != nil { |
templates/commit.html modified
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | <dt>commit</dt><dd><code>{{$c.Hash}}</code> <a href="/{{$p.Repo.Path}}/tree/{{$c.Hash}}/">browse tree</a></dd> |
| 8 | 8 | {{range $c.Parents}}<dt>parent</dt><dd><a href="/{{$p.Repo.Path}}/commit/{{.}}"><code>{{.}}</code></a></dd>{{end}} |
| 9 | 9 | <dt>author</dt><dd>{{$c.Author}} <{{$c.Email}}></dd> |
| 10 | +<dt>signature</dt><dd>{{with signer $c}}<span class="verified">Verified ~{{.}}</span> signed with a key registered by <a href="/~{{.}}">~{{.}}</a>{{else}}{{if and $c.Signature (ne $c.Signature "N")}}<span class="verified unknown">Unverified</span> signed with an unregistered key{{else}}none{{end}}{{end}}</dd> | |
| 10 | 11 | <dt>date</dt><dd>{{date $c.Time}} UTC</dd> |
| 11 | 12 | </dl> |
| 12 | 13 | {{if $c.Body}}<pre class="body">{{$c.Body}}</pre>{{end}} |
templates/log.html modified
| @@ -2,7 +2,8 @@ | ||
| 2 | 2 | {{$p := .Data.Page}} |
| 3 | 3 | {{if .Data.Path}}<p class="muted">History of <a href="/{{$p.Repo.Path}}/tree/{{$p.Ref}}/{{.Data.Path}}">{{.Data.Path}}</a> on {{$p.Ref}}</p>{{end}} |
| 4 | 4 | {{if $p.Empty}}<p class="muted">No commits.</p>{{else}} |
| 5 | -<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a></div><div class="muted small">{{.Author}} · <span title="{{date .Time}}">{{ago .Time}}</span></div>{{if .Body}}<pre class="body">{{.Body}}</pre>{{end}}</div>{{end}}</div> | |
| 5 | +<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a>{{template "badge" .}}</div><div class="muted small">{{.Author}} · <span title="{{date .Time}}">{{ago .Time}}</span></div>{{if .Body}}<pre class="body">{{.Body}}</pre>{{end}}</div>{{end}}</div> | |
| 6 | 6 | {{with .Data.Next}}<p><a class="btn" href="?from={{.}}{{if $.Data.Path}}&path={{$.Data.Path}}{{end}}">Older ▸</a></p>{{end}} |
| 7 | 7 | {{end}} |
| 8 | 8 | {{end}} |
| 9 | +{{define "badge"}}{{with signer .}}<span class="verified" title="Signed with a key registered by ~{{.}}">Verified ~{{.}}</span>{{else}}{{if and .Signature (ne .Signature "N")}}<span class="verified unknown" title="Signed with an unregistered key">Unverified</span>{{end}}{{end}}{{end}} | |
templates/pull.html modified
| @@ -36,7 +36,7 @@ | ||
| 36 | 36 | </div> |
| 37 | 37 | {{if .Data.Commits}} |
| 38 | 38 | <h3>Commits</h3> |
| 39 | -<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a></div><div class="muted small">{{.Author}} · {{ago .Time}}</div></div>{{end}}</div> | |
| 39 | +<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a>{{with signer .}}<span class="verified" title="Signed with a key registered by ~{{.}}">Verified ~{{.}}</span>{{end}}</div><div class="muted small">{{.Author}} · {{ago .Time}}</div></div>{{end}}</div> | |
| 40 | 40 | {{end}} |
| 41 | 41 | {{if .Data.Files}} |
| 42 | 42 | <h3>Changes <span class="muted">{{len .Data.Files}} files, <span class="add">+{{.Data.Added}}</span> <span class="del">−{{.Data.Removed}}</span></span></h3> |
templates/repo.html modified
| @@ -7,7 +7,7 @@ git push -u origin {{$p.Ref}}</pre> | ||
| 7 | 7 | {{else}} |
| 8 | 8 | <div class="summary"> |
| 9 | 9 | <div class="commits"> |
| 10 | -{{range .Data.Commits}}<div class="event"><div class="row"><span><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> — {{.Author}}</span><a class="when" href="/{{$p.Repo.Path}}/commit/{{.Hash}}" title="{{date .Time}}">{{ago .Time}}</a></div><pre class="subject">{{.Subject}}</pre></div>{{end}} | |
| 10 | +{{range .Data.Commits}}<div class="event"><div class="row"><span><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> — {{.Author}}{{with signer .}}<span class="verified" title="Signed with a key registered by ~{{.}}">Verified</span>{{end}}</span><a class="when" href="/{{$p.Repo.Path}}/commit/{{.Hash}}" title="{{date .Time}}">{{ago .Time}}</a></div><pre class="subject">{{.Subject}}</pre></div>{{end}} | |
| 11 | 11 | </div> |
| 12 | 12 | <div class="refs"> |
| 13 | 13 | <h3>refs</h3> |
web.go modified
| @@ -70,6 +70,7 @@ func newServer(store *Store, root, host string, open bool) *Server { | ||
| 70 | 70 | }, |
| 71 | 71 | "inc": func(i int) int { return i + 1 }, |
| 72 | 72 | "perm": perm, |
| 73 | + "signer": s.signer, | |
| 73 | 74 | "join": func(parts []string, upto int) string { return strings.Join(parts[:upto], "/") }, |
| 74 | 75 | "string": func(b []byte) string { return string(b) }, |
| 75 | 76 | } |
| @@ -85,6 +86,7 @@ func newServer(store *Store, root, host string, open bool) *Server { | ||
| 85 | 86 | highlighting.NewHighlighting(highlighting.WithFormatOptions(chromahtml.WithClasses(true))))) |
| 86 | 87 | s.policy = bluemonday.UGCPolicy() |
| 87 | 88 | s.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^[a-z]{1,8}( [a-z]{1,8})*$`)).OnElements("span", "pre", "code") |
| 89 | + s.writeSigners() | |
| 88 | 90 | return s |
| 89 | 91 | } |
| 90 | 92 | |
| @@ -287,6 +289,33 @@ func (s *Server) create(w http.ResponseWriter, r *http.Request) { | ||
| 287 | 289 | http.Redirect(w, r, "/"+repo.Path(), http.StatusSeeOther) |
| 288 | 290 | } |
| 289 | 291 | |
| 292 | +func (s *Server) signer(c Commit) string { | |
| 293 | + if c.Signature != "G" || c.Key == "" { | |
| 294 | + return "" | |
| 295 | + } | |
| 296 | + if u, ok := s.store.UserByFingerprint(c.Key); ok { | |
| 297 | + return u.Name | |
| 298 | + } | |
| 299 | + return "" | |
| 300 | +} | |
| 301 | + | |
| 302 | +func (s *Server) writeSigners() { | |
| 303 | + keys, err := s.store.AllKeys() | |
| 304 | + if err != nil { | |
| 305 | + return | |
| 306 | + } | |
| 307 | + var b strings.Builder | |
| 308 | + for _, k := range keys { | |
| 309 | + b.WriteString("* namespaces=\"git\" " + k + "\n") | |
| 310 | + } | |
| 311 | + path := filepath.Join(s.root, "signers") | |
| 312 | + if err := os.WriteFile(path+".tmp", []byte(b.String()), 0o644); err == nil { | |
| 313 | + if os.Rename(path+".tmp", path) == nil { | |
| 314 | + signersFile = path | |
| 315 | + } | |
| 316 | + } | |
| 317 | +} | |
| 318 | + | |
| 290 | 319 | func visibility(v string) int { |
| 291 | 320 | switch v { |
| 292 | 321 | case "private": |
| @@ -474,6 +503,7 @@ func (s *Server) addKey(w http.ResponseWriter, r *http.Request) { | ||
| 474 | 503 | http.Redirect(w, r, "/settings/keys?error="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther) |
| 475 | 504 | return |
| 476 | 505 | } |
| 506 | + s.writeSigners() | |
| 477 | 507 | http.Redirect(w, r, "/settings/keys", http.StatusSeeOther) |
| 478 | 508 | } |
| 479 | 509 | |
| @@ -484,6 +514,7 @@ func (s *Server) deleteKey(w http.ResponseWriter, r *http.Request) { | ||
| 484 | 514 | } |
| 485 | 515 | id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64) |
| 486 | 516 | s.store.DeleteKey(u.ID, id) |
| 517 | + s.writeSigners() | |
| 487 | 518 | http.Redirect(w, r, "/settings/keys", http.StatusSeeOther) |
| 488 | 519 | } |
| 489 | 520 | |