repos / git-pr

a self-hosted git collaboration server
git clone https://github.com/picosh/git-pr.git

commit
6aa7aac
parent
73b7bbe
author
Eric Bower
date
2024-07-10 13:37:51 -0400 EDT
feat: config via toml
13 files changed,  +231, -151
M cfg.go
M go.mod
M go.sum
M ssh.go
M web.go
M .env.example
+11, -11
 1@@ -1,13 +1,13 @@
 2 CF_API_TOKEN=
 3 
 4-GIT_V4=
 5-GIT_V6=
 6-GIT_HTTP_V4=$GIT_V4:80
 7-GIT_HTTP_V6=[$GIT_V6]:80
 8-GIT_HTTPS_V4=$GIT_V4:443
 9-GIT_HTTPS_V6=[$GIT_V6]:443
10-GIT_SSH_V4=$GIT_V4:22
11-GIT_SSH_V6=[$GIT_V6]:22
12-GIT_HOST=
13-GIT_SSH_PORT=2222
14-GIT_WEB_PORT=3000
15+GITPR_V4=
16+GITPR_V6=
17+GITPR_HTTP_V4=$GIT_V4:80
18+GITPR_HTTP_V6=[$GIT_V6]:80
19+GITPR_HTTPS_V4=$GIT_V4:443
20+GITPR_HTTPS_V6=[$GIT_V6]:443
21+GITPR_SSH_V4=$GIT_V4:22
22+GITPR_SSH_V6=[$GIT_V6]:22
23+GITPR_HOST=
24+GITPR_SSH_PORT=2222
25+GITPR_WEB_PORT=3000
M Dockerfile
+2, -3
 1@@ -45,7 +45,7 @@ WORKDIR /app
 2 COPY --from=builder-web /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
 3 COPY --from=builder-web /go/bin/web ./web
 4 
 5-ENTRYPOINT ["/app/web"]
 6+ENTRYPOINT ["/app/web", "--config", "${GITPR_CONFIG_PATH}"]
 7 
 8 FROM scratch as release-ssh
 9 
