dashboard / erock/git-pr / single binary #64 rss

open · opened on 2025-04-17T21:14:13Z by jolheiser
Help
checkout latest patchset:
ssh pr.pico.sh print pr-64 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 64
add review to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add --review 64
accept PR:
ssh pr.pico.sh pr accept 64
close PR:
ssh pr.pico.sh pr close 64

Logs

jolheiser created pr with ps-131 on 2025-04-17T21:14:13Z

Patchsets

ps-131 by jolheiser on 2025-04-17T21:14:13Z

Patchset ps-131

single binary

jolheiser
2025-04-17T21:07:03Z
Caddyfile
+1 -1
Dockerfile
+6 -29
Makefile
+3 -8
README.md
+3 -13
e2e_test.go
+7 -6
ssh.go
+3 -30
web.go
+15 -21
Back to top

single binary

Reduce web and ssh to a single binary called git-pr that runs both the SSH and web servers at the same time

Signed-off-by: jolheiser <git@jolheiser.com>
Caddyfile link
+1 -1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
diff --git a/Caddyfile b/Caddyfile
index ff096d2..8025377 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -48,6 +48,6 @@
 }
 
 :443 {
-	reverse_proxy git-web:3000
+	reverse_proxy git-pr:3000
 	encode zstd gzip
 }
Dockerfile link
+6 -29
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
diff --git a/Dockerfile b/Dockerfile
index aea6895..a542b06 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,7 +10,7 @@ COPY go.* ./
 
 RUN go mod download
 
-FROM builder-deps as builder-web
+FROM builder-deps as builder
 
 COPY . .
 
@@ -22,37 +22,14 @@ ENV LDFLAGS="-s -w"
 
 ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH}
 
-RUN go build -ldflags "$LDFLAGS" -o /go/bin/git-web ./cmd/git-web
+RUN go build -ldflags "$LDFLAGS" -o /go/bin/git-pr ./cmd/git-pr
 
-FROM builder-deps as builder-ssh
-
-COPY . .
-
-ARG TARGETOS
-ARG TARGETARCH
-
-ENV CGO_ENABLED=0
-ENV LDFLAGS="-s -w"
-
-ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH}
-
-RUN go build -ldflags "$LDFLAGS" -o /go/bin/git-ssh ./cmd/git-ssh
-
-FROM scratch as release-web
-
-WORKDIR /app
-
-COPY --from=builder-web /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
-COPY --from=builder-web /go/bin/git-web ./git-web
-
-CMD ["/app/git-web"]
-
-FROM scratch as release-ssh
+FROM scratch as release
 
 WORKDIR /app
 ENV TERM="xterm-256color"
 
-COPY --from=builder-ssh /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
-COPY --from=builder-ssh /go/bin/git-ssh ./git-ssh
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
+COPY --from=builder /go/bin/git-pr ./git-pr
 
-CMD ["/app/git-ssh"]
+CMD ["/app/git-pr"]
Makefile link
+3 -8
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
diff --git a/Makefile b/Makefile
index 80e7ae4..395c4bc 100644
--- a/Makefile
+++ b/Makefile
@@ -21,8 +21,7 @@ snapshot:
 .PHONY: snapshot
 
 build:
-	go build -o ./build/git-ssh ./cmd/git-ssh
-	go build -o ./build/git-web ./cmd/git-web
+	go build -o ./build/git-pr ./cmd/git-pr
 .PHONY: build
 
 bp-setup:
@@ -30,12 +29,8 @@ bp-setup:
 	$(DOCKER_CMD) buildx use pico
 .PHONY: bp-setup
 
