Slice 5: Gitea forge - PRs and Merge & clean up
New internal/forge (provider-abstracted, Gitea impl via code.gitea.io/sdk/gitea) with tested remote-URL parsing and read+write ops: list open PRs, and merge-and-cleanup (squash-merge + delete head branch when head/base share a repo). Service resolves repo->owner/repo from remotes (prefers origin) and records a pr-merged event; config gains GITEA_URL/GITEA_TOKEN (forge disabled without both). MCP tools list_prs and merge_and_cleanup_pr (merge tool tells Claude to confirm first, 1.4). HTTP GET /api/repo/prs, POST /api/repo/pr/merge. New <pr-list> component with a confirming Merge & clean up button, hidden when no forge. Verified graceful-disabled path; real merge pending token + a designated PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+40
-1
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"gitmanager/internal/activity"
|
||||
"gitmanager/internal/config"
|
||||
"gitmanager/internal/forge"
|
||||
"gitmanager/internal/git"
|
||||
"gitmanager/internal/logging"
|
||||
mcpserver "gitmanager/internal/mcp"
|
||||
@@ -56,8 +57,18 @@ func main() {
|
||||
// Coordination state: active project + activity feed (§8.2).
|
||||
feed := activity.New(log, 200)
|
||||
|
||||
// Forge provider (Gitea) — optional; nil when unconfigured (§8.4).
|
||||
fg, err := forge.NewGitea(cfg.GiteaURL, cfg.GiteaToken)
|
||||
if err != nil {
|
||||
log.Warn("forge disabled — invalid config", "err", err)
|
||||
} else if fg != nil {
|
||||
log.Info("forge enabled", "provider", "gitea", "url", cfg.GiteaURL)
|
||||
} else {
|
||||
log.Info("forge disabled — set GITEA_URL and GITEA_TOKEN to enable")
|
||||
}
|
||||
|
||||
// The one service layer both the HTTP API and the MCP server call (§1.7).
|
||||
svc := service.New(g, scanner.Index, feed)
|
||||
svc := service.New(g, scanner.Index, feed, fg)
|
||||
|
||||
tmpl, err := render.New("web/templates")
|
||||
if err != nil {
|
||||
@@ -150,6 +161,34 @@ func main() {
|
||||
return c.JSON(http.StatusOK, map[string]any{"pending": false})
|
||||
})
|
||||
|
||||
// Forge PRs + "Merge & clean up" (§8.4).
|
||||
e.GET("/api/repo/prs", func(c echo.Context) error {
|
||||
prs, err := svc.ForgePRs(c.Request().Context(), c.QueryParam("path"))
|
||||
if err != nil {
|
||||
// Not configured / not on the forge host is a normal "no PRs here"
|
||||
// state — tell the UI to hide the section rather than error.
|
||||
if err == forge.ErrNotConfigured || err == forge.ErrNotSupported {
|
||||
return c.JSON(http.StatusOK, map[string]any{"supported": false})
|
||||
}
|
||||
return c.JSON(http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]any{"supported": true, "prs": prs})
|
||||
})
|
||||
e.POST("/api/repo/pr/merge", func(c echo.Context) error {
|
||||
var body struct {
|
||||
Path string `json:"path"`
|
||||
Number int64 `json:"number"`
|
||||
}
|
||||
if err := c.Bind(&body); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid body"})
|
||||
}
|
||||
res, err := svc.MergeAndCleanup(c.Request().Context(), activity.ActorUser, body.Path, body.Number)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, res)
|
||||
})
|
||||
|
||||
// SSE stream of activity events for live UI (§8.2).
|
||||
e.GET("/events", func(c echo.Context) error {
|
||||
w := c.Response()
|
||||
|
||||
Reference in New Issue
Block a user