Changeset 0.21.4 (#90)
This commit is contained in:
333
server/handlers/git.go
Normal file
333
server/handlers/git.go
Normal file
@@ -0,0 +1,333 @@
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user