git-pr

created pr with 64.1 on 2025-04-17T21:14:13Z · by 964fa508
cmds
checkout latest patchset:
ssh pr.pico.sh print 64 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print 64.[rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 64
+1 -1 Caddyfile #
......@@ -48,6 +48,6 @@
4848 }
4949
5050 :443 {
51- reverse_proxy git-web:3000
51+ reverse_proxy git-pr:3000
5252 encode zstd gzip
5353 }
+6 -29 Dockerfile #
......@@ -10,7 +10,7 @@ COPY go.* ./
1010
1111 RUN go mod download
1212
13-FROM builder-deps as builder-web
13+FROM builder-deps as builder
1414
1515 COPY . .
1616
......@@ -22,37 +22,14 @@ ENV LDFLAGS="-s -w"
2222
2323 ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH}
2424
25-RUN go build -ldflags "$LDFLAGS" -o /go/bin/git-web ./cmd/git-web
25+RUN go build -ldflags "$LDFLAGS" -o /go/bin/git-pr ./cmd/git-pr
2626
27-FROM builder-deps as builder-ssh
28-
29-COPY . .
30-
31-ARG TARGETOS
32-ARG TARGETARCH
33-
34-ENV CGO_ENABLED=0
35-ENV LDFLAGS="-s -w"
36-
37-ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH}
38-
39-RUN go build -ldflags "$LDFLAGS" -o /go/bin/git-ssh ./cmd/git-ssh
40-
41-FROM scratch as release-web
42-
43-WORKDIR /app
44-
45-COPY --from=builder-web /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
46-COPY --from=builder-web /go/bin/git-web ./git-web
47-
48-CMD ["/app/git-web"]
49-
50-FROM scratch as release-ssh
27+FROM scratch as release
5128
5229 WORKDIR /app
5330 ENV TERM="xterm-256color"
5431
55-COPY --from=builder-ssh /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
56-COPY --from=builder-ssh /go/bin/git-ssh ./git-ssh
32+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
33+COPY --from=builder /go/bin/git-pr ./git-pr
5734
58-CMD ["/app/git-ssh"]
35+CMD ["/app/git-pr"]
+3 -8 Makefile #
......@@ -21,8 +21,7 @@ snapshot:
2121 .PHONY: snapshot
2222
2323 build:
24- go build -o ./build/git-ssh ./cmd/git-ssh
25- go build -o ./build/git-web ./cmd/git-web
24+ go build -o ./build/git-pr ./cmd/git-pr
2625 .PHONY: build
2726
2827 bp-setup:
......@@ -30,12 +29,8 @@ bp-setup:
3029 $(DOCKER_CMD) buildx use pico
3130 .PHONY: bp-setup
3231
33-bp-web: bp-setup
34- $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-web:$(DOCKER_TAG)" --target release-web .
35-.PHONY: bp-web
36-
37-bp: bp-web
38- $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-ssh:$(DOCKER_TAG)" --target release-ssh .
32+bp: bp-setup
33+ $(DOCKER_BUILDX_BUILD) -t "ghcr.io/picosh/pico/git-pr:$(DOCKER_TAG)" --target release-pr .
3934 .PHONY: bp
4035
4136 deploy: bp-web
+3 -13 README.md #
......@@ -159,16 +159,10 @@ vim ./data/git-pr.toml
159159
160160 ## docker
161161
162-Run the ssh app image:
162+Run the app image:
163163
164164 ```bash
165-docker run -d -v ./data:/app/data ghcr.io/picosh/pico/git-ssh:latest
166-```
167-
168-Run the web app image:
169-
170-```bash
171-docker run -d -v ./data:/app/data ghcr.io/picosh/pico/git-web:latest
165+docker run -d -v ./data:/app/data ghcr.io/picosh/pico/git-pr:latest
172166 ```
173167
174168 ## golang
......@@ -180,11 +174,7 @@ make build
180174 ```
181175
182176 ```bash
183-./build/ssh --config ./data/git-pr.toml
184-```
185-
186-```bash
187-./build/web --config ./data/git-pr.toml
177+./build/git-pr --config ./data/git-pr.toml
188178 ```
189179
190180 ## done!
+57 -0 cmd/git-pr/main.go #
......@@ -0,0 +1,57 @@
1+package main
2+
3+import (
4+ "context"
5+ "flag"
6+ "fmt"
7+ "log/slog"
8+ "net/http"
9+ "os"
10+ "os/signal"
11+ "syscall"
12+ "time"
13+
14+ git "github.com/picosh/git-pr"
15+)
16+
17+func main() {
18+ fpath := flag.String("config", "git-pr.toml", "configuration toml file")
19+ flag.Parse()
20+ opts := &slog.HandlerOptions{
21+ AddSource: true,
22+ }
23+ logger := slog.New(
24+ slog.NewTextHandler(os.Stdout, opts),
25+ )
26+ git.LoadConfigFile(*fpath, logger)
27+ cfg := git.NewGitCfg(logger)
28+
29+ // SSH Server
30+ ssh := git.GitSshServer(cfg)
31+ cfg.Logger.Info("starting SSH server", "host", cfg.Host, "port", cfg.SshPort)
32+ go func() {
33+ if err := ssh.ListenAndServe(); err != nil {
34+ cfg.Logger.Error("serve error", "err", err)
35+ }
36+ }()
37+
38+ // Web Server
39+ addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
40+ web := git.GitWebServer(cfg)
41+ cfg.Logger.Info("starting web server", "addr", addr)
42+ go func() {
43+ if err := http.ListenAndServe(addr, web); err != nil {
44+ cfg.Logger.Error("listen", "err", err)
45+ }
46+ }()
47+
48+ done := make(chan os.Signal, 1)
49+ signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
50+ <-done
51+ cfg.Logger.Info("stopping SSH server")
52+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
53+ defer func() { cancel() }()
54+ if err := ssh.Shutdown(ctx); err != nil {
55+ cfg.Logger.Error("shutdown", "err", err)
56+ }
57+}
+0 -22 cmd/git-ssh/main.go #
......@@ -1,22 +0,0 @@
1-package main
2-
3-import (
4- "flag"
5- "log/slog"
6- "os"
7-
8- git "github.com/picosh/git-pr"
9-)
10-
11-func main() {
12- fpath := flag.String("config", "git-pr.toml", "configuration toml file")
13- flag.Parse()
14- opts := &slog.HandlerOptions{
15- AddSource: true,
16- }
17- logger := slog.New(
18- slog.NewTextHandler(os.Stdout, opts),
19- )
20- git.LoadConfigFile(*fpath, logger)
21- git.GitSshServer(git.NewGitCfg(logger), nil)
22-}
+0 -22 cmd/git-web/main.go #
......@@ -1,22 +0,0 @@
1-package main
2-
3-import (
4- "flag"
5- "log/slog"
6- "os"
7-
8- git "github.com/picosh/git-pr"
9-)
10-
11-func main() {
12- fpath := flag.String("config", "git-pr.toml", "configuration toml file")
13- flag.Parse()
14- opts := &slog.HandlerOptions{
15- AddSource: true,
16- }
17- logger := slog.New(
18- slog.NewTextHandler(os.Stdout, opts),
19- )
20- git.LoadConfigFile(*fpath, logger)
21- git.StartWebServer(git.NewGitCfg(logger))
22-}
+6 -2 contrib/dev/main.go #
......@@ -4,6 +4,7 @@ import (
44 "flag"
55 "fmt"
66 "log/slog"
7+ "net/http"
78 "os"
89 "os/signal"
910 "syscall"
......@@ -37,9 +38,12 @@ func main() {
3738 git.LoadConfigFile(cfgPath, logger)
3839 cfg := git.NewGitCfg(logger)
3940
40- go git.GitSshServer(cfg, nil)
41+ s := git.GitSshServer(cfg)
42+ go s.ListenAndServe()
4143 time.Sleep(time.Millisecond * 100)
42- go git.StartWebServer(cfg)
44+ w := git.GitWebServer(cfg)
45+ addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
46+ go http.ListenAndServe(addr, w)
4347
4448 // Hack to wait for startup
4549 time.Sleep(time.Millisecond * 100)
+2 -9 docker-compose.prod.yml #
......@@ -18,19 +18,12 @@ services:
1818 - "${GITPR_HTTP_V4:-80}:80"
1919 - "${GITPR_HTTPS_V6:-[::1]:443}:443"
2020 - "${GITPR_HTTP_V6:-[::1]:80}:80"
21- web:
22- command: "/app/git-web --config ${GITPR_CONFIG_PATH}"
21+ git-pr:
22+ command: "/app/git-pr --config ${GITPR_CONFIG_PATH}"
2323 networks:
2424 git:
2525 aliases:
2626 - web
27- env_file:
28- - .env.prod
29- ssh:
30- command: "/app/git-ssh --config ${GITPR_CONFIG_PATH}"
31- networks:
32- git:
33- aliases:
3427 - ssh
3528 env_file:
3629 - .env.prod
+2 -7 docker-compose.yml #
......@@ -1,11 +1,6 @@
11 services:
2- web:
3- image: ghcr.io/picosh/pico/git-web:latest
4- restart: always
5- volumes:
6- - ./data/git-pr/data:/app/data
7- ssh:
8- image: ghcr.io/picosh/pico/git-ssh:latest
2+ git-pr:
3+ image: ghcr.io/picosh/pico/git-pr:latest
94 restart: always
105 volumes:
116 - ./data/git-pr/data:/app/data
+7 -6 e2e_test.go #
......@@ -1,6 +1,7 @@
11 package git
22
33 import (
4+ "context"
45 "log/slog"
56 "os"
67 "testing"
......@@ -23,8 +24,8 @@ func testSingleTenantE2E(t *testing.T) {
2324 os.RemoveAll(dataDir)
2425 }()
2526 suite := setupTest(dataDir, cfgSingleTenantTmpl)
26- done := make(chan error)
27- go GitSshServer(suite.cfg, done)
27+ s := GitSshServer(suite.cfg)
28+ go s.ListenAndServe()
2829 // Hack to wait for startup
2930 time.Sleep(time.Millisecond * 100)
3031
......@@ -43,7 +44,7 @@ func testSingleTenantE2E(t *testing.T) {
4344 bail(err)
4445 snaps.MatchSnapshot(t, actual)
4546
46- done <- nil
47+ s.Shutdown(context.Background())
4748 }
4849
4950 func testMultiTenantE2E(t *testing.T) {
......@@ -53,8 +54,8 @@ func testMultiTenantE2E(t *testing.T) {
5354 os.RemoveAll(dataDir)
5455 }()
5556 suite := setupTest(dataDir, cfgMultiTenantTmpl)
56- done := make(chan error)
57- go GitSshServer(suite.cfg, done)
57+ s := GitSshServer(suite.cfg)
58+ go s.ListenAndServe()
5859
5960 time.Sleep(time.Millisecond * 100)
6061
......@@ -120,7 +121,7 @@ func testMultiTenantE2E(t *testing.T) {
120121 bail(err)
121122 snaps.MatchSnapshot(t, actual)
122123
123- done <- nil
124+ s.Shutdown(context.Background())
124125 }
125126
126127 type TestSuite struct {
+3 -30 ssh.go #
......@@ -1,13 +1,8 @@
11 package git
22
33 import (
4- "context"
54 "fmt"
6- "os"
7- "os/signal"
85 "path/filepath"
9- "syscall"
10- "time"
116
127 "github.com/charmbracelet/ssh"
138 "github.com/charmbracelet/wish"
......@@ -31,7 +26,7 @@ func authHandler(pr *PrCmd) func(ctx ssh.Context, key ssh.PublicKey) bool {
3126 }
3227 }
3328
34-func GitSshServer(cfg *GitCfg, killCh chan error) {
29+func GitSshServer(cfg *GitCfg) *ssh.Server {
3530 dbpath := filepath.Join(cfg.DataDir, "pr.db?_fk=on")
3631 dbh, err := SqliteOpen("file:"+dbpath, cfg.Logger)
3732 if err != nil {
......@@ -59,31 +54,9 @@ func GitSshServer(cfg *GitCfg, killCh chan error) {
5954 GitPatchRequestMiddleware(be, prCmd),
6055 ),
6156 )
62-
6357 if err != nil {
6458 cfg.Logger.Error("could not create server", "err", err)
65- return
66- }
67-
68- done := make(chan os.Signal, 1)
69- signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
70- cfg.Logger.Info("starting SSH server", "host", cfg.Host, "port", cfg.SshPort)
71- go func() {
72- if err = s.ListenAndServe(); err != nil {
73- cfg.Logger.Error("serve error", "err", err)
74- // os.Exit(1)
75- }
76- }()
77-
78- select {
79- case <-done:
80- case <-killCh:
81- }
82- cfg.Logger.Info("stopping SSH server")
83- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
84- defer func() { cancel() }()
85- if err := s.Shutdown(ctx); err != nil {
86- cfg.Logger.Error("shutdown", "err", err)
87- // os.Exit(1)
59+ return nil
8860 }
61+ return s
8962 }
+15 -21 web.go #
......@@ -1086,9 +1086,7 @@ func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) {
10861086 return fsys, nil
10871087 }
10881088
1089-func StartWebServer(cfg *GitCfg) {
1090- addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort)
1091-
1089+func GitWebServer(cfg *GitCfg) http.Handler {
10921090 dbpath := filepath.Join(cfg.DataDir, "pr.db?_fk=on")
10931091 dbh, err := SqliteOpen("file:"+dbpath, cfg.Logger)
10941092 if err != nil {
......@@ -1122,28 +1120,24 @@ func StartWebServer(cfg *GitCfg) {
11221120
11231121 // ensure legacy router is disabled
11241122 // GODEBUG=httpmuxgo121=0
1125- http.HandleFunc("GET /prs/{id}", ctxMdw(ctx, createPrDetail("pr")))
1126- http.HandleFunc("GET /prs/{id}/rss", ctxMdw(ctx, rssHandler))
1127- http.HandleFunc("GET /ps/{id}", ctxMdw(ctx, createPrDetail("ps")))
1128- http.HandleFunc("GET /rd/{id}", ctxMdw(ctx, createPrDetail("rd")))
1129- http.HandleFunc("GET /r/{user}/{repo}/rss", ctxMdw(ctx, rssHandler))
1130- http.HandleFunc("GET /r/{user}/{repo}", ctxMdw(ctx, repoDetailHandler))
1131- http.HandleFunc("GET /r/{user}", ctxMdw(ctx, userDetailHandler))
1132- http.HandleFunc("GET /rss/{user}", ctxMdw(ctx, rssHandler))
1133- http.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
1134- http.HandleFunc("GET /", ctxMdw(ctx, indexHandler))
1135- http.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
1123+ mux := http.NewServeMux()
1124+ mux.HandleFunc("GET /prs/{id}", ctxMdw(ctx, createPrDetail("pr")))
1125+ mux.HandleFunc("GET /prs/{id}/rss", ctxMdw(ctx, rssHandler))
1126+ mux.HandleFunc("GET /ps/{id}", ctxMdw(ctx, createPrDetail("ps")))
1127+ mux.HandleFunc("GET /rd/{id}", ctxMdw(ctx, createPrDetail("rd")))
1128+ mux.HandleFunc("GET /r/{user}/{repo}/rss", ctxMdw(ctx, rssHandler))
1129+ mux.HandleFunc("GET /r/{user}/{repo}", ctxMdw(ctx, repoDetailHandler))
1130+ mux.HandleFunc("GET /r/{user}", ctxMdw(ctx, userDetailHandler))
1131+ mux.HandleFunc("GET /rss/{user}", ctxMdw(ctx, rssHandler))
1132+ mux.HandleFunc("GET /rss", ctxMdw(ctx, rssHandler))
1133+ mux.HandleFunc("GET /", ctxMdw(ctx, indexHandler))
1134+ mux.HandleFunc("GET /syntax.css", ctxMdw(ctx, chromaStyleHandler))
11361135 embedFS, err := getEmbedFS(embedStaticFS, "static")
11371136 if err != nil {
11381137 panic(err)
11391138 }
11401139 userFS := getUserDefinedFS(cfg.DataDir, "static")
11411140
1142- http.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
1143-
1144- cfg.Logger.Info("starting web server", "addr", addr)
1145- err = http.ListenAndServe(addr, nil)
1146- if err != nil {
1147- cfg.Logger.Error("listen", "err", err)
1148- }
1141+ mux.HandleFunc("GET /static/{file}", ctxMdw(ctx, serveFile(userFS, embedFS)))
1142+ return mux
11491143 }
Back to top