-bp-web: bp-setup
-	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
-.PHONY: bp-web
-
-bp: bp-web
-	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
+bp: bp-setup
+	$(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-pr:$(DOCKER_TAG)" --target release-pr .
 .PHONY: bp
 
 deploy: bp-web
README.md link
+3 -13
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
diff --git a/README.md b/README.md
index 4d0e87c..7ef332d 100644
--- a/README.md
+++ b/README.md
@@ -159,16 +159,10 @@ vim ./data/git-pr.toml
 
 ## docker
 
-Run the ssh app image:
+Run the app image:
 
 ```bash
-docker run -d -v ./data:/app/data ghcr.io/picosh/pico/git-ssh:latest
-```
-
-Run the web app image:
-
-```bash
-docker run -d -v ./data:/app/data ghcr.io/picosh/pico/git-web:latest
+docker run -d -v ./data:/app/data ghcr.io/picosh/pico/git-pr:latest
 ```
 
 ## golang
@@ -180,11 +174,7 @@ make build
 ```
 
 ```bash
-./build/ssh --config ./data/git-pr.toml
-```
-
-```bash
-./build/web --config ./data/git-pr.toml
+./build/git-pr --config ./data/git-pr.toml
 ```
 
 ## done!
cmd/git-pr/main.go link
+57 -0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
diff --git a/cmd/git-pr/main.go b/cmd/git-pr/main.go
new file mode 100644
index 0000000..dca7dcb
--- /dev/null
+++ b/cmd/git-pr/main.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"log/slog"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	git "github.com/picosh/git-pr"
+)
+
+func main() {
+	fpath := flag.String("config", "git-pr.toml", "configuration toml file")
+	flag.Parse()
+	opts := &slog.HandlerOptions{
+		AddSource: true,
+	}
+	logger := slog.New(
+		slog.NewTextHandler(os.Stdout, opts),
+	)
+	git.LoadConfigFile(*fpath, logger)
+	cfg := git.NewGitCfg(logger)
+
+	// SSH Server
+	ssh := git.GitSshServer(cfg)
+	cfg.Logger.Info("starting SSH server", "host", cfg.Host, "port", cfg.SshPort)
+	go func() {
+		if err := ssh.ListenAndServe(); err != nil {
+			cfg.Logger.Error("serve error", "err", err)
+		}
+	}()
+
+	// Web Server
+	addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
+	web := git.GitWebServer(cfg)
+	cfg.Logger.Info("starting web server", "addr", addr)
+	go func() {
+		if err := http.ListenAndServe(addr, web); err != nil {
+			cfg.Logger.Error("listen", "err", err)
+		}
+	}()
+
+	done := make(chan os.Signal, 1)
+	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+	<-done
+	cfg.Logger.Info("stopping SSH server")
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer func() { cancel() }()
+	if err := ssh.Shutdown(ctx); err != nil {
+		cfg.Logger.Error("shutdown", "err", err)
+	}
+}
cmd/git-ssh/main.go link
+0 -22
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
diff --git a/cmd/git-ssh/main.go b/cmd/git-ssh/main.go
deleted file mode 100644
index 7d12c82..0000000
--- a/cmd/git-ssh/main.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package main
-
-import (
-	"flag"
-	"log/slog"
-	"os"
-
-	git "github.com/picosh/git-pr"
-)
-
-func main() {
-	fpath := flag.String("config", "git-pr.toml", "configuration toml file")
-	flag.Parse()
-	opts := &slog.HandlerOptions{
-		AddSource: true,
-	}
-	logger := slog.New(
-		slog.NewTextHandler(os.Stdout, opts),
-	)
-	git.LoadConfigFile(*fpath, logger)
-	git.GitSshServer(git.NewGitCfg(logger), nil)
-}
cmd/git-web/main.go link
+0 -22
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
diff --git a/cmd/git-web/main.go b/cmd/git-web/main.go
deleted file mode 100644
index 01749ef..0000000
--- a/cmd/git-web/main.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package main
-
-import (
-	"flag"
-	"log/slog"
-	"os"
-
-	git "github.com/picosh/git-pr"
-)
-
-func main() {
-	fpath := flag.String("config", "git-pr.toml", "configuration toml file")
-	flag.Parse()
-	opts := &slog.HandlerOptions{
-		AddSource: true,
-	}
-	logger := slog.New(
-		slog.NewTextHandler(os.Stdout, opts),
-	)
-	git.LoadConfigFile(*fpath, logger)
-	git.StartWebServer(git.NewGitCfg(logger))
-}
contrib/dev/main.go link
+6 -2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
diff --git a/contrib/dev/main.go b/contrib/dev/main.go
index 277a265..ff5c539 100644
--- a/contrib/dev/main.go
+++ b/contrib/dev/main.go
@@ -4,6 +4,7 @@ import (
 	"flag"
 	"fmt"
 	"log/slog"
+	"net/http"
 	"os"
 	"os/signal"
 	"syscall"
@@ -37,9 +38,12 @@ func main() {
 	git.LoadConfigFile(cfgPath, logger)
 	cfg := git.NewGitCfg(logger)
 
-	go git.GitSshServer(cfg, nil)
+	s := git.GitSshServer(cfg)
+	go s.ListenAndServe()
 	time.Sleep(time.Millisecond * 100)
-	go git.StartWebServer(cfg)
+	w := git.GitWebServer(cfg)
+	addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
+	go http.ListenAndServe(addr, w)
 
 	// Hack to wait for startup
 	time.Sleep(time.Millisecond * 100)
docker-compose.prod.yml link
+2 -9
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 3195073..84b2a55 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -18,19 +18,12 @@ services:
       - "${GITPR_HTTP_V4:-80}:80"
       - "${GITPR_HTTPS_V6:-[::1]:443}:443"
       - "${GITPR_HTTP_V6:-[::1]:80}:80"
-  web:
-    command: "/app/git-web --config ${GITPR_CONFIG_PATH}"
+  git-pr:
+    command: "/app/git-pr --config ${GITPR_CONFIG_PATH}"
     networks:
       git:
         aliases:
           - web
-    env_file:
-      - .env.prod
-  ssh:
-    command: "/app/git-ssh --config ${GITPR_CONFIG_PATH}"
-    networks:
-      git:
-        aliases:
           - ssh
     env_file:
       - .env.prod
docker-compose.yml link
+2 -7
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
diff --git a/docker-compose.yml b/docker-compose.yml
index fdead31..a85b9e8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,11 +1,6 @@
 services:
-  web:
-    image: ghcr.io/picosh/pico/git-web:latest
-    restart: always
-    volumes:
-      - ./data/git-pr/data:/app/data
-  ssh:
-    image: ghcr.io/picosh/pico/git-ssh:latest
+  git-pr:
+    image: ghcr.io/picosh/pico/git-pr:latest
     restart: always
     volumes:
       - ./data/git-pr/data:/app/data
e2e_test.go link
+7 -6
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
diff --git a/e2e_test.go b/e2e_test.go
index 7e1ea6b..b116e3f 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -1,6 +1,7 @@
 package git
 
 import (
+	"context"
 	"log/slog"
 	"os"
 	"testing"
@@ -23,8 +24,8 @@ func testSingleTenantE2E(t *testing.T) {
 		os.RemoveAll(dataDir)
 	}()
 	suite := setupTest(dataDir, cfgSingleTenantTmpl)
-	done := make(chan error)
-	go GitSshServer(suite.cfg, done)
+	s := GitSshServer(suite.cfg)
+	go s.ListenAndServe()
 	// Hack to wait for startup
 	time.Sleep(time.Millisecond * 100)
 
@@ -43,7 +44,7 @@ func testSingleTenantE2E(t *testing.T) {
 	bail(err)
 	snaps.MatchSnapshot(t, actual)
 
-	done <- nil
+	s.Shutdown(context.Background())
 }
 
 func testMultiTenantE2E(t *testing.T) {
@@ -53,8 +54,8 @@ func testMultiTenantE2E(t *testing.T) {
 		os.RemoveAll(dataDir)
 	}()
 	suite := setupTest(dataDir, cfgMultiTenantTmpl)
-	done := make(chan error)
-	go GitSshServer(suite.cfg, done)
+	s := GitSshServer(suite.cfg)
+	go s.ListenAndServe()
 
 	time.Sleep(time.Millisecond * 100)
 
@@ -120,7 +121,7 @@ func testMultiTenantE2E(t *testing.T) {
 	bail(err)
 	snaps.MatchSnapshot(t, actual)
 
-	done <- nil
+	s.Shutdown(context.Background())
 }
 
 type TestSuite struct {
ssh.go link
+3 -30
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
diff --git a/ssh.go b/ssh.go
index e5c5593..5be9658 100644
--- a/ssh.go
+++ b/ssh.go
@@ -1,13 +1,8 @@
 package git
 
 import (
-	"context"
 	"fmt"
-	"os"
-	"os/signal"
 	"path/filepath"
-	"syscall"
-	"time"
 
 	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
@@ -31,7 +26,7 @@ func authHandler(pr *PrCmd) func(ctx ssh.Context, key ssh.PublicKey) bool {
 	}
 }
 
-func GitSshServer(cfg *GitCfg, killCh chan error) {
+func GitSshServer(cfg *GitCfg) *ssh.Server {
 	dbpath := filepath.Join(cfg.DataDir, "pr.db?_fk=on")
 	dbh, err := SqliteOpen("file:"+dbpath, cfg.Logger)
 	if err != nil {
@@ -59,31 +54,9 @@ func GitSshServer(cfg *GitCfg, killCh chan error) {
 			GitPatchRequestMiddleware(be, prCmd),
 		),
 	)
-
 	if err != nil {
 		cfg.Logger.Error("could not create server", "err", err)
-		return
-	}
-
-	done := make(chan os.Signal, 1)
-	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-	cfg.Logger.Info("starting SSH server", "host", cfg.Host, "port", cfg.SshPort)
-	go func() {
-		if err = s.ListenAndServe(); err != nil {
-			cfg.Logger.Error("serve error", "err", err)
-			// os.Exit(1)
-		}
-	}()
-
-	select {
-	case <-done:
-	case <-killCh:
-	}
-	cfg.Logger.Info("stopping SSH server")
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
-	defer func() { cancel() }()
-	if err := s.Shutdown(ctx); err != nil {
-		cfg.Logger.Error("shutdown", "err", err)
-		// os.Exit(1)
+		return nil
 	}
+	return s
 }
web.go link
+15 -21
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
diff --git a/web.go b/web.go
index 8bc8839..a6da13b 100644
--- a/web.go
+++ b/web.go
@@ -1086,9 +1086,7 @@ func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) {
 	return fsys, nil
 }
 
-func StartWebServer(cfg *GitCfg) {
-	addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
-
+func GitWebServer(cfg *GitCfg) http.Handler {
 	dbpath := filepath.Join(cfg.DataDir, "pr.db?_fk=on")
 	dbh, err := SqliteOpen("file:"+dbpath, cfg.Logger)
 	if err != nil {
@@ -1122,28 +1120,24 @@ func StartWebServer(cfg *GitCfg) {
 
 	// ensure legacy router is disabled
 	// GODEBUG=httpmuxgo121=0
-	http.HandleFunc("GET /prs/{id}", ctxMdw(ctx, createPrDetail("pr")))
-	http.HandleFunc("GET /prs/{id}/rss", ctxMdw(ctx, rssHandler))
-	http.HandleFunc("GET /ps/{id}", ctxMdw(ctx, createPrDetail("ps")))
-	http.HandleFunc("GET /rd/{id}", ctxMdw(ctx, createPrDetail("rd")))
-	http.HandleFunc("GET /r/{user}/{repo}/rss", ctxMdw(ctx, rssHandler))
-	http.HandleFunc("GET /r/{user}/{repo}", ctxMdw(ctx, repoDetailHandler))
-	http.HandleFunc("GET /r/{user}", ctxMdw(ctx, userDetailHandler))
-	http.HandleFunc("GET /rss/{user}", ctxMdw(ctx, rssHandler))
-	http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
-	http.HandleFunc("GET /", ctxMdw(ctx, indexHandler))
-	http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
+	mux := http.NewServeMux()
+	mux.HandleFunc("GET /prs/{id}", ctxMdw(ctx, createPrDetail("pr")))
+	mux.HandleFunc("GET /prs/{id}/rss", ctxMdw(ctx, rssHandler))
+	mux.HandleFunc("GET /ps/{id}", ctxMdw(ctx, createPrDetail("ps")))
+	mux.HandleFunc("GET /rd/{id}", ctxMdw(ctx, createPrDetail("rd")))
+	mux.HandleFunc("GET /r/{user}/{repo}/rss", ctxMdw(ctx, rssHandler))
+	mux.HandleFunc("GET /r/{user}/{repo}", ctxMdw(ctx, repoDetailHandler))
+	mux.HandleFunc("GET /r/{user}", ctxMdw(ctx, userDetailHandler))
+	mux.HandleFunc("GET /rss/{user}", ctxMdw(ctx, rssHandler))
+	mux.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
+	mux.HandleFunc("GET /", ctxMdw(ctx, indexHandler))
+	mux.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
 	embedFS, err := getEmbedFS(embedStaticFS, "static")
 	if err != nil {
 		panic(err)
 	}
 	userFS := getUserDefinedFS(cfg.DataDir, "static")
 
-	http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
-
-	cfg.Logger.Info("starting web server", "addr", addr)
-	err = http.ListenAndServe(addr, nil)
-	if err != nil {
-		cfg.Logger.Error("listen", "err", err)
-	}
+	mux.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
+	return mux
 }