Files
kandev/apps/backend/internal/agent/handlers/git_handlers.go
T

350 lines
9.4 KiB
Go

// Package handlers provides WebSocket and HTTP handlers for agent operations.
package handlers
import (
"context"
"fmt"
"github.com/kandev/kandev/internal/agent/lifecycle"
"github.com/kandev/kandev/internal/agentctl/client"
"github.com/kandev/kandev/internal/common/logger"
ws "github.com/kandev/kandev/pkg/websocket"
"go.uber.org/zap"
)
// GitHandlers provides WebSocket handlers for git worktree operations.
// Operations are executed via agentctl which runs in the worktree context.
type GitHandlers struct {
lifecycleMgr *lifecycle.Manager
logger *logger.Logger
}
// NewGitHandlers creates a new GitHandlers instance
func NewGitHandlers(lifecycleMgr *lifecycle.Manager, log *logger.Logger) *GitHandlers {
return &GitHandlers{
lifecycleMgr: lifecycleMgr,
logger: log.WithFields(zap.String("component", "git_handlers")),
}
}
// RegisterHandlers registers git handlers with the WebSocket dispatcher
func (h *GitHandlers) RegisterHandlers(d *ws.Dispatcher) {
d.RegisterFunc(ws.ActionWorktreePull, h.wsPull)
d.RegisterFunc(ws.ActionWorktreePush, h.wsPush)
d.RegisterFunc(ws.ActionWorktreeRebase, h.wsRebase)
d.RegisterFunc(ws.ActionWorktreeMerge, h.wsMerge)
d.RegisterFunc(ws.ActionWorktreeAbort, h.wsAbort)
d.RegisterFunc(ws.ActionWorktreeCommit, h.wsCommit)
d.RegisterFunc(ws.ActionWorktreeStage, h.wsStage)
d.RegisterFunc(ws.ActionWorktreeCreatePR, h.wsCreatePR)
d.RegisterFunc(ws.ActionSessionCommitDiff, h.wsCommitDiff)
}
// GitPullRequest for worktree.pull action
type GitPullRequest struct {
SessionID string `json:"session_id"`
Rebase bool `json:"rebase"`
}
// GitPushRequest for worktree.push action
type GitPushRequest struct {
SessionID string `json:"session_id"`
Force bool `json:"force"`
SetUpstream bool `json:"set_upstream"`
}
// GitRebaseRequest for worktree.rebase action
type GitRebaseRequest struct {
SessionID string `json:"session_id"`
BaseBranch string `json:"base_branch"`
}
// GitMergeRequest for worktree.merge action
type GitMergeRequest struct {
SessionID string `json:"session_id"`
BaseBranch string `json:"base_branch"`
}
// GitAbortRequest for worktree.abort action
type GitAbortRequest struct {
SessionID string `json:"session_id"`
Operation string `json:"operation"` // "merge" or "rebase"
}
// GitCommitRequest for worktree.commit action
type GitCommitRequest struct {
SessionID string `json:"session_id"`
Message string `json:"message"`
StageAll bool `json:"stage_all"`
}
// GitStageRequest for worktree.stage action
type GitStageRequest struct {
SessionID string `json:"session_id"`
Paths []string `json:"paths"` // Empty = stage all
}
// GitCreatePRRequest for worktree.create_pr action
type GitCreatePRRequest struct {
SessionID string `json:"session_id"`
Title string `json:"title"`
Body string `json:"body"`
BaseBranch string `json:"base_branch"`
}
// GitShowCommitRequest for session.commit_diff action
type GitShowCommitRequest struct {
SessionID string `json:"session_id"`
CommitSHA string `json:"commit_sha"`
}
// wsPull handles worktree.pull action
func (h *GitHandlers) wsPull(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitPullRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
client, err := h.getAgentCtlClient(req.SessionID)
if err != nil {
return nil, err
}
result, err := client.GitPull(ctx, req.Rebase)
if err != nil {
return nil, fmt.Errorf("pull failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
// wsPush handles worktree.push action
func (h *GitHandlers) wsPush(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitPushRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
client, err := h.getAgentCtlClient(req.SessionID)
if err != nil {
return nil, err
}
result, err := client.GitPush(ctx, req.Force, req.SetUpstream)
if err != nil {
return nil, fmt.Errorf("push failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
// wsRebase handles worktree.rebase action
func (h *GitHandlers) wsRebase(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitRebaseRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
if req.BaseBranch == "" {
return nil, fmt.Errorf("base_branch is required")
}
client, err := h.getAgentCtlClient(req.SessionID)
if err != nil {
return nil, err
}
result, err := client.GitRebase(ctx, req.BaseBranch)
if err != nil {
return nil, fmt.Errorf("rebase failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
// wsMerge handles worktree.merge action
func (h *GitHandlers) wsMerge(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitMergeRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
if req.BaseBranch == "" {
return nil, fmt.Errorf("base_branch is required")
}
client, err := h.getAgentCtlClient(req.SessionID)
if err != nil {
return nil, err
}
result, err := client.GitMerge(ctx, req.BaseBranch)
if err != nil {
return nil, fmt.Errorf("merge failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
// wsAbort handles worktree.abort action
func (h *GitHandlers) wsAbort(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitAbortRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
if req.Operation != "merge" && req.Operation != "rebase" {
return nil, fmt.Errorf("operation must be 'merge' or 'rebase'")
}
client, err := h.getAgentCtlClient(req.SessionID)
if err != nil {
return nil, err
}
result, err := client.GitAbort(ctx, req.Operation)
if err != nil {
return nil, fmt.Errorf("abort failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
// wsCommit handles worktree.commit action
func (h *GitHandlers) wsCommit(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitCommitRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
if req.Message == "" {
return nil, fmt.Errorf("message is required")
}
client, err := h.getAgentCtlClient(req.SessionID)
if err != nil {
return nil, err
}
result, err := client.GitCommit(ctx, req.Message, req.StageAll)
if err != nil {
return nil, fmt.Errorf("commit failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
// wsStage handles worktree.stage action
func (h *GitHandlers) wsStage(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitStageRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
client, err := h.getAgentCtlClient(req.SessionID)
if err != nil {
return nil, err
}
result, err := client.GitStage(ctx, req.Paths)
if err != nil {
return nil, fmt.Errorf("stage failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
// wsCreatePR handles worktree.create_pr action
func (h *GitHandlers) wsCreatePR(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitCreatePRRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
if req.Title == "" {
return nil, fmt.Errorf("title is required")
}
client, err := h.getAgentCtlClient(req.SessionID)
if err != nil {
return nil, err
}
result, err := client.GitCreatePR(ctx, req.Title, req.Body, req.BaseBranch)
if err != nil {
return nil, fmt.Errorf("create PR failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
// wsCommitDiff handles session.commit_diff action
func (h *GitHandlers) wsCommitDiff(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
var req GitShowCommitRequest
if err := msg.ParsePayload(&req); err != nil {
return nil, fmt.Errorf("invalid payload: %w", err)
}
if req.SessionID == "" {
return nil, fmt.Errorf("session_id is required")
}
if req.CommitSHA == "" {
return nil, fmt.Errorf("commit_sha is required")
}
client, err := h.getAgentCtlClient(req.SessionID)
if err != nil {
return nil, err
}
result, err := client.GitShowCommit(ctx, req.CommitSHA)
if err != nil {
return nil, fmt.Errorf("show commit failed: %w", err)
}
return ws.NewResponse(msg.ID, msg.Action, result)
}
// getAgentCtlClient gets the agentctl client for a session
func (h *GitHandlers) getAgentCtlClient(sessionID string) (*client.Client, error) {
execution, ok := h.lifecycleMgr.GetExecutionBySessionID(sessionID)
if !ok {
return nil, fmt.Errorf("no agent running for session %s", sessionID)
}
c := execution.GetAgentCtlClient()
if c == nil {
return nil, fmt.Errorf("agent client not available for session %s", sessionID)
}
return c, nil
}