package api import ( "errors" "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/kandev/kandev/internal/agentctl/server/process" "go.uber.org/zap" ) // GitPullRequest for POST /api/v1/git/pull type GitPullRequest struct { Rebase bool `json:"rebase"` } // GitPushRequest for POST /api/v1/git/push type GitPushRequest struct { Force bool `json:"force"` SetUpstream bool `json:"set_upstream"` } // GitRebaseRequest for POST /api/v1/git/rebase type GitRebaseRequest struct { BaseBranch string `json:"base_branch"` } // GitMergeRequest for POST /api/v1/git/merge type GitMergeRequest struct { BaseBranch string `json:"base_branch"` } // GitAbortRequest for POST /api/v1/git/abort type GitAbortRequest struct { Operation string `json:"operation"` // "merge" or "rebase" } // GitCommitRequest for POST /api/v1/git/commit type GitCommitRequest struct { Message string `json:"message"` StageAll bool `json:"stage_all"` Amend bool `json:"amend"` } // GitRenameBranchRequest for POST /api/v1/git/rename-branch type GitRenameBranchRequest struct { NewName string `json:"new_name"` } // GitStageRequest for POST /api/v1/git/stage type GitStageRequest struct { Paths []string `json:"paths"` // Empty = stage all } // GitUnstageRequest for POST /api/v1/git/unstage type GitUnstageRequest struct { Paths []string `json:"paths"` // Empty = unstage all } // GitDiscardRequest for POST /api/v1/git/discard type GitDiscardRequest struct { Paths []string `json:"paths"` // Required - files to discard } // GitShowCommitRequest for GET /api/v1/git/commit/:sha type GitShowCommitRequest struct { CommitSHA string `uri:"sha" binding:"required"` } // GitRevertCommitRequest for POST /api/v1/git/revert-commit type GitRevertCommitRequest struct { CommitSHA string `json:"commit_sha"` } // GitCreatePRRequest for POST /api/v1/git/create-pr type GitCreatePRRequest struct { Title string `json:"title"` Body string `json:"body"` BaseBranch string `json:"base_branch"` Draft bool `json:"draft"` } // GitResetRequest for POST /api/v1/git/reset type GitResetRequest struct { CommitSHA string `json:"commit_sha"` Mode string `json:"mode"` // "soft", "mixed", or "hard" } // GitLogRequest for GET /api/v1/git/log type GitLogRequest struct { Since string `form:"since"` // Base commit SHA (exclusive) TargetBranch string `form:"target_branch"` // Target branch for merge-base calculation (e.g., "origin/main") Limit int `form:"limit"` // Max commits to return } // GitCumulativeDiffRequest for GET /api/v1/git/cumulative-diff type GitCumulativeDiffRequest struct { Base string `form:"base" binding:"required"` // Base commit SHA } // handleGitPull handles POST /api/v1/git/pull func (s *Server) handleGitPull(c *gin.Context) { var req GitPullRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "pull", Error: "invalid request: " + err.Error(), }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Pull(c.Request.Context(), req.Rebase) if err != nil { s.handleGitError(c, "pull", err) return } c.JSON(http.StatusOK, result) } // handleGitPush handles POST /api/v1/git/push func (s *Server) handleGitPush(c *gin.Context) { var req GitPushRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "push", Error: "invalid request: " + err.Error(), }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Push(c.Request.Context(), req.Force, req.SetUpstream) if err != nil { s.handleGitError(c, "push", err) return } c.JSON(http.StatusOK, result) } // handleGitRebase handles POST /api/v1/git/rebase func (s *Server) handleGitRebase(c *gin.Context) { var req GitRebaseRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "rebase", Error: "invalid request: " + err.Error(), }) return } if req.BaseBranch == "" { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "rebase", Error: "base_branch is required", }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Rebase(c.Request.Context(), req.BaseBranch) if err != nil { s.handleGitError(c, "rebase", err) return } c.JSON(http.StatusOK, result) } // handleGitMerge handles POST /api/v1/git/merge func (s *Server) handleGitMerge(c *gin.Context) { var req GitMergeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "merge", Error: "invalid request: " + err.Error(), }) return } if req.BaseBranch == "" { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "merge", Error: "base_branch is required", }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Merge(c.Request.Context(), req.BaseBranch) if err != nil { s.handleGitError(c, "merge", err) return } c.JSON(http.StatusOK, result) } // handleGitAbort handles POST /api/v1/git/abort func (s *Server) handleGitAbort(c *gin.Context) { var req GitAbortRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "abort", Error: "invalid request: " + err.Error(), }) return } if req.Operation != "merge" && req.Operation != "rebase" { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "abort", Error: "operation must be 'merge' or 'rebase'", }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Abort(c.Request.Context(), req.Operation) if err != nil { s.handleGitError(c, "abort", err) return } c.JSON(http.StatusOK, result) } // handleGitCommit handles POST /api/v1/git/commit func (s *Server) handleGitCommit(c *gin.Context) { var req GitCommitRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "commit", Error: "invalid request: " + err.Error(), }) return } if req.Message == "" { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "commit", Error: "message is required", }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Commit(c.Request.Context(), req.Message, req.StageAll, req.Amend) if err != nil { s.handleGitError(c, "commit", err) return } c.JSON(http.StatusOK, result) } // handleGitRenameBranch handles POST /api/v1/git/rename-branch func (s *Server) handleGitRenameBranch(c *gin.Context) { var req GitRenameBranchRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "rename_branch", Error: "invalid request: " + err.Error(), }) return } if req.NewName == "" { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "rename_branch", Error: "new_name is required", }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.RenameBranch(c.Request.Context(), req.NewName) if err != nil { s.handleGitError(c, "rename_branch", err) return } c.JSON(http.StatusOK, result) } // handleGitStage handles POST /api/v1/git/stage func (s *Server) handleGitStage(c *gin.Context) { var req GitStageRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "stage", Error: "invalid request: " + err.Error(), }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Stage(c.Request.Context(), req.Paths) if err != nil { s.handleGitError(c, "stage", err) return } c.JSON(http.StatusOK, result) } // handleGitUnstage handles POST /api/v1/git/unstage func (s *Server) handleGitUnstage(c *gin.Context) { var req GitUnstageRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "unstage", Error: "invalid request: " + err.Error(), }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Unstage(c.Request.Context(), req.Paths) if err != nil { s.handleGitError(c, "unstage", err) return } c.JSON(http.StatusOK, result) } // handleGitDiscard handles POST /api/v1/git/discard func (s *Server) handleGitDiscard(c *gin.Context) { var req GitDiscardRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "discard", Error: "invalid request: " + err.Error(), }) return } if len(req.Paths) == 0 { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "discard", Error: "paths are required", }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Discard(c.Request.Context(), req.Paths) if err != nil { s.handleGitError(c, "discard", err) return } c.JSON(http.StatusOK, result) } // handleGitCreatePR handles POST /api/v1/git/create-pr func (s *Server) handleGitCreatePR(c *gin.Context) { var req GitCreatePRRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.PRCreateResult{ Success: false, Error: "invalid request: " + err.Error(), }) return } if req.Title == "" { c.JSON(http.StatusBadRequest, process.PRCreateResult{ Success: false, Error: "title is required", }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.CreatePR(c.Request.Context(), req.Title, req.Body, req.BaseBranch, req.Draft) if err != nil { if errors.Is(err, process.ErrOperationInProgress) { c.JSON(http.StatusConflict, process.PRCreateResult{ Success: false, Error: "another git operation is already in progress", }) return } s.logger.Error("git create-pr failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, process.PRCreateResult{ Success: false, Error: err.Error(), }) return } c.JSON(http.StatusOK, result) } // handleGitRevertCommit handles POST /api/v1/git/revert-commit func (s *Server) handleGitRevertCommit(c *gin.Context) { var req GitRevertCommitRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "revert_commit", Error: "invalid request: " + err.Error(), }) return } if req.CommitSHA == "" { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "revert_commit", Error: "commit_sha is required", }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.RevertCommit(c.Request.Context(), req.CommitSHA) if err != nil { s.handleGitError(c, "revert_commit", err) return } c.JSON(http.StatusOK, result) } // handleGitShowCommit handles GET /api/v1/git/commit/:sha func (s *Server) handleGitShowCommit(c *gin.Context) { var req GitShowCommitRequest if err := c.ShouldBindUri(&req); err != nil { c.JSON(http.StatusBadRequest, process.CommitDiffResult{ Success: false, Error: "invalid request: " + err.Error(), }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.ShowCommit(c.Request.Context(), req.CommitSHA) if err != nil { s.logger.Error("git show commit failed", zap.String("commit_sha", req.CommitSHA), zap.Error(err)) c.JSON(http.StatusInternalServerError, process.CommitDiffResult{ Success: false, CommitSHA: req.CommitSHA, Error: err.Error(), }) return } c.JSON(http.StatusOK, result) } // handleGitReset handles POST /api/v1/git/reset func (s *Server) handleGitReset(c *gin.Context) { var req GitResetRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "reset", Error: "invalid request: " + err.Error(), }) return } if req.CommitSHA == "" { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "reset", Error: "commit_sha is required", }) return } if req.Mode == "" { req.Mode = "mixed" } validModes := map[string]bool{"soft": true, "mixed": true, "hard": true} if !validModes[req.Mode] { c.JSON(http.StatusBadRequest, process.GitOperationResult{ Success: false, Operation: "reset", Error: fmt.Sprintf("invalid reset mode: %s (must be soft, mixed, or hard)", req.Mode), }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.Reset(c.Request.Context(), req.CommitSHA, req.Mode) if err != nil { s.handleGitError(c, "reset", err) return } c.JSON(http.StatusOK, result) } // handleGitLog handles GET /api/v1/git/log func (s *Server) handleGitLog(c *gin.Context) { var req GitLogRequest if err := c.ShouldBindQuery(&req); err != nil { c.JSON(http.StatusBadRequest, process.GitLogResult{ Success: false, Error: "invalid request: " + err.Error(), }) return } // Bound limit to prevent expensive history scans const ( defaultLogLimit = 100 maxLogLimit = 500 ) limit := req.Limit if limit <= 0 { limit = defaultLogLimit } else if limit > maxLogLimit { c.JSON(http.StatusBadRequest, process.GitLogResult{ Success: false, Error: fmt.Sprintf("limit must be between 1 and %d", maxLogLimit), }) return } gitOp := s.procMgr.GitOperator() // If target_branch is provided, compute merge-base dynamically. // This ensures we only show commits not yet merged into the target branch, // which is accurate even after rebases. baseCommit := req.Since if req.TargetBranch != "" { mergeBase, err := gitOp.GetMergeBase(c.Request.Context(), "HEAD", req.TargetBranch) if err != nil { s.logger.Warn("failed to compute merge-base, falling back to since", zap.String("target_branch", req.TargetBranch), zap.Error(err)) // Fall back to the provided base commit } else if mergeBase != "" { baseCommit = mergeBase } } result, err := gitOp.GetLog(c.Request.Context(), baseCommit, limit) if err != nil { s.logger.Error("git log failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, process.GitLogResult{ Success: false, Error: err.Error(), }) return } c.JSON(http.StatusOK, result) } // handleGitCumulativeDiff handles GET /api/v1/git/cumulative-diff func (s *Server) handleGitCumulativeDiff(c *gin.Context) { var req GitCumulativeDiffRequest if err := c.ShouldBindQuery(&req); err != nil { c.JSON(http.StatusBadRequest, process.CumulativeDiffResult{ Success: false, Error: "invalid request: " + err.Error(), }) return } gitOp := s.procMgr.GitOperator() result, err := gitOp.GetCumulativeDiff(c.Request.Context(), req.Base) if err != nil { s.logger.Error("git cumulative diff failed", zap.String("base", req.Base), zap.Error(err)) c.JSON(http.StatusInternalServerError, process.CumulativeDiffResult{ Success: false, Error: err.Error(), }) return } c.JSON(http.StatusOK, result) } // GitStatusResult represents the result of a git status query. type GitStatusResult struct { Success bool `json:"success"` Branch string `json:"branch"` RemoteBranch string `json:"remote_branch"` HeadCommit string `json:"head_commit"` BaseCommit string `json:"base_commit"` // Merge-base with origin branch Ahead int `json:"ahead"` Behind int `json:"behind"` Modified []string `json:"modified"` Added []string `json:"added"` Deleted []string `json:"deleted"` Untracked []string `json:"untracked"` Renamed []string `json:"renamed"` Files map[string]interface{} `json:"files"` Timestamp string `json:"timestamp"` BranchAdditions int `json:"branch_additions,omitempty"` BranchDeletions int `json:"branch_deletions,omitempty"` Error string `json:"error,omitempty"` } // handleGitStatus handles GET /api/v1/git/status func (s *Server) handleGitStatus(c *gin.Context) { wt := s.procMgr.GetWorkspaceTracker() if wt == nil { c.JSON(http.StatusInternalServerError, GitStatusResult{ Success: false, Error: "workspace tracker not available", }) return } status, err := wt.GetCurrentGitStatus(c.Request.Context()) if err != nil { s.logger.Error("git status failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, GitStatusResult{ Success: false, Error: err.Error(), }) return } // Convert Files map to interface{} map for JSON serialization filesMap := make(map[string]interface{}, len(status.Files)) for k, v := range status.Files { filesMap[k] = v } c.JSON(http.StatusOK, GitStatusResult{ Success: true, Branch: status.Branch, RemoteBranch: status.RemoteBranch, HeadCommit: status.HeadCommit, BaseCommit: status.BaseCommit, Ahead: status.Ahead, Behind: status.Behind, Modified: status.Modified, Added: status.Added, Deleted: status.Deleted, Untracked: status.Untracked, Renamed: status.Renamed, Files: filesMap, Timestamp: status.Timestamp.Format("2006-01-02T15:04:05.000Z07:00"), BranchAdditions: status.BranchAdditions, BranchDeletions: status.BranchDeletions, }) } // handleGitError handles errors from git operations. func (s *Server) handleGitError(c *gin.Context, operation string, err error) { if errors.Is(err, process.ErrOperationInProgress) { c.JSON(http.StatusConflict, process.GitOperationResult{ Success: false, Operation: operation, Error: "another git operation is already in progress", }) return } s.logger.Error("git operation failed", zap.String("operation", operation), zap.Error(err)) c.JSON(http.StatusInternalServerError, process.GitOperationResult{ Success: false, Operation: operation, Error: err.Error(), }) }