10@@ -55,5 +55,4 @@ ENV TERM="xterm-256color"
11 COPY --from=builder-ssh /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
12 COPY --from=builder-ssh /go/bin/ssh ./ssh
13 
14-
15-ENTRYPOINT ["/app/ssh"]
16+ENTRYPOINT ["/app/ssh", "--config", "${GITPR_CONFIG_PATH}"]
M cfg.go
+96, -36
  1@@ -1,58 +1,118 @@
  2 package git
  3 
  4 import (
  5-	"os"
  6+	"fmt"
  7+	"log/slog"
  8+	"strings"
  9 
 10 	"github.com/charmbracelet/ssh"
 11+	"github.com/knadh/koanf/parsers/toml"
 12+	"github.com/knadh/koanf/providers/env"
 13+	"github.com/knadh/koanf/providers/file"
 14+	"github.com/knadh/koanf/v2"
 15 )
 16 
 17 type Repo struct {
 18-	ID            string
 19-	Desc          string
 20-	CloneAddr     string
 21-	DefaultBranch string
 22+	ID            string `koanf:"id"`
 23+	Desc          string `koanf:"desc"`
 24+	CloneAddr     string `koanf:"clone_addr"`
 25+	DefaultBranch string `koanf:"default_branch"`
 26 }
 27 
 28-func NewRepo(id, cloneAddr string) *Repo {
 29-	return &Repo{
 30-		ID:            id,
 31-		CloneAddr:     cloneAddr,
 32-		DefaultBranch: "main",
 33-	}
 34-}
 35+var k = koanf.New(".")
 36 
 37 type GitCfg struct {
 38-	DataPath string
 39-	Admins   []ssh.PublicKey
 40-	Repos    []*Repo
 41-	Url      string
 42-	Host     string
 43-	SshPort  string
 44-	WebPort  string
 45+	DataPath  string          `koanf:"data"`
 46+	Repos     []*Repo         `koanf:"repo"`
 47+	Url       string          `koanf:"url"`
 48+	Host      string          `koanf:"host"`
 49+	SshPort   string          `koanf:"ssh_port"`
 50+	WebPort   string          `koanf:"web_port"`
 51+	AdminsStr []string        `koanf:"admins"`
 52+	Admins    []ssh.PublicKey `koanf:"admins_pk"`
 53+	Theme     string          `koanf:"theme"`
 54+	Logger    *slog.Logger
 55 }
 56 
 57-func NewGitCfg(dataPath, url string, repos []*Repo) *GitCfg {
 58-	host := os.Getenv("GIT_HOST")
 59-	if host == "" {
 60-		host = "0.0.0.0"
 61+func NewGitCfg(fpath string, logger *slog.Logger) *GitCfg {
 62+	logger.Info("loading configuration file", "fpath", fpath)
 63+
 64+	if err := k.Load(file.Provider(fpath), toml.Parser()); err != nil {
 65+		panic(fmt.Sprintf("error loading config: %v", err))
 66+	}
 67+
 68+	err := k.Load(env.Provider("GITPR_", ".", func(s string) string {
 69+		keyword := strings.ToLower(strings.TrimPrefix(s, "GITPR_"))
 70+		return keyword
 71+	}), nil)
 72+	if err != nil {
 73+		panic(fmt.Sprintf("could not load environment variables: %v", err))
 74 	}
 75 
 76-	sshPort := os.Getenv("GIT_SSH_PORT")
 77-	if sshPort == "" {
 78-		sshPort = "2222"
 79+	var out GitCfg
 80+	err = k.UnmarshalWithConf("", &out, koanf.UnmarshalConf{Tag: "koanf"})
 81+	if err != nil {
 82+		panic(fmt.Sprintf("could not unmarshal config: %v", err))
 83 	}
 84 
 85-	webPort := os.Getenv("GIT_WEB_PORT")
 86-	if webPort == "" {
 87-		webPort = "3000"
 88+	if len(out.AdminsStr) > 0 {
 89+		keys, err := getAuthorizedKeys(out.AdminsStr)
 90+		if err == nil {
 91+			out.Admins = keys
 92+		} else {
 93+			panic(fmt.Sprintf("could not parse authorized keys file: %v", err))
 94+		}
 95+	} else {
 96+		logger.Info("no admin specified in config so no one can submit a review!")
 97 	}
 98 
 99-	return &GitCfg{
100-		DataPath: dataPath,
101-		Url:      url,
102-		Repos:    repos,
103-		Host:     host,
104-		SshPort:  sshPort,
105-		WebPort:  webPort,
106+	if out.DataPath == "" {
107+		out.DataPath = "data"
108 	}
109+
110+	if out.Host == "" {
111+		out.Host = "0.0.0.0"
112+	}
113+
114+	if out.SshPort == "" {
115+		out.SshPort = "2222"
116+	}
117+
118+	if out.WebPort == "" {
119+		out.WebPort = "3000"
120+	}
121+
122+	if out.Theme == "" {
123+		out.Theme = "dracula"
124+	}
125+
126+	logger.Info(
127+		"config",
128+		"url", out.Url,
129+		"data", out.DataPath,
130+		"host", out.Host,
131+		"ssh_port", out.SshPort,
132+		"web_port", out.WebPort,
133+		"theme", out.Theme,
134+	)
135+
136+	for _, pubkey := range out.AdminsStr {
137+		logger.Info("admin", "pubkey", pubkey)
138+	}
139+
140+	for _, repo := range out.Repos {
141+		if repo.DefaultBranch == "" {
142+			repo.DefaultBranch = "main"
143+		}
144+		logger.Info(
145+			"repo",
146+			"id", repo.ID,
147+			"desc", repo.Desc,
148+			"clone_addr", repo.CloneAddr,
149+			"default_branch", repo.DefaultBranch,
150+		)
151+	}
152+
153+	out.Logger = logger
154+	return &out
155 }
D cmd/cfg.go
+0, -32
 1@@ -1,32 +0,0 @@
 2-package cmd
 3-
 4-import "github.com/picosh/git-pr"
 5-
 6-func NewPicoCfg() *git.GitCfg {
 7-	test := git.NewRepo("test", "git@github.com:picosh/test")
 8-	test.Desc = "A test repo to play around with Patch Requests"
 9-
10-	pico := git.NewRepo("pico", "git@github.com:picosh/pico")
11-	pico.Desc = "hacker labs - open and managed web services leveraging ssh"
12-
13-	pr := git.NewRepo("git-pr", "git@github.com:picosh/git-pr")
14-	pr.Desc = "the easiest git collaboration tool"
15-
16-	ptun := git.NewRepo("ptun", "git@github.com:picosh/ptun")
17-	ptun.Desc = "passwordless authentication for the web"
18-
19-	pobj := git.NewRepo("pobj", "git@github.com:picosh/pobj")
20-	pobj.Desc = "rsync, scp, sftp for your object store"
21-
22-	send := git.NewRepo("send", "git@github.com:picosh/send")
23-	send.Desc = "ssh wish middleware for sending and receiving files from familiar tools (rsync, scp, sftp)"
24-
25-	docs := git.NewRepo("docs", "git@github.com:picosh/docs")
26-	docs.Desc = "pico.sh doc site"
27-
28-	return git.NewGitCfg(
29-		"ssh_data",
30-		"pr.pico.sh",
31-		[]*git.Repo{test, pico, ptun, pobj, send, docs, pr},
32-	)
33-}
M cmd/ssh/main.go
+13, -2
 1@@ -1,10 +1,21 @@
 2 package main
 3 
 4 import (
 5+	"flag"
 6+	"log/slog"
 7+	"os"
 8+
 9 	git "github.com/picosh/git-pr"
10-	"github.com/picosh/git-pr/cmd"
11 )
12 
13 func main() {
14-	git.GitSshServer(cmd.NewPicoCfg())
15+	fpath := flag.String("config", "git-pr.toml", "configuration toml file")
16+	flag.Parse()
17+	opts := &slog.HandlerOptions{
18+		AddSource: true,
19+	}
20+	logger := slog.New(
21+		slog.NewTextHandler(os.Stdout, opts),
22+	)
23+	git.GitSshServer(git.NewGitCfg(*fpath, logger))
24 }
M cmd/web/main.go
+13, -2
 1@@ -1,10 +1,21 @@
 2 package main
 3 
 4 import (
 5+	"flag"
 6+	"log/slog"
 7+	"os"
 8+
 9 	git "github.com/picosh/git-pr"
10-	"github.com/picosh/git-pr/cmd"
11 )
12 
13 func main() {
14-	git.StartWebServer(cmd.NewPicoCfg())
15+	fpath := flag.String("config", "git-pr.toml", "configuration toml file")
16+	flag.Parse()
17+	opts := &slog.HandlerOptions{
18+		AddSource: true,
19+	}
20+	logger := slog.New(
21+		slog.NewTextHandler(os.Stdout, opts),
22+	)
23+	git.StartWebServer(git.NewGitCfg(*fpath, logger))
24 }
M docker-compose.yml
+2, -2
 1@@ -3,9 +3,9 @@ services:
 2     image: ghcr.io/picosh/pico/git-web:latest
 3     restart: always
 4     volumes:
 5-      - ./data/git-pr/data:/app/ssh_data
 6+      - ./data/git-pr/data:/app/data
 7   ssh:
 8     image: ghcr.io/picosh/pico/git-ssh:latest
 9     restart: always
10     volumes:
11-      - ./data/git-pr/data:/app/ssh_data
12+      - ./data/git-pr/data:/app/data
A git-pr.toml
+41, -0
 1@@ -0,0 +1,41 @@
 2+url = "pr.pico.sh"
 3+data = "ssh_data"
 4+admins = [
 5+  "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHM2RPNgcyt+Tpb77uj0oQYZWLadfB8x8mqJFy0C7Y8P",
 6+  "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINlr0pScstAuTqs9Qr1KaMspHuFGO7cQMuvMMdJjbWG3"
 7+]
 8+
 9+[[repo]]
10+id = "test"
11+clone_addr = "git@github.com:picosh/test"
12+desc = "test repo to play around with Patch Requests"
13+
14+[[repo]]
15+id = "pico"
16+clone_addr = "git@github.com:picosh/pico"
17+desc = "open and managed web services leveraging ssh"
18+
19+[[repo]]
20+id = "git-pr"
21+clone_addr = "git@github.com:picosh/git-pr"
22+desc = "the easiest git collaboration tool"
23+
24+[[repo]]
25+id = "tunkit"
26+clone_addr = "git@github.com:picosh/tunkit"
27+desc = "ssh tunnel tooling"
28+
29+[[repo]]
30+id = "pobj"
31+clone_addr = "git@github.com:picosh/pobj"
32+desc = "rsync, scp, sftp for your object store"
33+
34+[[repo]]
35+id = "send"
36+clone_addr = "git@github.com:picosh/send"
37+desc = "ssh wish middleware for sending and receiving files from familiar tools (rsync, scp, sftp)"
38+
39+[[repo]]
40+id = "docs"
41+clone_addr = "git@github.com:picosh/docs"
42+desc = "pico.sh doc site"
M go.mod
+11, -1
 1@@ -10,6 +10,10 @@ require (
 2 	github.com/charmbracelet/wish v1.3.2
 3 	github.com/gorilla/feeds v1.1.2
 4 	github.com/jmoiron/sqlx v1.3.5
 5+	github.com/knadh/koanf/parsers/toml v0.1.0
 6+	github.com/knadh/koanf/providers/env v0.1.0
 7+	github.com/knadh/koanf/providers/file v1.0.0
 8+	github.com/knadh/koanf/v2 v2.1.1
 9 	github.com/urfave/cli/v2 v2.27.2
10 	golang.org/x/crypto v0.21.0
11 	modernc.org/sqlite v1.27.0
12@@ -29,17 +33,23 @@ require (
13 	github.com/creack/pty v1.1.21 // indirect
14 	github.com/dlclark/regexp2 v1.11.0 // indirect
15 	github.com/dustin/go-humanize v1.0.1 // indirect
16+	github.com/fsnotify/fsnotify v1.7.0 // indirect
17 	github.com/go-logfmt/logfmt v0.6.0 // indirect
18+	github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
19 	github.com/google/uuid v1.4.0 // indirect
20 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
21+	github.com/knadh/koanf/maps v0.1.1 // indirect
22 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
23 	github.com/mattn/go-isatty v0.0.20 // indirect
24 	github.com/mattn/go-localereader v0.0.1 // indirect
25 	github.com/mattn/go-runewidth v0.0.15 // indirect
26+	github.com/mitchellh/copystructure v1.2.0 // indirect
27+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
28 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
29 	github.com/muesli/cancelreader v0.2.2 // indirect
30 	github.com/muesli/reflow v0.3.0 // indirect
31 	github.com/muesli/termenv v0.15.2 // indirect
32+	github.com/pelletier/go-toml v1.9.5 // indirect
33 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
34 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
35 	github.com/rivo/uniseg v0.4.7 // indirect
36@@ -48,7 +58,7 @@ require (
37 	golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
38 	golang.org/x/mod v0.14.0 // indirect
39 	golang.org/x/sync v0.6.0 // indirect
40-	golang.org/x/sys v0.18.0 // indirect
41+	golang.org/x/sys v0.21.0 // indirect
42 	golang.org/x/term v0.18.0 // indirect
43 	golang.org/x/text v0.14.0 // indirect
44 	golang.org/x/tools v0.15.0 // indirect
M go.sum
+22, -2
 1@@ -40,10 +40,14 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK
 2 github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 3 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 4 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 5+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 6+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 7 github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
 8 github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 9 github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
10 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
11+github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
12+github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
13 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
14 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
15 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
16@@ -58,6 +62,16 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
17 github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
18 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
19 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
20+github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
21+github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
22+github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI=
23+github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18=
24+github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
25+github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
26+github.com/knadh/koanf/providers/file v1.0.0 h1:DtPvSQBeF+N0QLPMz0yf2bx0nFSxUcncpqQvzCxfCyk=
27+github.com/knadh/koanf/providers/file v1.0.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
28+github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
29+github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
30 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
31 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
32 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
33@@ -77,6 +91,10 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
34 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
35 github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
36 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
37+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
38+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
39+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
40+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
41 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
42 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
43 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
44@@ -85,6 +103,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
45 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
46 github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
47 github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
48+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
49+github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
50 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
51 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
52 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
53@@ -113,8 +133,8 @@ golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
54 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
55 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
56 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
57-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
58-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
59+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
60+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
61 golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
62 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
63 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
M ssh.go
+8, -21
 1@@ -3,7 +3,6 @@ package git
 2 import (
 3 	"context"
 4 	"fmt"
 5-	"log/slog"
 6 	"os"
 7 	"os/signal"
 8 	"path/filepath"
 9@@ -33,27 +32,14 @@ func authHandler(pr *PrCmd) func(ctx ssh.Context, key ssh.PublicKey) bool {
10 }
11 
12 func GitSshServer(cfg *GitCfg) {
13-	opts := &slog.HandlerOptions{
14-		AddSource: true,
15-	}
16-	logger := slog.New(
17-		slog.NewTextHandler(os.Stdout, opts),
18-	)
19-	dbh, err := Open(filepath.Join(cfg.DataPath, "pr.db"), logger)
20+	dbh, err := Open(filepath.Join(cfg.DataPath, "pr.db"), cfg.Logger)
21 	if err != nil {
22 		panic(err)
23 	}
24 
25-	keys, err := getAuthorizedKeys(filepath.Join(cfg.DataPath, "authorized_keys"))
26-	if err == nil {
27-		cfg.Admins = keys
28-	} else {
29-		logger.Error("could not parse authorized keys file", "err", err)
30-	}
31-
32 	be := &Backend{
33 		DB:     dbh,
34-		Logger: logger,
35+		Logger: cfg.Logger,
36 		Cfg:    cfg,
37 	}
38 	prCmd := &PrCmd{
39@@ -74,25 +60,26 @@ func GitSshServer(cfg *GitCfg) {
40 	)
41 
42 	if err != nil {
43-		logger.Error("could not create server", "err", err)
44+		cfg.Logger.Error("could not create server", "err", err)
45+		return
46 	}
47 
48 	done := make(chan os.Signal, 1)
49 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
50-	logger.Info("starting SSH server", "host", cfg.Host, "port", cfg.SshPort)
51+	cfg.Logger.Info("starting SSH server", "host", cfg.Host, "port", cfg.SshPort)
52 	go func() {
53 		if err = s.ListenAndServe(); err != nil {
54-			logger.Error("serve error", "err", err)
55+			cfg.Logger.Error("serve error", "err", err)
56 			os.Exit(1)
57 		}
58 	}()
59 
60 	<-done
61-	logger.Info("stopping SSH server")
62+	cfg.Logger.Info("stopping SSH server")
63 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
64 	defer func() { cancel() }()
65 	if err := s.Shutdown(ctx); err != nil {
66-		logger.Error("shutdown", "err", err)
67+		cfg.Logger.Error("shutdown", "err", err)
68 		os.Exit(1)
69 	}
70 }
M util.go
+5, -24
 1@@ -1,13 +1,8 @@
 2 package git
 3 
 4 import (
 5-	"bufio"
 6-	"bytes"
 7-	"errors"
 8 	"fmt"
 9-	"io"
10 	"math/rand"
11-	"os"
12 	"strconv"
13 	"strings"
14 
15@@ -29,30 +24,16 @@ func truncateSha(sha string) string {
16 	return sha[:7]
17 }
18 
19-func getAuthorizedKeys(path string) ([]ssh.PublicKey, error) {
20+func getAuthorizedKeys(pubkeys []string) ([]ssh.PublicKey, error) {
21 	keys := []ssh.PublicKey{}
22-	f, err := os.Open(path)
23-	if err != nil {
24-		return keys, err
25-	}
26-	defer f.Close() // nolint: errcheck
27-
28-	rd := bufio.NewReader(f)
29-	for {
30-		line, _, err := rd.ReadLine()
31-		if err != nil {
32-			if errors.Is(err, io.EOF) {
33-				break
34-			}
35-			return keys, err
36-		}
37-		if strings.TrimSpace(string(line)) == "" {
38+	for _, pubkey := range pubkeys {
39+		if strings.TrimSpace(pubkey) == "" {
40 			continue
41 		}
42-		if bytes.HasPrefix(line, []byte{'#'}) {
43+		if strings.HasPrefix(pubkey, "#") {
44 			continue
45 		}
46-		upk, _, _, _, err := ssh.ParseAuthorizedKey(line)
47+		upk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey))
48 		if err != nil {
49 			return keys, err
50 		}
M web.go
+7, -15
 1@@ -484,17 +484,16 @@ func chromaStyleHandler(w http.ResponseWriter, r *http.Request) {
 2 
 3 func StartWebServer(cfg *GitCfg) {
 4 	addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
 5-	logger := slog.Default()
 6 
 7-	dbh, err := Open(filepath.Join(cfg.DataPath, "pr.db"), logger)
 8+	dbh, err := Open(filepath.Join(cfg.DataPath, "pr.db"), cfg.Logger)
 9 	if err != nil {
10-		logger.Error("could not open db", "err", err)
11+		cfg.Logger.Error("could not open db", "err", err)
12 		return
13 	}
14 
15 	be := &Backend{
16 		DB:     dbh,
17-		Logger: logger,
18+		Logger: cfg.Logger,
19 		Cfg:    cfg,
20 	}
21 	prCmd := &PrCmd{
22@@ -507,16 +506,9 @@ func StartWebServer(cfg *GitCfg) {
23 	web := &WebCtx{
24 		Pr:        prCmd,
25 		Backend:   be,
26-		Logger:    logger,
27+		Logger:    cfg.Logger,
28 		Formatter: formatter,
29-		Theme:     styles.Get("dracula"),
30-	}
31-
32-	keys, err := getAuthorizedKeys(filepath.Join(cfg.DataPath, "authorized_keys"))
33-	if err == nil {
34-		cfg.Admins = keys
35-	} else {
36-		logger.Error("could not parse authorized keys file", "err", err)
37+		Theme:     styles.Get(cfg.Theme),
38 	}
39 
40 	ctx := context.Background()
41@@ -532,9 +524,9 @@ func StartWebServer(cfg *GitCfg) {
42 	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
43 	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
44 
45-	logger.Info("starting web server", "addr", addr)
46+	cfg.Logger.Info("starting web server", "addr", addr)
47 	err = http.ListenAndServe(addr, nil)
48 	if err != nil {
49-		logger.Error("listen", "err", err)
50+		cfg.Logger.Error("listen", "err", err)
51 	}
52 }