Files
kandev/apps/backend/internal/agentctl/server/api/git.go
T

668 lines
17 KiB
Go

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)
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()
result, err := gitOp.GetLog(c.Request.Context(), req.Since, 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(),
})
}