package handlers import ( "encoding/json" "fmt" "net/http" "strconv" "git.gobha.me/xcaliber/chat-switchboard/crypto" "git.gobha.me/xcaliber/chat-switchboard/models" "git.gobha.me/xcaliber/chat-switchboard/store" "git.gobha.me/xcaliber/chat-switchboard/workspace" "github.com/gin-gonic/gin" ) // ========================================= // GIT HANDLER — Git operations on workspaces // ========================================= type GitHandler struct { stores store.Stores gitOps *workspace.GitOps } func NewGitHandler(s store.Stores, gitOps *workspace.GitOps) *GitHandler { return &GitHandler{stores: s, gitOps: gitOps} } // ── Clone ──────────────────────────────────── func (h *GitHandler) Clone(c *gin.Context) { wsID := c.Param("id") var req struct { URL string `json:"url" binding:"required"` Branch string `json:"branch"` CredentialID string `json:"credential_id"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } w, err := h.stores.Workspaces.GetByID(c.Request.Context(), wsID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "workspace not found"}) return } if err := h.gitOps.Clone(c.Request.Context(), w, req.URL, req.Branch, req.CredentialID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "cloned"}) } // ── Pull ───────────────────────────────────── func (h *GitHandler) Pull(c *gin.Context) { w, err := h.loadWorkspace(c) if err != nil { return } if err := h.gitOps.Pull(c.Request.Context(), w); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "pulled"}) } // ── Push ───────────────────────────────────── func (h *GitHandler) Push(c *gin.Context) { w, err := h.loadWorkspace(c) if err != nil { return } if err := h.gitOps.Push(c.Request.Context(), w); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "pushed"}) } // ── Status ─────────────────────────────────── func (h *GitHandler) Status(c *gin.Context) { w, err := h.loadWorkspace(c) if err != nil { return } status, err := h.gitOps.Status(c.Request.Context(), w) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, status) } // ── Diff ───────────────────────────────────── func (h *GitHandler) Diff(c *gin.Context) { w, err := h.loadWorkspace(c) if err != nil { return } path := c.Query("path") diff, err := h.gitOps.Diff(c.Request.Context(), w, path) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"diff": diff}) } // ── Commit ─────────────────────────────────── func (h *GitHandler) Commit(c *gin.Context) { w, err := h.loadWorkspace(c) if err != nil { return } var req struct { Message string `json:"message" binding:"required"` Paths []string `json:"paths"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.gitOps.Commit(c.Request.Context(), w, req.Message, req.Paths); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "committed"}) } // ── Log ────────────────────────────────────── func (h *GitHandler) Log(c *gin.Context) { w, err := h.loadWorkspace(c) if err != nil { return } n := 20 if v := c.Query("n"); v != "" { if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 { n = parsed if n > 100 { n = 100 } } } entries, err := h.gitOps.Log(c.Request.Context(), w, n) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, entries) } // ── Branches ───────────────────────────────── func (h *GitHandler) Branches(c *gin.Context) { w, err := h.loadWorkspace(c) if err != nil { return } branches, current, err := h.gitOps.BranchList(c.Request.Context(), w) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "current": current, "branches": branches, }) } // ── Checkout ───────────────────────────────── func (h *GitHandler) Checkout(c *gin.Context) { w, err := h.loadWorkspace(c) if err != nil { return } var req struct { Branch string `json:"branch" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.gitOps.Checkout(c.Request.Context(), w, req.Branch); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "checked out", "branch": req.Branch}) } // loadWorkspace loads the workspace and validates git is configured. func (h *GitHandler) loadWorkspace(c *gin.Context) (*models.Workspace, error) { wsID := c.Param("id") w, err := h.stores.Workspaces.GetByID(c.Request.Context(), wsID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "workspace not found"}) return nil, err } if w.GitRemoteURL == nil || *w.GitRemoteURL == "" { err := fmt.Errorf("workspace has no git remote configured") c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return nil, err } return w, nil } // ========================================= // GIT CREDENTIAL HANDLER — CRUD for encrypted git credentials // ========================================= type GitCredentialHandler struct { stores store.Stores vault *crypto.KeyResolver } func NewGitCredentialHandler(s store.Stores, vault *crypto.KeyResolver) *GitCredentialHandler { return &GitCredentialHandler{stores: s, vault: vault} } // Create stores a new encrypted git credential. func (h *GitCredentialHandler) Create(c *gin.Context) { userID := getUserID(c) var req struct { Name string `json:"name" binding:"required"` AuthType string `json:"auth_type" binding:"required"` // Raw credential data — encrypted before storage Token string `json:"token,omitempty"` // for https_pat Username string `json:"username,omitempty"` // for https_basic Password string `json:"password,omitempty"` // for https_basic PrivateKey string `json:"private_key,omitempty"` // for ssh_key Passphrase string `json:"passphrase,omitempty"` // for ssh_key } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate auth type switch req.AuthType { case "https_pat": if req.Token == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "token is required for https_pat"}) return } case "https_basic": if req.Username == "" || req.Password == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "username and password required for https_basic"}) return } case "ssh_key": if req.PrivateKey == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "private_key is required for ssh_key"}) return } default: c.JSON(http.StatusBadRequest, gin.H{"error": "auth_type must be https_pat, https_basic, or ssh_key"}) return } // Build plaintext JSON var credData map[string]string switch req.AuthType { case "https_pat": credData = map[string]string{"token": req.Token} case "https_basic": credData = map[string]string{"username": req.Username, "password": req.Password} case "ssh_key": credData = map[string]string{"private_key": req.PrivateKey, "passphrase": req.Passphrase} } plaintext, _ := json.Marshal(credData) // Encrypt using global scope (same vault pattern as BYOK) ciphertext, nonce, err := h.vault.EncryptForScope(string(plaintext), "global", userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "encryption failed"}) return } cred := &models.GitCredential{ UserID: userID, Name: req.Name, AuthType: req.AuthType, EncryptedData: ciphertext, Nonce: nonce, } if err := h.stores.GitCredentials.Create(c.Request.Context(), cred); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, cred.Summary()) } // List returns all git credentials for the current user (summaries only). func (h *GitCredentialHandler) List(c *gin.Context) { userID := getUserID(c) creds, err := h.stores.GitCredentials.ListByUser(c.Request.Context(), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } summaries := make([]models.GitCredentialSummary, 0, len(creds)) for _, cred := range creds { summaries = append(summaries, cred.Summary()) } c.JSON(http.StatusOK, summaries) } // Delete removes a git credential. func (h *GitCredentialHandler) Delete(c *gin.Context) { userID := getUserID(c) credID := c.Param("id") if err := h.stores.GitCredentials.Delete(c.Request.Context(), credID, userID); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "credential not found"}) return } c.JSON(http.StatusOK, gin.H{"deleted": true}) }