git-pr
created pr with
64.1
cmds
checkout latest patchset:
ssh pr.pico.sh print 64 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 64.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 64
Patchset
64.1
single binary
jolheiser
2025-04-17T21:07:03ZReduce 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>
Semantic diff summary
2 added,
6 modified,
1 signature changed,
3 removed
across 7 analyzed files
(6 files skipped: unsupported file type)
+1
-1
Caddyfile
#
+6
-29
Dockerfile
#
| ... | ... | @@ -22,37 +22,14 @@ ENV LDFLAGS="-s -w" | |
| 22 | 22 | ||
| 23 | 23 | ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} | |
| 24 | 24 | ||
| 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 | |
| 26 | 26 | ||
| 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 | |
| 51 | 28 | ||
| 52 | 29 | WORKDIR /app | |
| 53 | 30 | ENV TERM="xterm-256color" | |
| 54 | 31 | ||
| 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 | |
| 57 | 34 | ||
| 58 | - | CMD ["/app/git-ssh"] | |
| 35 | + | CMD ["/app/git-pr"] |
+3
-8
Makefile
#
| ... | ... | @@ -30,12 +29,8 @@ bp-setup: | |
| 30 | 29 | $(DOCKER_CMD) buildx use pico | |
| 31 | 30 | .PHONY: bp-setup | |
| 32 | 31 | ||
| 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 . | |
| 39 | 34 | .PHONY: bp | |
| 40 | 35 | ||
| 41 | 36 | deploy: bp-web |
+3
-13
README.md
#
| ... | ... | @@ -159,16 +159,10 @@ vim ./data/git-pr.toml | |
| 159 | 159 | ||
| 160 | 160 | ## docker | |
| 161 | 161 | ||
| 162 | - | Run the ssh app image: | |
| 162 | + | Run the app image: | |
| 163 | 163 | ||
| 164 | 164 | ```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 | |
| 172 | 166 | ``` | |
| 173 | 167 | ||
| 174 | 168 | ## golang |
+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
#
| ... | ... | @@ -37,9 +38,12 @@ func main() { | |
| 37 | 38 | git.LoadConfigFile(cfgPath, logger) | |
| 38 | 39 | cfg := git.NewGitCfg(logger) | |
| 39 | 40 | ||
| 40 | - | go git.GitSshServer(cfg, nil) | |
| 41 | + | s := git.GitSshServer(cfg) | |
| 42 | + | go s.ListenAndServe() | |
| 41 | 43 | 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) | |
| 43 | 47 | ||
| 44 | 48 | // Hack to wait for startup | |
| 45 | 49 | time.Sleep(time.Millisecond * 100) |
+2
-9
docker-compose.prod.yml
#
| ... | ... | @@ -18,19 +18,12 @@ services: | |
| 18 | 18 | - "${GITPR_HTTP_V4:-80}:80" | |
| 19 | 19 | - "${GITPR_HTTPS_V6:-[::1]:443}:443" | |
| 20 | 20 | - "${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}" | |
| 23 | 23 | networks: | |
| 24 | 24 | git: | |
| 25 | 25 | aliases: | |
| 26 | 26 | - web | |
| 27 | - | env_file: | |
| 28 | - | - .env.prod | |
| 29 | - | ssh: | |
| 30 | - | command: "/app/git-ssh --config ${GITPR_CONFIG_PATH}" | |
| 31 | - | networks: | |
| 32 | - | git: | |
| 33 | - | aliases: | |
| 34 | 27 | - ssh | |
| 35 | 28 | env_file: | |
| 36 | 29 | - .env.prod |
+2
-7
docker-compose.yml
#
| ... | ... | @@ -1,11 +1,6 @@ | |
| 1 | 1 | 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 | |
| 9 | 4 | restart: always | |
| 10 | 5 | volumes: | |
| 11 | 6 | - ./data/git-pr/data:/app/data |
+7
-6
e2e_test.go
#
| ... | ... | @@ -23,8 +24,8 @@ func testSingleTenantE2E(t *testing.T) { | |
| 23 | 24 | os.RemoveAll(dataDir) | |
| 24 | 25 | }() | |
| 25 | 26 | suite := setupTest(dataDir, cfgSingleTenantTmpl) | |
| 26 | - | done := make(chan error) | |
| 27 | - | go GitSshServer(suite.cfg, done) | |
| 27 | + | s := GitSshServer(suite.cfg) | |
| 28 | + | go s.ListenAndServe() | |
| 28 | 29 | // Hack to wait for startup | |
| 29 | 30 | time.Sleep(time.Millisecond * 100) | |
| 30 | 31 |
| ... | ... | @@ -53,8 +54,8 @@ func testMultiTenantE2E(t *testing.T) { | |
| 53 | 54 | os.RemoveAll(dataDir) | |
| 54 | 55 | }() | |
| 55 | 56 | suite := setupTest(dataDir, cfgMultiTenantTmpl) | |
| 56 | - | done := make(chan error) | |
| 57 | - | go GitSshServer(suite.cfg, done) | |
| 57 | + | s := GitSshServer(suite.cfg) | |
| 58 | + | go s.ListenAndServe() | |
| 58 | 59 | ||
| 59 | 60 | time.Sleep(time.Millisecond * 100) | |
| 60 | 61 |
+3
-30
ssh.go
#
| ... | ... | @@ -31,7 +26,7 @@ func authHandler(pr *PrCmd) func(ctx ssh.Context, key ssh.PublicKey) bool { | |
| 31 | 26 | } | |
| 32 | 27 | } | |
| 33 | 28 | ||
| 34 | - | func GitSshServer(cfg *GitCfg, killCh chan error) { | |
| 29 | + | func GitSshServer(cfg *GitCfg) *ssh.Server { | |
| 35 | 30 | dbpath := filepath.Join(cfg.DataDir, "pr.db?_fk=on") | |
| 36 | 31 | dbh, err := SqliteOpen("file:"+dbpath, cfg.Logger) | |
| 37 | 32 | if err != nil { |
| ... | ... | @@ -59,31 +54,9 @@ func GitSshServer(cfg *GitCfg, killCh chan error) { | |
| 59 | 54 | GitPatchRequestMiddleware(be, prCmd), | |
| 60 | 55 | ), | |
| 61 | 56 | ) | |
| 62 | - | ||
| 63 | 57 | if err != nil { | |
| 64 | 58 | 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 | |
| 88 | 60 | } | |
| 61 | + | return s | |
| 89 | 62 | } |
+15
-21
web.go
#
| ... | ... | @@ -1086,9 +1086,7 @@ func getEmbedFS(ffs embed.FS, dirName string) (fs.FS, error) { | |
| 1086 | 1086 | return fsys, nil | |
| 1087 | 1087 | } | |
| 1088 | 1088 | ||
| 1089 | - | func StartWebServer(cfg *GitCfg) { | |
| 1090 | - | addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.WebPort) | |
| 1091 | - | ||
| 1089 | + | func GitWebServer(cfg *GitCfg) http.Handler { | |
| 1092 | 1090 | dbpath := filepath.Join(cfg.DataDir, "pr.db?_fk=on") | |
| 1093 | 1091 | dbh, err := SqliteOpen("file:"+dbpath, cfg.Logger) | |
| 1094 | 1092 | if err != nil { |
| ... | ... | @@ -1122,28 +1120,24 @@ func StartWebServer(cfg *GitCfg) { | |
| 1122 | 1120 | ||
| 1123 | 1121 | // ensure legacy router is disabled | |
| 1124 | 1122 | // 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)) | |
| 1136 | 1135 | embedFS, err := getEmbedFS(embedStaticFS, "static") | |
| 1137 | 1136 | if err != nil { | |
| 1138 | 1137 | panic(err) | |
| 1139 | 1138 | } | |
| 1140 | 1139 | userFS := getUserDefinedFS(cfg.DataDir, "static") | |
| 1141 | 1140 | ||
| 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 | |
| 1149 | 1143 | } |