9e8079de69
Bundles a string of multi-repo fixes and refactors that came up while testing the feature end-to-end: - workspace_path stored on multi-repo TaskEnvironment was one level too high, so agentctl booted with the wrong WorkDir on cold start; fixed in resolveTaskEnvWorkspacePath + GetWorkspaceInfoForSession. - Stage/Unstage/Discard/Commit/Revert/Reset/Amend now thread `repo` through agentctl client → WS handler → frontend hook → SessionGit fan-out, so the right git repo is targeted (was running at the task root, which isn't a git repo for multi-repo tasks). - agentctl GitLog is multi-repo aware: when no `repo` is specified and per-repo trackers exist, fans out one log per repo and merges sorted newest-first with RepositoryName stamped on each commit. - Per-repo grouping for files (Changes panel) and commits, with collapsible group headers; commits section defaults to collapsed. Algorithm extracted into one generic groupByRepositoryName helper (was copy-pasted three times). - Task creation dialog rewritten around a unified `fs.repositories[]` list — no more "primary vs extras" split. Chip-style row with one pill per repo + branch, "+" to add, x to remove, GitHub URL toggle inline. Discovered (on-machine) repos appear in any chip's dropdown. Removes ~6 fields and 3 helpers from the dialog state. - Branch listing endpoints collapsed to one: GET /workspaces/:id/branches?repository_id=X | path=Y Frontend collapses to a single useBranches hook with a tagged-union source. Drops two HTTP routes, two service methods, two API client funcs, and one whole hook file. Tests added across the chain: workspace_path resolver, sort-commits- by-date, manager poll-mode propagation, RepoSubpaths, chip grouping + collapse + per-repo isLatest, group-by-repo helper, dialog payload builder + chip wiring + discovered-repo flow.
484 lines
19 KiB
Go
484 lines
19 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/task/dto"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
"github.com/kandev/kandev/internal/task/service"
|
|
ws "github.com/kandev/kandev/pkg/websocket"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type RepositoryHandlers struct {
|
|
service *service.Service
|
|
logger *logger.Logger
|
|
}
|
|
|
|
func NewRepositoryHandlers(svc *service.Service, log *logger.Logger) *RepositoryHandlers {
|
|
return &RepositoryHandlers{
|
|
service: svc,
|
|
logger: log.WithFields(zap.String("component", "task-repository-handlers")),
|
|
}
|
|
}
|
|
|
|
func RegisterRepositoryRoutes(router *gin.Engine, dispatcher *ws.Dispatcher, svc *service.Service, log *logger.Logger) {
|
|
handlers := NewRepositoryHandlers(svc, log)
|
|
handlers.registerHTTP(router)
|
|
handlers.registerWS(dispatcher)
|
|
}
|
|
|
|
func (h *RepositoryHandlers) registerHTTP(router *gin.Engine) {
|
|
api := router.Group("/api/v1")
|
|
api.GET("/workspaces/:id/repositories", h.httpListRepositories)
|
|
api.POST("/workspaces/:id/repositories", h.httpCreateRepository)
|
|
api.GET("/workspaces/:id/repositories/discover", h.httpDiscoverRepositories)
|
|
// Unified branch listing — accepts either ?repository_id= for an imported
|
|
// workspace repo, or ?path= for an on-machine folder discovered but not
|
|
// yet imported. Both paths bottom out in `listGitBranches`; the only
|
|
// difference is how the absolute path is resolved.
|
|
api.GET("/workspaces/:id/branches", h.httpListBranches)
|
|
api.GET("/workspaces/:id/repositories/validate", h.httpValidateRepositoryPath)
|
|
api.GET("/repositories/:id", h.httpGetRepository)
|
|
api.PATCH("/repositories/:id", h.httpUpdateRepository)
|
|
api.DELETE("/repositories/:id", h.httpDeleteRepository)
|
|
api.GET("/repositories/:id/scripts", h.httpListRepositoryScripts)
|
|
api.POST("/repositories/:id/scripts", h.httpCreateRepositoryScript)
|
|
api.GET("/scripts/:id", h.httpGetRepositoryScript)
|
|
api.PUT("/scripts/:id", h.httpUpdateRepositoryScript)
|
|
api.DELETE("/scripts/:id", h.httpDeleteRepositoryScript)
|
|
}
|
|
|
|
func (h *RepositoryHandlers) registerWS(dispatcher *ws.Dispatcher) {
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryList, h.wsListRepositories)
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryCreate, h.wsCreateRepository)
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryGet, h.wsGetRepository)
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryUpdate, h.wsUpdateRepository)
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryDelete, h.wsDeleteRepository)
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryScriptList, h.wsListRepositoryScripts)
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryScriptCreate, h.wsCreateRepositoryScript)
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryScriptGet, h.wsGetRepositoryScript)
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryScriptUpdate, h.wsUpdateRepositoryScript)
|
|
dispatcher.RegisterFunc(ws.ActionRepositoryScriptDelete, h.wsDeleteRepositoryScript)
|
|
}
|
|
|
|
// HTTP handlers
|
|
|
|
func (h *RepositoryHandlers) httpListRepositories(c *gin.Context) {
|
|
workspaceID := c.Param("id")
|
|
includeScripts := c.Query("include_scripts") == queryValueTrue
|
|
|
|
repositories, err := h.service.ListRepositories(c.Request.Context(), workspaceID)
|
|
if err != nil {
|
|
h.logger.Error("failed to list repositories", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list repositories"})
|
|
return
|
|
}
|
|
|
|
resp := dto.ListRepositoriesResponse{
|
|
Repositories: make([]dto.RepositoryDTO, 0, len(repositories)),
|
|
Total: len(repositories),
|
|
}
|
|
|
|
var scriptsByRepo map[string][]*models.RepositoryScript
|
|
if includeScripts && len(repositories) > 0 {
|
|
repoIDs := make([]string, len(repositories))
|
|
for i, r := range repositories {
|
|
repoIDs[i] = r.ID
|
|
}
|
|
scriptsByRepo, _ = h.service.ListScriptsByRepositoryIDs(c.Request.Context(), repoIDs)
|
|
}
|
|
|
|
for _, repository := range repositories {
|
|
repoDTO := dto.FromRepository(repository)
|
|
if scripts, ok := scriptsByRepo[repository.ID]; ok {
|
|
repoDTO.Scripts = make([]dto.RepositoryScriptDTO, 0, len(scripts))
|
|
for _, script := range scripts {
|
|
repoDTO.Scripts = append(repoDTO.Scripts, dto.FromRepositoryScript(script))
|
|
}
|
|
}
|
|
resp.Repositories = append(resp.Repositories, repoDTO)
|
|
}
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
func (h *RepositoryHandlers) httpDiscoverRepositories(c *gin.Context) {
|
|
root := c.Query("root")
|
|
result, err := h.service.DiscoverLocalRepositories(c.Request.Context(), root)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrPathNotAllowed) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "root is not within allowed paths"})
|
|
return
|
|
}
|
|
h.logger.Error("failed to discover repositories", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to discover repositories"})
|
|
return
|
|
}
|
|
|
|
resp := dto.RepositoryDiscoveryResponse{
|
|
Roots: result.Roots,
|
|
Repositories: make([]dto.LocalRepositoryDTO, 0, len(result.Repositories)),
|
|
Total: len(result.Repositories),
|
|
}
|
|
for _, repo := range result.Repositories {
|
|
resp.Repositories = append(resp.Repositories, dto.FromLocalRepository(repo))
|
|
}
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
func (h *RepositoryHandlers) httpValidateRepositoryPath(c *gin.Context) {
|
|
path := c.Query("path")
|
|
if path == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "path is required"})
|
|
return
|
|
}
|
|
result, err := h.service.ValidateLocalRepositoryPath(c.Request.Context(), path)
|
|
if err != nil {
|
|
h.logger.Error("failed to validate repository path", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to validate repository path"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, dto.RepositoryPathValidationResponse{
|
|
Path: result.Path,
|
|
Exists: result.Exists,
|
|
IsGitRepo: result.IsGitRepo,
|
|
Allowed: result.Allowed,
|
|
DefaultBranch: result.DefaultBranch,
|
|
Message: result.Message,
|
|
})
|
|
}
|
|
|
|
// httpListBranches handles the unified branch endpoint:
|
|
//
|
|
// GET /api/v1/workspaces/:id/branches?repository_id=X
|
|
// GET /api/v1/workspaces/:id/branches?path=/abs/path
|
|
//
|
|
// Exactly one of the two query params must be set. Both bottom out in the
|
|
// same `listGitBranches` call; the difference is just where the absolute
|
|
// path is resolved from (DB row vs request param).
|
|
func (h *RepositoryHandlers) httpListBranches(c *gin.Context) {
|
|
repoID := c.Query("repository_id")
|
|
path := c.Query("path")
|
|
if repoID == "" && path == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "repository_id or path is required"})
|
|
return
|
|
}
|
|
if repoID != "" && path != "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "specify only one of repository_id or path"})
|
|
return
|
|
}
|
|
branches, err := h.service.ListBranches(c.Request.Context(), repoID, path)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrPathNotAllowed) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "path is not within allowed roots"})
|
|
return
|
|
}
|
|
h.logger.Error("failed to list branches", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list branches"})
|
|
return
|
|
}
|
|
dtoBranches := make([]dto.BranchDTO, len(branches))
|
|
for i, branch := range branches {
|
|
dtoBranches[i] = dto.FromBranch(branch)
|
|
}
|
|
c.JSON(http.StatusOK, dto.RepositoryBranchesResponse{
|
|
Branches: dtoBranches,
|
|
Total: len(dtoBranches),
|
|
})
|
|
}
|
|
|
|
type httpCreateRepositoryRequest struct {
|
|
Name string `json:"name"`
|
|
SourceType string `json:"source_type"`
|
|
LocalPath string `json:"local_path"`
|
|
Provider string `json:"provider"`
|
|
ProviderRepoID string `json:"provider_repo_id"`
|
|
ProviderOwner string `json:"provider_owner"`
|
|
ProviderName string `json:"provider_name"`
|
|
DefaultBranch string `json:"default_branch"`
|
|
WorktreeBranchPrefix string `json:"worktree_branch_prefix"`
|
|
PullBeforeWorktree *bool `json:"pull_before_worktree"`
|
|
SetupScript string `json:"setup_script"`
|
|
CleanupScript string `json:"cleanup_script"`
|
|
DevScript string `json:"dev_script"`
|
|
}
|
|
|
|
func (h *RepositoryHandlers) httpCreateRepository(c *gin.Context) {
|
|
var body httpCreateRepositoryRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
if body.Name == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
|
return
|
|
}
|
|
repository, err := h.service.CreateRepository(c.Request.Context(), &service.CreateRepositoryRequest{
|
|
WorkspaceID: c.Param("id"),
|
|
Name: body.Name,
|
|
SourceType: body.SourceType,
|
|
LocalPath: body.LocalPath,
|
|
Provider: body.Provider,
|
|
ProviderRepoID: body.ProviderRepoID,
|
|
ProviderOwner: body.ProviderOwner,
|
|
ProviderName: body.ProviderName,
|
|
DefaultBranch: body.DefaultBranch,
|
|
WorktreeBranchPrefix: body.WorktreeBranchPrefix,
|
|
PullBeforeWorktree: body.PullBeforeWorktree,
|
|
SetupScript: body.SetupScript,
|
|
CleanupScript: body.CleanupScript,
|
|
DevScript: body.DevScript,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrInvalidRepositorySettings) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid repository settings"})
|
|
return
|
|
}
|
|
h.logger.Error("failed to create repository", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create repository"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, dto.FromRepository(repository))
|
|
}
|
|
|
|
func (h *RepositoryHandlers) httpGetRepository(c *gin.Context) {
|
|
repository, err := h.service.GetRepository(c.Request.Context(), c.Param("id"))
|
|
if err != nil {
|
|
handleNotFound(c, h.logger, err, "repository not found")
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, dto.FromRepository(repository))
|
|
}
|
|
|
|
|
|
type httpUpdateRepositoryRequest struct {
|
|
Name *string `json:"name"`
|
|
SourceType *string `json:"source_type"`
|
|
LocalPath *string `json:"local_path"`
|
|
Provider *string `json:"provider"`
|
|
ProviderRepoID *string `json:"provider_repo_id"`
|
|
ProviderOwner *string `json:"provider_owner"`
|
|
ProviderName *string `json:"provider_name"`
|
|
DefaultBranch *string `json:"default_branch"`
|
|
WorktreeBranchPrefix *string `json:"worktree_branch_prefix"`
|
|
PullBeforeWorktree *bool `json:"pull_before_worktree"`
|
|
SetupScript *string `json:"setup_script"`
|
|
CleanupScript *string `json:"cleanup_script"`
|
|
DevScript *string `json:"dev_script"`
|
|
}
|
|
|
|
func (h *RepositoryHandlers) httpUpdateRepository(c *gin.Context) {
|
|
var body httpUpdateRepositoryRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
repository, err := h.service.UpdateRepository(c.Request.Context(), c.Param("id"), &service.UpdateRepositoryRequest{
|
|
Name: body.Name,
|
|
SourceType: body.SourceType,
|
|
LocalPath: body.LocalPath,
|
|
Provider: body.Provider,
|
|
ProviderRepoID: body.ProviderRepoID,
|
|
ProviderOwner: body.ProviderOwner,
|
|
ProviderName: body.ProviderName,
|
|
DefaultBranch: body.DefaultBranch,
|
|
WorktreeBranchPrefix: body.WorktreeBranchPrefix,
|
|
PullBeforeWorktree: body.PullBeforeWorktree,
|
|
SetupScript: body.SetupScript,
|
|
CleanupScript: body.CleanupScript,
|
|
DevScript: body.DevScript,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrInvalidRepositorySettings) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid repository settings"})
|
|
return
|
|
}
|
|
handleNotFound(c, h.logger, err, "repository not found")
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, dto.FromRepository(repository))
|
|
}
|
|
|
|
func (h *RepositoryHandlers) httpDeleteRepository(c *gin.Context) {
|
|
if err := h.service.DeleteRepository(c.Request.Context(), c.Param("id")); err != nil {
|
|
if errors.Is(err, service.ErrActiveTaskSessions) {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "repository is used by an active agent session"})
|
|
return
|
|
}
|
|
handleNotFound(c, h.logger, err, "repository not found")
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// WS handlers
|
|
|
|
func reposToListResponse(repositories []*models.Repository) dto.ListRepositoriesResponse {
|
|
resp := dto.ListRepositoriesResponse{
|
|
Repositories: make([]dto.RepositoryDTO, 0, len(repositories)),
|
|
Total: len(repositories),
|
|
}
|
|
for _, repository := range repositories {
|
|
resp.Repositories = append(resp.Repositories, dto.FromRepository(repository))
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func (h *RepositoryHandlers) wsListRepositories(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
|
var req struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
}
|
|
if err := msg.ParsePayload(&req); err != nil {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
|
}
|
|
repositories, err := h.service.ListRepositories(ctx, req.WorkspaceID)
|
|
if err != nil {
|
|
h.logger.Error("failed to list repositories", zap.Error(err))
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list repositories", nil)
|
|
}
|
|
return ws.NewResponse(msg.ID, msg.Action, reposToListResponse(repositories))
|
|
}
|
|
|
|
type wsCreateRepositoryRequest struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
SourceType string `json:"source_type"`
|
|
LocalPath string `json:"local_path"`
|
|
Provider string `json:"provider"`
|
|
ProviderRepoID string `json:"provider_repo_id"`
|
|
ProviderOwner string `json:"provider_owner"`
|
|
ProviderName string `json:"provider_name"`
|
|
DefaultBranch string `json:"default_branch"`
|
|
WorktreeBranchPrefix string `json:"worktree_branch_prefix"`
|
|
SetupScript string `json:"setup_script"`
|
|
CleanupScript string `json:"cleanup_script"`
|
|
DevScript string `json:"dev_script"`
|
|
}
|
|
|
|
func (h *RepositoryHandlers) wsCreateRepository(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
|
var req wsCreateRepositoryRequest
|
|
if err := msg.ParsePayload(&req); err != nil {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
|
}
|
|
if req.WorkspaceID == "" || req.Name == "" {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "workspace_id and name are required", nil)
|
|
}
|
|
repository, err := h.service.CreateRepository(ctx, &service.CreateRepositoryRequest{
|
|
WorkspaceID: req.WorkspaceID,
|
|
Name: req.Name,
|
|
SourceType: req.SourceType,
|
|
LocalPath: req.LocalPath,
|
|
Provider: req.Provider,
|
|
ProviderRepoID: req.ProviderRepoID,
|
|
ProviderOwner: req.ProviderOwner,
|
|
ProviderName: req.ProviderName,
|
|
DefaultBranch: req.DefaultBranch,
|
|
WorktreeBranchPrefix: req.WorktreeBranchPrefix,
|
|
SetupScript: req.SetupScript,
|
|
CleanupScript: req.CleanupScript,
|
|
DevScript: req.DevScript,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrInvalidRepositorySettings) {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "Invalid repository settings", nil)
|
|
}
|
|
h.logger.Error("failed to create repository", zap.Error(err))
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to create repository", nil)
|
|
}
|
|
return ws.NewResponse(msg.ID, msg.Action, dto.FromRepository(repository))
|
|
}
|
|
|
|
type wsGetRepositoryRequest struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
func (h *RepositoryHandlers) wsGetRepository(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
|
var req wsGetRepositoryRequest
|
|
if err := msg.ParsePayload(&req); err != nil {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
|
}
|
|
if req.ID == "" {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil)
|
|
}
|
|
repository, err := h.service.GetRepository(ctx, req.ID)
|
|
if err != nil {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Repository not found", nil)
|
|
}
|
|
return ws.NewResponse(msg.ID, msg.Action, dto.FromRepository(repository))
|
|
}
|
|
|
|
type wsUpdateRepositoryRequest struct {
|
|
ID string `json:"id"`
|
|
Name *string `json:"name,omitempty"`
|
|
SourceType *string `json:"source_type,omitempty"`
|
|
LocalPath *string `json:"local_path,omitempty"`
|
|
Provider *string `json:"provider,omitempty"`
|
|
ProviderRepoID *string `json:"provider_repo_id,omitempty"`
|
|
ProviderOwner *string `json:"provider_owner,omitempty"`
|
|
ProviderName *string `json:"provider_name,omitempty"`
|
|
DefaultBranch *string `json:"default_branch,omitempty"`
|
|
WorktreeBranchPrefix *string `json:"worktree_branch_prefix,omitempty"`
|
|
SetupScript *string `json:"setup_script,omitempty"`
|
|
CleanupScript *string `json:"cleanup_script,omitempty"`
|
|
DevScript *string `json:"dev_script,omitempty"`
|
|
}
|
|
|
|
func (h *RepositoryHandlers) wsUpdateRepository(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
|
var req wsUpdateRepositoryRequest
|
|
if err := msg.ParsePayload(&req); err != nil {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
|
}
|
|
if req.ID == "" {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil)
|
|
}
|
|
repository, err := h.service.UpdateRepository(ctx, req.ID, &service.UpdateRepositoryRequest{
|
|
Name: req.Name,
|
|
SourceType: req.SourceType,
|
|
LocalPath: req.LocalPath,
|
|
Provider: req.Provider,
|
|
ProviderRepoID: req.ProviderRepoID,
|
|
ProviderOwner: req.ProviderOwner,
|
|
ProviderName: req.ProviderName,
|
|
DefaultBranch: req.DefaultBranch,
|
|
WorktreeBranchPrefix: req.WorktreeBranchPrefix,
|
|
SetupScript: req.SetupScript,
|
|
CleanupScript: req.CleanupScript,
|
|
DevScript: req.DevScript,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrInvalidRepositorySettings) {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "Invalid repository settings", nil)
|
|
}
|
|
h.logger.Error("failed to update repository", zap.Error(err))
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update repository", nil)
|
|
}
|
|
return ws.NewResponse(msg.ID, msg.Action, dto.FromRepository(repository))
|
|
}
|
|
|
|
type wsDeleteRepositoryRequest struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
func (h *RepositoryHandlers) wsDeleteRepository(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
|
var req wsDeleteRepositoryRequest
|
|
if err := msg.ParsePayload(&req); err != nil {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
|
}
|
|
if req.ID == "" {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil)
|
|
}
|
|
if err := h.service.DeleteRepository(ctx, req.ID); err != nil {
|
|
h.logger.Error("failed to delete repository", zap.Error(err))
|
|
if errors.Is(err, service.ErrActiveTaskSessions) {
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "repository is used by an active agent session", nil)
|
|
}
|
|
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Repository not found", nil)
|
|
}
|
|
return ws.NewResponse(msg.ID, msg.Action, gin.H{"deleted": true})
|
|
}
|