Files
GitManager/cmd/server/main.go
T
TBNilles a192a05aaa Add repo-detail panel and /api/repo endpoint
New <repo-detail> component listens for repo:select and shows a repo's remotes, local branches (current + upstream), and recent commits. Backed by GET /api/repo (restricted to indexed repos) and read-only git readers LocalBranches/RecentCommits/RemoteDetails via repos.BuildDetail. <repo-list> highlights the selection; page docks the two panels left/right.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 16:43:01 -04:00

117 lines
3.3 KiB
Go

// Command server is the GitManager entrypoint. It wires config, logging, the
// Git boundary, the repo scanner, and the Echo HTTP server, then serves the
// dashboard shell and the JSON endpoints the web components fetch from.
package main
import (
"context"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"gitmanager/internal/config"
"gitmanager/internal/git"
"gitmanager/internal/logging"
"gitmanager/internal/render"
"gitmanager/internal/repos"
)
func main() {
cfg, err := config.Load()
if err != nil {
panic(err)
}
log, closer, err := logging.Setup(cfg.Dev, cfg.LogFile)
if err != nil {
panic(err)
}
if closer != nil {
defer closer.Close()
}
g := git.New(cfg.GitBin)
if v, err := g.Version(context.Background()); err != nil {
log.Warn("git binary not usable — repo operations will fail", "bin", cfg.GitBin, "err", err)
} else {
log.Info("git detected", "version", v)
}
// Start the read-only scanner in the background.
scanner := repos.NewScanner(g, log, cfg.RepoRoots, cfg.ScanMaxDepth, cfg.ScanIgnore, cfg.ScanInterval, cfg.ScanFetchEnabled)
scanCtx, stopScan := context.WithCancel(context.Background())
defer stopScan()
go scanner.Run(scanCtx)
log.Info("scanner started", "roots", cfg.RepoRoots, "interval", cfg.ScanInterval.String(), "fetch", cfg.ScanFetchEnabled)
tmpl, err := render.New("web/templates")
if err != nil {
log.Error("failed to parse templates", "err", err)
os.Exit(1)
}
e := echo.New()
e.HideBanner = true
e.Renderer = tmpl
e.Use(middleware.Recover())
e.Use(middleware.RequestID())
// Static assets and component sources.
e.Static("/static", "web/static")
e.Static("/components", "components")
// Page shells.
e.GET("/", func(c echo.Context) error {
return c.Render(http.StatusOK, "index.html", nil)
})
e.GET("/help", func(c echo.Context) error {
return c.Render(http.StatusOK, "help.html", nil)
})
// JSON API — components self-fetch from here.
e.GET("/healthz", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
})
e.GET("/api/repos", func(c echo.Context) error {
return c.JSON(http.StatusOK, scanner.Index.List())
})
e.GET("/api/repo", func(c echo.Context) error {
// Only serve details for a repo we already discovered — never run git
// against an arbitrary path supplied in the query string. Clean the
// input so separator style (/, \) doesn't defeat the exact-match lookup.
path := filepath.Clean(c.QueryParam("path"))
base, ok := scanner.Index.Get(path)
if !ok {
return c.JSON(http.StatusNotFound, map[string]string{"error": "unknown repository"})
}
return c.JSON(http.StatusOK, repos.BuildDetail(c.Request().Context(), g, base))
})
// Serve with graceful shutdown.
go func() {
if err := e.Start(cfg.ListenAddr); err != nil && err != http.ErrServerClosed {
log.Error("server error", "err", err)
os.Exit(1)
}
}()
log.Info("listening", "addr", cfg.ListenAddr)
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
log.Info("shutting down")
stopScan()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
log.Error("graceful shutdown failed", "err", err)
}
}