331 lines
9.5 KiB
Go
331 lines
9.5 KiB
Go
package azuredevops
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
)
|
|
|
|
const notConfiguredCode = "azure_devops_not_configured"
|
|
|
|
type Controller struct {
|
|
service *Service
|
|
log *logger.Logger
|
|
}
|
|
|
|
func RegisterRoutes(router *gin.Engine, service *Service, log *logger.Logger) {
|
|
controller := &Controller{service: service, log: log}
|
|
api := router.Group("/api/v1/azure-devops")
|
|
api.GET("/config", controller.getConfig)
|
|
api.POST("/config", controller.setConfig)
|
|
api.DELETE("/config", controller.deleteConfig)
|
|
api.POST("/config/test", controller.testConfig)
|
|
api.POST("/config/copy", controller.copyConfig)
|
|
api.GET("/views", controller.getSavedViews)
|
|
api.PUT("/views", controller.setSavedViews)
|
|
api.GET("/projects", controller.listProjects)
|
|
api.GET("/repositories", controller.listRepositories)
|
|
api.GET("/branches", controller.listBranches)
|
|
api.POST("/work-items/search", controller.searchWorkItems)
|
|
api.GET("/work-items/:id", controller.getWorkItem)
|
|
api.GET("/pull-requests", controller.listPullRequests)
|
|
api.GET("/pull-requests/:projectId/:repositoryId/:pullRequestId", controller.getPullRequest)
|
|
api.GET("/pull-requests/:projectId/:repositoryId/:pullRequestId/feedback", controller.getPullRequestFeedback)
|
|
api.GET("/workspaces/:workspaceId/task-prs", controller.listWorkspaceTaskPRs)
|
|
api.POST("/tasks/:taskId/pull-requests", controller.associateTaskPR)
|
|
api.POST("/tasks/:taskId/pull-requests/sync", controller.syncTaskPR)
|
|
}
|
|
|
|
func (c *Controller) getSavedViews(ctx *gin.Context) {
|
|
views, err := c.service.GetSavedViewsForWorkspace(ctx.Request.Context(), workspaceID(ctx))
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{"views": views})
|
|
}
|
|
|
|
func (c *Controller) setSavedViews(ctx *gin.Context) {
|
|
var request struct {
|
|
Views []SavedView `json:"views"`
|
|
}
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
|
return
|
|
}
|
|
views, err := c.service.SetSavedViewsForWorkspace(
|
|
ctx.Request.Context(), workspaceID(ctx), request.Views,
|
|
)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{"views": views})
|
|
}
|
|
|
|
func (c *Controller) getConfig(ctx *gin.Context) {
|
|
cfg, err := c.service.GetConfigForWorkspace(ctx.Request.Context(), workspaceID(ctx))
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
if cfg == nil {
|
|
ctx.Status(http.StatusNoContent)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, cfg)
|
|
}
|
|
|
|
func (c *Controller) setConfig(ctx *gin.Context) {
|
|
var request SetConfigRequest
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
|
return
|
|
}
|
|
cfg, err := c.service.SetConfigForWorkspace(ctx.Request.Context(), workspaceID(ctx), &request)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, cfg)
|
|
}
|
|
|
|
func (c *Controller) deleteConfig(ctx *gin.Context) {
|
|
if err := c.service.DeleteConfigForWorkspace(ctx.Request.Context(), workspaceID(ctx)); err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{"deleted": true})
|
|
}
|
|
|
|
func (c *Controller) testConfig(ctx *gin.Context) {
|
|
var request SetConfigRequest
|
|
if ctx.Request.ContentLength != 0 {
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
|
return
|
|
}
|
|
}
|
|
result, err := c.service.TestConnectionForWorkspace(ctx.Request.Context(), workspaceID(ctx), &request)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func (c *Controller) copyConfig(ctx *gin.Context) {
|
|
var request struct {
|
|
TargetWorkspaceID string `json:"targetWorkspaceId"`
|
|
}
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
|
return
|
|
}
|
|
cfg, err := c.service.CopyConfigToWorkspace(ctx.Request.Context(), workspaceID(ctx), request.TargetWorkspaceID)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, cfg)
|
|
}
|
|
|
|
func (c *Controller) listProjects(ctx *gin.Context) {
|
|
projects, err := c.service.ListProjectsForWorkspace(ctx.Request.Context(), workspaceID(ctx))
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{"projects": projects})
|
|
}
|
|
|
|
func (c *Controller) listRepositories(ctx *gin.Context) {
|
|
repositories, err := c.service.ListRepositoriesForWorkspace(
|
|
ctx.Request.Context(), workspaceID(ctx), ctx.Query("project"),
|
|
)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{"repositories": repositories})
|
|
}
|
|
|
|
func (c *Controller) listBranches(ctx *gin.Context) {
|
|
branches, err := c.service.ListBranchesForWorkspace(
|
|
ctx.Request.Context(), workspaceID(ctx), ctx.Query("organization"), ctx.Query("project"),
|
|
ctx.Query("repository"),
|
|
)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{"branches": branches})
|
|
}
|
|
|
|
func (c *Controller) searchWorkItems(ctx *gin.Context) {
|
|
var request struct {
|
|
Project string `json:"project"`
|
|
WIQL string `json:"wiql"`
|
|
Top int `json:"top"`
|
|
}
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
|
return
|
|
}
|
|
result, err := c.service.SearchWorkItemsForWorkspace(
|
|
ctx.Request.Context(), workspaceID(ctx), request.Project, request.WIQL, request.Top,
|
|
)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func (c *Controller) getWorkItem(ctx *gin.Context) {
|
|
id, err := strconv.Atoi(ctx.Param("id"))
|
|
if err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid work item id"})
|
|
return
|
|
}
|
|
item, err := c.service.GetWorkItemForWorkspace(
|
|
ctx.Request.Context(), workspaceID(ctx), ctx.Query("project"), id,
|
|
)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, item)
|
|
}
|
|
|
|
func (c *Controller) listPullRequests(ctx *gin.Context) {
|
|
skip, _ := strconv.Atoi(ctx.Query("skip"))
|
|
top, _ := strconv.Atoi(ctx.Query("top"))
|
|
result, err := c.service.ListPullRequestsForWorkspace(ctx.Request.Context(), workspaceID(ctx), PullRequestFilter{
|
|
ProjectID: ctx.Query("project"), RepositoryID: ctx.Query("repository"), Status: ctx.Query("status"),
|
|
CreatorID: ctx.Query("creator"), ReviewerID: ctx.Query("reviewer"),
|
|
SourceBranch: ctx.Query("source_branch"), TargetBranch: ctx.Query("target_branch"), Skip: skip, Top: top,
|
|
})
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func (c *Controller) getPullRequest(ctx *gin.Context) {
|
|
id, ok := pullRequestID(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
pr, err := c.service.GetPullRequestForWorkspace(ctx.Request.Context(), workspaceID(ctx), ctx.Param("projectId"), ctx.Param("repositoryId"), id)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, pr)
|
|
}
|
|
|
|
func (c *Controller) getPullRequestFeedback(ctx *gin.Context) {
|
|
id, ok := pullRequestID(ctx)
|
|
if !ok {
|
|
return
|
|
}
|
|
feedback, err := c.service.GetPullRequestFeedbackForWorkspace(ctx.Request.Context(), workspaceID(ctx), ctx.Param("projectId"), ctx.Param("repositoryId"), id)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, feedback)
|
|
}
|
|
|
|
type taskPRRequest struct {
|
|
RepositoryID string `json:"repositoryId" binding:"required"`
|
|
PullRequestID int `json:"pullRequestId" binding:"required"`
|
|
}
|
|
|
|
func (c *Controller) listWorkspaceTaskPRs(ctx *gin.Context) {
|
|
rows, err := c.service.ListTaskPRsByWorkspace(ctx.Request.Context(), ctx.Param("workspaceId"))
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, TaskPRsResponse{TaskPRs: rows})
|
|
}
|
|
|
|
func (c *Controller) associateTaskPR(ctx *gin.Context) {
|
|
c.writeTaskPRSync(ctx, c.service.AssociateTaskPR)
|
|
}
|
|
|
|
func (c *Controller) syncTaskPR(ctx *gin.Context) {
|
|
c.writeTaskPRSync(ctx, c.service.SyncTaskPR)
|
|
}
|
|
|
|
func (c *Controller) writeTaskPRSync(
|
|
ctx *gin.Context,
|
|
sync func(context.Context, string, string, string, int) (*TaskPR, error),
|
|
) {
|
|
var request taskPRRequest
|
|
if err := ctx.ShouldBindJSON(&request); err != nil || request.PullRequestID <= 0 {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "repositoryId and positive pullRequestId are required"})
|
|
return
|
|
}
|
|
row, err := sync(
|
|
ctx.Request.Context(), workspaceID(ctx), ctx.Param("taskId"),
|
|
request.RepositoryID, request.PullRequestID,
|
|
)
|
|
if err != nil {
|
|
c.writeError(ctx, err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, row)
|
|
}
|
|
|
|
func (c *Controller) writeError(ctx *gin.Context, err error) {
|
|
status := http.StatusInternalServerError
|
|
code := ""
|
|
switch {
|
|
case errors.Is(err, ErrInvalidWorkspaceID), errors.Is(err, ErrInvalidConfig),
|
|
errors.Is(err, ErrInvalidTaskPRAssociation):
|
|
status = http.StatusBadRequest
|
|
case errors.Is(err, ErrNotConfigured):
|
|
status, code = http.StatusServiceUnavailable, notConfiguredCode
|
|
default:
|
|
var apiErr *APIError
|
|
if errors.As(err, &apiErr) {
|
|
status = upstreamStatus(apiErr.StatusCode)
|
|
}
|
|
}
|
|
body := gin.H{"error": err.Error()}
|
|
if code != "" {
|
|
body["code"] = code
|
|
}
|
|
ctx.JSON(status, body)
|
|
}
|
|
|
|
func workspaceID(ctx *gin.Context) string { return strings.TrimSpace(ctx.Query("workspace_id")) }
|
|
func pullRequestID(ctx *gin.Context) (int, bool) {
|
|
id, err := strconv.Atoi(ctx.Param("pullRequestId"))
|
|
if err != nil || id <= 0 {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid pull request id"})
|
|
return 0, false
|
|
}
|
|
return id, true
|
|
}
|
|
func upstreamStatus(status int) int {
|
|
switch status {
|
|
case http.StatusUnauthorized, http.StatusForbidden, http.StatusTooManyRequests:
|
|
return status
|
|
default:
|
|
if status >= 500 {
|
|
return http.StatusBadGateway
|
|
}
|
|
return http.StatusBadRequest
|
|
}
|
|
}
|