功能开发完成
This commit is contained in:
@@ -5,9 +5,9 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/logger"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/service"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// RepoHandler 仓库API处理器
|
||||
@@ -23,6 +23,15 @@ func NewRepoHandler(repoService *service.RepoService) *RepoHandler {
|
||||
}
|
||||
|
||||
// AddBatch 批量添加仓库
|
||||
// @Summary 批量添加仓库
|
||||
// @Description 批量添加多个Git仓库,异步克隆到本地
|
||||
// @Tags 仓库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body service.AddReposRequest true "仓库URL列表"
|
||||
// @Success 200 {object} Response{data=service.AddReposResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Router /repos/batch [post]
|
||||
func (h *RepoHandler) AddBatch(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.AddReposRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -30,8 +39,8 @@ func (h *RepoHandler) AddBatch(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.URLs) == 0 {
|
||||
respondError(w, http.StatusBadRequest, 40001, "urls cannot be empty")
|
||||
if len(req.Repos) == 0 {
|
||||
respondError(w, http.StatusBadRequest, 40001, "repos cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,6 +55,16 @@ func (h *RepoHandler) AddBatch(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// List 获取仓库列表
|
||||
// @Summary 获取仓库列表
|
||||
// @Description 分页查询仓库列表,支持按状态筛选
|
||||
// @Tags 仓库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param status query string false "状态筛选(pending/cloning/ready/failed)"
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(20)
|
||||
// @Success 200 {object} Response
|
||||
// @Router /repos [get]
|
||||
func (h *RepoHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
status := r.URL.Query().Get("status")
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
@@ -76,6 +95,15 @@ func (h *RepoHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Get 获取仓库详情
|
||||
// @Summary 获取仓库详情
|
||||
// @Description 根据ID获取仓库详细信息
|
||||
// @Tags 仓库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "仓库ID"
|
||||
// @Success 200 {object} Response{data=models.Repository}
|
||||
// @Failure 404 {object} Response
|
||||
// @Router /repos/{id} [get]
|
||||
func (h *RepoHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -93,6 +121,16 @@ func (h *RepoHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// SwitchBranch 切换分支
|
||||
// @Summary 切换仓库分支
|
||||
// @Description 异步切换仓库到指定分支
|
||||
// @Tags 仓库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "仓库ID"
|
||||
// @Param request body object{branch=string} true "分支名称"
|
||||
// @Success 200 {object} Response{data=models.Task}
|
||||
// @Failure 400 {object} Response
|
||||
// @Router /repos/{id}/switch-branch [post]
|
||||
func (h *RepoHandler) SwitchBranch(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -124,6 +162,15 @@ func (h *RepoHandler) SwitchBranch(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Update 更新仓库
|
||||
// @Summary 更新仓库
|
||||
// @Description 异步拉取仓库最新代码(git pull)
|
||||
// @Tags 仓库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "仓库ID"
|
||||
// @Success 200 {object} Response{data=models.Task}
|
||||
// @Failure 400 {object} Response
|
||||
// @Router /repos/{id}/update [post]
|
||||
func (h *RepoHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -142,6 +189,14 @@ func (h *RepoHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Reset 重置仓库
|
||||
// @Summary 重置仓库
|
||||
// @Description 异步重置仓库到最新状态
|
||||
// @Tags 仓库管理
|
||||
// @Produce json
|
||||
// @Param id path int true "仓库ID"
|
||||
// @Success 200 {object} Response{data=models.Task}
|
||||
// @Failure 400 {object} Response
|
||||
// @Router /repos/{id}/reset [post]
|
||||
func (h *RepoHandler) Reset(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -160,6 +215,14 @@ func (h *RepoHandler) Reset(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Delete 删除仓库
|
||||
// @Summary 删除仓库
|
||||
// @Description 删除指定仓库
|
||||
// @Tags 仓库管理
|
||||
// @Produce json
|
||||
// @Param id path int true "仓库ID"
|
||||
// @Success 200 {object} Response
|
||||
// @Failure 400 {object} Response
|
||||
// @Router /repos/{id} [delete]
|
||||
func (h *RepoHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -175,3 +238,35 @@ func (h *RepoHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
respondJSON(w, http.StatusOK, 0, "repository deleted successfully", nil)
|
||||
}
|
||||
|
||||
// GetBranches 获取仓库分支列表
|
||||
// @Summary 获取仓库分支列表
|
||||
// @Description 获取指定仓库的所有分支
|
||||
// @Tags 仓库管理
|
||||
// @Produce json
|
||||
// @Param id path int true "仓库ID"
|
||||
// @Success 200 {object} Response{data=object}
|
||||
// @Failure 400 {object} Response
|
||||
// @Failure 404 {object} Response
|
||||
// @Router /repos/{id}/branches [get]
|
||||
func (h *RepoHandler) GetBranches(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, 40001, "invalid repository id")
|
||||
return
|
||||
}
|
||||
|
||||
branches, err := h.repoService.GetBranches(r.Context(), id)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Int64("repo_id", id).Msg("failed to get branches")
|
||||
respondError(w, http.StatusInternalServerError, 50000, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"branches": branches,
|
||||
"count": len(branches),
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, 0, "success", data)
|
||||
}
|
||||
|
||||
@@ -7,21 +7,33 @@ import (
|
||||
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/logger"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/service"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/storage"
|
||||
)
|
||||
|
||||
// StatsHandler 统计API处理器
|
||||
type StatsHandler struct {
|
||||
statsService *service.StatsService
|
||||
store storage.Store
|
||||
}
|
||||
|
||||
// NewStatsHandler 创建统计处理器
|
||||
func NewStatsHandler(statsService *service.StatsService) *StatsHandler {
|
||||
func NewStatsHandler(statsService *service.StatsService, store storage.Store) *StatsHandler {
|
||||
return &StatsHandler{
|
||||
statsService: statsService,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate 触发统计计算
|
||||
// @Summary 触发统计任务
|
||||
// @Description 异步触发统计计算任务
|
||||
// @Tags 统计管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body service.CalculateRequest true "统计请求"
|
||||
// @Success 200 {object} Response{data=models.Task}
|
||||
// @Failure 400 {object} Response
|
||||
// @Router /stats/calculate [post]
|
||||
func (h *StatsHandler) Calculate(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CalculateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -56,6 +68,19 @@ func (h *StatsHandler) Calculate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// QueryResult 查询统计结果
|
||||
// @Summary 查询统计结果
|
||||
// @Description 查询统计计算结果
|
||||
// @Tags 统计管理
|
||||
// @Produce json
|
||||
// @Param repo_id query int true "仓库ID"
|
||||
// @Param branch query string true "分支名称"
|
||||
// @Param constraint_type query string false "约束类型"
|
||||
// @Param from query string false "开始日期"
|
||||
// @Param to query string false "结束日期"
|
||||
// @Param limit query int false "提交数限制"
|
||||
// @Success 200 {object} Response{data=models.StatsResult}
|
||||
// @Failure 400 {object} Response
|
||||
// @Router /stats/query [get]
|
||||
func (h *StatsHandler) QueryResult(w http.ResponseWriter, r *http.Request) {
|
||||
repoID, _ := strconv.ParseInt(r.URL.Query().Get("repo_id"), 10, 64)
|
||||
branch := r.URL.Query().Get("branch")
|
||||
@@ -98,6 +123,16 @@ func (h *StatsHandler) QueryResult(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// CountCommits 统计提交次数
|
||||
// @Summary 统计提交次数
|
||||
// @Description 统计指定条件下的提交次数
|
||||
// @Tags 统计管理
|
||||
// @Produce json
|
||||
// @Param repo_id query int true "仓库ID"
|
||||
// @Param branch query string true "分支名称"
|
||||
// @Param from query string false "开始日期"
|
||||
// @Success 200 {object} Response{data=service.CountCommitsResponse}
|
||||
// @Failure 400 {object} Response
|
||||
// @Router /stats/commits/count [get]
|
||||
func (h *StatsHandler) CountCommits(w http.ResponseWriter, r *http.Request) {
|
||||
repoID, _ := strconv.ParseInt(r.URL.Query().Get("repo_id"), 10, 64)
|
||||
branch := r.URL.Query().Get("branch")
|
||||
@@ -128,3 +163,62 @@ func (h *StatsHandler) CountCommits(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
respondJSON(w, http.StatusOK, 0, "success", result)
|
||||
}
|
||||
|
||||
// ListCaches 获取统计缓存列表
|
||||
// @Summary 获取统计缓存列表
|
||||
// @Description 获取已计算的统计缓存列表
|
||||
// @Tags 统计管理
|
||||
// @Produce json
|
||||
// @Param repo_id query int false "仓库ID(可选,不传则返回所有)"
|
||||
// @Param limit query int false "返回数量限制" default(50)
|
||||
// @Success 200 {object} Response{data=object}
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /stats/caches [get]
|
||||
func (h *StatsHandler) ListCaches(w http.ResponseWriter, r *http.Request) {
|
||||
repoID, _ := strconv.ParseInt(r.URL.Query().Get("repo_id"), 10, 64)
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
|
||||
limit := 50
|
||||
if limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
caches, total, err := h.store.StatsCache().List(r.Context(), repoID, limit)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Msg("failed to list stats caches")
|
||||
respondError(w, http.StatusInternalServerError, 50000, "failed to list stats caches")
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"caches": caches,
|
||||
"total": total,
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, 0, "success", data)
|
||||
}
|
||||
|
||||
// ClearAllCaches 清除所有统计缓存
|
||||
// @Summary 清除所有统计缓存
|
||||
// @Description 删除所有统计缓存记录和文件
|
||||
// @Tags 统计管理
|
||||
// @Produce json
|
||||
// @Success 200 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /stats/caches/clear [delete]
|
||||
func (h *StatsHandler) ClearAllCaches(w http.ResponseWriter, r *http.Request) {
|
||||
// 删除数据库中的缓存记录
|
||||
if err := h.store.StatsCache().DeleteAll(r.Context()); err != nil {
|
||||
logger.Logger.Error().Err(err).Msg("failed to clear all caches")
|
||||
respondError(w, http.StatusInternalServerError, 50000, "failed to clear caches")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Logger.Info().Msg("all stats caches cleared")
|
||||
respondJSON(w, http.StatusOK, 0, "所有统计缓存已清除", nil)
|
||||
}
|
||||
|
||||
99
internal/api/handlers/task.go
Normal file
99
internal/api/handlers/task.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/logger"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/storage"
|
||||
)
|
||||
|
||||
// TaskHandler 任务API处理器
|
||||
type TaskHandler struct {
|
||||
store storage.Store
|
||||
}
|
||||
|
||||
// NewTaskHandler 创建任务处理器
|
||||
func NewTaskHandler(store storage.Store) *TaskHandler {
|
||||
return &TaskHandler{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// List 查询任务列表
|
||||
// @Summary 查询任务列表
|
||||
// @Description 查询任务列表,可按状态过滤
|
||||
// @Tags 任务管理
|
||||
// @Produce json
|
||||
// @Param status query string false "任务状态"
|
||||
// @Param limit query int false "返回数量限制" default(50)
|
||||
// @Success 200 {object} Response{data=object}
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /tasks [get]
|
||||
func (h *TaskHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
status := r.URL.Query().Get("status")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
|
||||
limit := 50
|
||||
if limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 List 方法,repoID=0 表示不过滤仓库
|
||||
tasks, total, err := h.store.Tasks().List(r.Context(), 0, status, 1, limit)
|
||||
if err != nil {
|
||||
logger.Logger.Error().Err(err).Msg("failed to list tasks")
|
||||
respondError(w, http.StatusInternalServerError, 50000, "failed to list tasks")
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"tasks": tasks,
|
||||
"total": total,
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, 0, "success", data)
|
||||
}
|
||||
|
||||
// ClearAllTasks 清除所有任务记录
|
||||
// @Summary 清除所有任务记录
|
||||
// @Description 删除所有任务记录(包括进行中的)
|
||||
// @Tags 任务管理
|
||||
// @Produce json
|
||||
// @Success 200 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /tasks/clear [delete]
|
||||
func (h *TaskHandler) ClearAllTasks(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.store.Tasks().DeleteAll(r.Context()); err != nil {
|
||||
logger.Logger.Error().Err(err).Msg("failed to clear all tasks")
|
||||
respondError(w, http.StatusInternalServerError, 50000, "failed to clear tasks")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Logger.Info().Msg("all tasks cleared")
|
||||
respondJSON(w, http.StatusOK, 0, "所有任务记录已清除", nil)
|
||||
}
|
||||
|
||||
// ClearCompletedTasks 清除已完成的任务记录
|
||||
// @Summary 清除已完成的任务记录
|
||||
// @Description 删除已完成、失败或取消的任务记录
|
||||
// @Tags 任务管理
|
||||
// @Produce json
|
||||
// @Success 200 {object} Response
|
||||
// @Failure 500 {object} Response
|
||||
// @Router /tasks/clear-completed [delete]
|
||||
func (h *TaskHandler) ClearCompletedTasks(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.store.Tasks().DeleteCompleted(r.Context()); err != nil {
|
||||
logger.Logger.Error().Err(err).Msg("failed to clear completed tasks")
|
||||
respondError(w, http.StatusInternalServerError, 50000, "failed to clear completed tasks")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Logger.Info().Msg("completed tasks cleared")
|
||||
respondJSON(w, http.StatusOK, 0, "已完成的任务记录已清除", nil)
|
||||
}
|
||||
@@ -3,23 +3,32 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
_ "github.com/gitcodestatic/gitcodestatic/docs"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/api/handlers"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/service"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/storage"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
)
|
||||
|
||||
// Router 路由配置
|
||||
type Router struct {
|
||||
repoHandler *handlers.RepoHandler
|
||||
statsHandler *handlers.StatsHandler
|
||||
taskHandler *handlers.TaskHandler
|
||||
webDir string
|
||||
webEnabled bool
|
||||
}
|
||||
|
||||
// NewRouter 创建路由
|
||||
func NewRouter(repoService *service.RepoService, statsService *service.StatsService) *Router {
|
||||
func NewRouter(repoService *service.RepoService, statsService *service.StatsService, store storage.Store, webDir string, webEnabled bool) *Router {
|
||||
return &Router{
|
||||
repoHandler: handlers.NewRepoHandler(repoService),
|
||||
statsHandler: handlers.NewStatsHandler(statsService),
|
||||
statsHandler: handlers.NewStatsHandler(statsService, store),
|
||||
taskHandler: handlers.NewTaskHandler(store),
|
||||
webDir: webDir,
|
||||
webEnabled: webEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +49,19 @@ func (rt *Router) Setup() http.Handler {
|
||||
w.Write([]byte(`{"status":"healthy"}`))
|
||||
})
|
||||
|
||||
// Swagger documentation
|
||||
r.Get("/swagger/*", httpSwagger.Handler(
|
||||
httpSwagger.URL("/swagger/doc.json"),
|
||||
))
|
||||
|
||||
// Web UI static files
|
||||
if rt.webEnabled {
|
||||
fileServer := http.FileServer(http.Dir(rt.webDir))
|
||||
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// API routes
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
// 仓库管理
|
||||
@@ -47,6 +69,7 @@ func (rt *Router) Setup() http.Handler {
|
||||
r.Post("/batch", rt.repoHandler.AddBatch)
|
||||
r.Get("/", rt.repoHandler.List)
|
||||
r.Get("/{id}", rt.repoHandler.Get)
|
||||
r.Get("/{id}/branches", rt.repoHandler.GetBranches)
|
||||
r.Post("/{id}/switch-branch", rt.repoHandler.SwitchBranch)
|
||||
r.Post("/{id}/update", rt.repoHandler.Update)
|
||||
r.Post("/{id}/reset", rt.repoHandler.Reset)
|
||||
@@ -58,6 +81,15 @@ func (rt *Router) Setup() http.Handler {
|
||||
r.Post("/calculate", rt.statsHandler.Calculate)
|
||||
r.Get("/result", rt.statsHandler.QueryResult)
|
||||
r.Get("/commit-count", rt.statsHandler.CountCommits)
|
||||
r.Get("/caches", rt.statsHandler.ListCaches)
|
||||
r.Delete("/caches/clear", rt.statsHandler.ClearAllCaches)
|
||||
})
|
||||
|
||||
// 任务
|
||||
r.Route("/tasks", func(r chi.Router) {
|
||||
r.Get("/", rt.taskHandler.List)
|
||||
r.Delete("/clear", rt.taskHandler.ClearAllTasks)
|
||||
r.Delete("/clear-completed", rt.taskHandler.ClearCompletedTasks)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
// Config 应用配置
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Web WebConfig `yaml:"web"`
|
||||
Workspace WorkspaceConfig `yaml:"workspace"`
|
||||
Storage StorageConfig `yaml:"storage"`
|
||||
Worker WorkerConfig `yaml:"worker"`
|
||||
@@ -29,6 +30,12 @@ type ServerConfig struct {
|
||||
WriteTimeout time.Duration `yaml:"write_timeout"`
|
||||
}
|
||||
|
||||
// WebConfig 前端配置
|
||||
type WebConfig struct {
|
||||
Dir string `yaml:"dir"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
// WorkspaceConfig 工作空间配置
|
||||
type WorkspaceConfig struct {
|
||||
BaseDir string `yaml:"base_dir"`
|
||||
@@ -82,7 +89,7 @@ type SecurityConfig struct {
|
||||
|
||||
// GitConfig Git配置
|
||||
type GitConfig struct {
|
||||
CommandPath string `yaml:"command_path"`
|
||||
CommandPath string `yaml:"command_path"`
|
||||
FallbackToGoGit bool `yaml:"fallback_to_gogit"`
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ func (m *CmdGitManager) Pull(ctx context.Context, localPath string, cred *models
|
||||
// Checkout 切换分支
|
||||
func (m *CmdGitManager) Checkout(ctx context.Context, localPath, branch string) error {
|
||||
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "checkout", branch)
|
||||
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
logger.Logger.Error().
|
||||
@@ -111,7 +111,7 @@ func (m *CmdGitManager) Checkout(ctx context.Context, localPath, branch string)
|
||||
// GetCurrentBranch 获取当前分支
|
||||
func (m *CmdGitManager) GetCurrentBranch(ctx context.Context, localPath string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "rev-parse", "--abbrev-ref", "HEAD")
|
||||
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get current branch: %w", err)
|
||||
@@ -124,7 +124,7 @@ func (m *CmdGitManager) GetCurrentBranch(ctx context.Context, localPath string)
|
||||
// GetHeadCommitHash 获取HEAD commit hash
|
||||
func (m *CmdGitManager) GetHeadCommitHash(ctx context.Context, localPath string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "rev-parse", "HEAD")
|
||||
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get HEAD commit hash: %w", err)
|
||||
@@ -137,15 +137,15 @@ func (m *CmdGitManager) GetHeadCommitHash(ctx context.Context, localPath string)
|
||||
// CountCommits 统计提交次数
|
||||
func (m *CmdGitManager) CountCommits(ctx context.Context, localPath, branch, fromDate string) (int, error) {
|
||||
args := []string{"-C", localPath, "rev-list", "--count"}
|
||||
|
||||
|
||||
if fromDate != "" {
|
||||
args = append(args, "--since="+fromDate)
|
||||
}
|
||||
|
||||
|
||||
args = append(args, branch)
|
||||
|
||||
|
||||
cmd := exec.CommandContext(ctx, m.gitPath, args...)
|
||||
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to count commits: %w", err)
|
||||
@@ -160,6 +160,46 @@ func (m *CmdGitManager) CountCommits(ctx context.Context, localPath, branch, fro
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ListBranches 获取仓库分支列表
|
||||
func (m *CmdGitManager) ListBranches(ctx context.Context, localPath string) ([]string, error) {
|
||||
// 首先获取远程分支
|
||||
cmd := exec.CommandContext(ctx, m.gitPath, "-C", localPath, "branch", "-r")
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
logger.Logger.Error().
|
||||
Err(err).
|
||||
Str("local_path", localPath).
|
||||
Msg("failed to list remote branches")
|
||||
return nil, fmt.Errorf("failed to list branches: %w", err)
|
||||
}
|
||||
|
||||
// 解析分支列表
|
||||
lines := strings.Split(string(output), "\n")
|
||||
branches := make([]string, 0)
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.Contains(line, "->") {
|
||||
// 跳过空行和HEAD指针
|
||||
continue
|
||||
}
|
||||
|
||||
// 移除 origin/ 前缀
|
||||
branch := strings.TrimPrefix(line, "origin/")
|
||||
if branch != "" {
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Logger.Debug().
|
||||
Str("local_path", localPath).
|
||||
Int("count", len(branches)).
|
||||
Msg("branches listed successfully")
|
||||
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
// injectCredentials 注入凭据到URL
|
||||
func (m *CmdGitManager) injectCredentials(url string, cred *models.Credential) string {
|
||||
if cred == nil || cred.Username == "" {
|
||||
|
||||
@@ -10,22 +10,25 @@ import (
|
||||
type Manager interface {
|
||||
// Clone 克隆仓库
|
||||
Clone(ctx context.Context, url, localPath string, cred *models.Credential) error
|
||||
|
||||
|
||||
// Pull 拉取更新
|
||||
Pull(ctx context.Context, localPath string, cred *models.Credential) error
|
||||
|
||||
|
||||
// Checkout 切换分支
|
||||
Checkout(ctx context.Context, localPath, branch string) error
|
||||
|
||||
|
||||
// GetCurrentBranch 获取当前分支
|
||||
GetCurrentBranch(ctx context.Context, localPath string) (string, error)
|
||||
|
||||
|
||||
// GetHeadCommitHash 获取HEAD commit hash
|
||||
GetHeadCommitHash(ctx context.Context, localPath string) (string, error)
|
||||
|
||||
|
||||
// CountCommits 统计提交次数
|
||||
CountCommits(ctx context.Context, localPath, branch, fromDate string) (int, error)
|
||||
|
||||
|
||||
// ListBranches 获取分支列表
|
||||
ListBranches(ctx context.Context, localPath string) ([]string, error)
|
||||
|
||||
// IsAvailable 检查Git是否可用
|
||||
IsAvailable() bool
|
||||
}
|
||||
|
||||
@@ -4,17 +4,17 @@ import "time"
|
||||
|
||||
// StatsCache 统计缓存模型
|
||||
type StatsCache struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
RepoID int64 `json:"repo_id" db:"repo_id"`
|
||||
Branch string `json:"branch" db:"branch"`
|
||||
ConstraintType string `json:"constraint_type" db:"constraint_type"` // date_range/commit_limit
|
||||
ConstraintValue string `json:"constraint_value" db:"constraint_value"` // JSON string
|
||||
CommitHash string `json:"commit_hash" db:"commit_hash"`
|
||||
ResultPath string `json:"result_path" db:"result_path"`
|
||||
ResultSize int64 `json:"result_size" db:"result_size"`
|
||||
CacheKey string `json:"cache_key" db:"cache_key"`
|
||||
HitCount int `json:"hit_count" db:"hit_count"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
ID int64 `json:"id" db:"id"`
|
||||
RepoID int64 `json:"repo_id" db:"repo_id"`
|
||||
Branch string `json:"branch" db:"branch"`
|
||||
ConstraintType string `json:"constraint_type" db:"constraint_type"` // date_range/commit_limit
|
||||
ConstraintValue string `json:"constraint_value" db:"constraint_value"` // JSON string
|
||||
CommitHash string `json:"commit_hash" db:"commit_hash"`
|
||||
ResultPath string `json:"result_path" db:"result_path"`
|
||||
ResultSize int64 `json:"result_size" db:"result_size"`
|
||||
CacheKey string `json:"cache_key" db:"cache_key"`
|
||||
HitCount int `json:"hit_count" db:"hit_count"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
LastHitAt *time.Time `json:"last_hit_at,omitempty" db:"last_hit_at"`
|
||||
}
|
||||
|
||||
@@ -34,24 +34,24 @@ const (
|
||||
|
||||
// StatsResult 统计结果
|
||||
type StatsResult struct {
|
||||
CacheHit bool `json:"cache_hit"`
|
||||
CachedAt *time.Time `json:"cached_at,omitempty"`
|
||||
CommitHash string `json:"commit_hash"`
|
||||
Statistics *Statistics `json:"statistics"`
|
||||
CacheHit bool `json:"cache_hit"`
|
||||
CachedAt *time.Time `json:"cached_at,omitempty"`
|
||||
CommitHash string `json:"commit_hash"`
|
||||
Statistics *Statistics `json:"statistics"`
|
||||
}
|
||||
|
||||
// Statistics 统计数据
|
||||
type Statistics struct {
|
||||
Summary StatsSummary `json:"summary"`
|
||||
ByContributor []ContributorStats `json:"by_contributor"`
|
||||
Summary StatsSummary `json:"summary"`
|
||||
ByContributor []ContributorStats `json:"by_contributor"`
|
||||
}
|
||||
|
||||
// StatsSummary 统计摘要
|
||||
type StatsSummary struct {
|
||||
TotalCommits int `json:"total_commits"`
|
||||
TotalContributors int `json:"total_contributors"`
|
||||
DateRange *DateRange `json:"date_range,omitempty"`
|
||||
CommitLimit *int `json:"commit_limit,omitempty"`
|
||||
TotalCommits int `json:"total_commits"`
|
||||
TotalContributors int `json:"total_contributors"`
|
||||
DateRange *DateRange `json:"date_range,omitempty"`
|
||||
CommitLimit *int `json:"commit_limit,omitempty"`
|
||||
}
|
||||
|
||||
// DateRange 日期范围
|
||||
@@ -62,13 +62,15 @@ type DateRange struct {
|
||||
|
||||
// ContributorStats 贡献者统计
|
||||
type ContributorStats struct {
|
||||
Author string `json:"author"`
|
||||
Email string `json:"email"`
|
||||
Commits int `json:"commits"`
|
||||
Additions int `json:"additions"` // 新增行数
|
||||
Deletions int `json:"deletions"` // 删除行数
|
||||
Modifications int `json:"modifications"` // 修改行数 = min(additions, deletions)
|
||||
NetAdditions int `json:"net_additions"` // 净增加 = additions - deletions
|
||||
Author string `json:"author"`
|
||||
Email string `json:"email"`
|
||||
Commits int `json:"commits"`
|
||||
Additions int `json:"additions"` // 新增行数
|
||||
Deletions int `json:"deletions"` // 删除行数
|
||||
Modifications int `json:"modifications"` // 修改行数 = min(additions, deletions)
|
||||
NetAdditions int `json:"net_additions"` // 净增加 = additions - deletions
|
||||
FirstCommitDate string `json:"first_commit_date"` // 首次提交日期
|
||||
LastCommitDate string `json:"last_commit_date"` // 最后提交日期
|
||||
}
|
||||
|
||||
// Credential 凭据模型
|
||||
|
||||
@@ -2,6 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -9,6 +11,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/git"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/logger"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/models"
|
||||
"github.com/gitcodestatic/gitcodestatic/internal/storage"
|
||||
@@ -17,30 +20,40 @@ import (
|
||||
|
||||
// RepoService 仓库服务
|
||||
type RepoService struct {
|
||||
store storage.Store
|
||||
queue *worker.Queue
|
||||
cacheDir string
|
||||
store storage.Store
|
||||
queue *worker.Queue
|
||||
cacheDir string
|
||||
gitManager git.Manager
|
||||
}
|
||||
|
||||
// NewRepoService 创建仓库服务
|
||||
func NewRepoService(store storage.Store, queue *worker.Queue, cacheDir string) *RepoService {
|
||||
func NewRepoService(store storage.Store, queue *worker.Queue, cacheDir string, gitManager git.Manager) *RepoService {
|
||||
return &RepoService{
|
||||
store: store,
|
||||
queue: queue,
|
||||
cacheDir: cacheDir,
|
||||
store: store,
|
||||
queue: queue,
|
||||
cacheDir: cacheDir,
|
||||
gitManager: gitManager,
|
||||
}
|
||||
}
|
||||
|
||||
// AddReposRequest 批量添加仓库请求
|
||||
// RepoInput 仓库输入
|
||||
type RepoInput struct {
|
||||
URL string `json:"url"`
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
|
||||
type AddReposRequest struct {
|
||||
URLs []string `json:"urls"`
|
||||
Repos []RepoInput `json:"repos"`
|
||||
Username string `json:"username,omitempty"` // 可选的认证信息
|
||||
Password string `json:"password,omitempty"` // 可选的认证信息
|
||||
}
|
||||
|
||||
// AddReposResponse 批量添加仓库响应
|
||||
type AddReposResponse struct {
|
||||
Total int `json:"total"`
|
||||
Succeeded []AddRepoResult `json:"succeeded"`
|
||||
Failed []AddRepoFailure `json:"failed"`
|
||||
Total int `json:"total"`
|
||||
Succeeded []AddRepoResult `json:"succeeded"`
|
||||
Failed []AddRepoFailure `json:"failed"`
|
||||
}
|
||||
|
||||
// AddRepoResult 添加仓库成功结果
|
||||
@@ -59,12 +72,36 @@ type AddRepoFailure struct {
|
||||
// AddRepos 批量添加仓库
|
||||
func (s *RepoService) AddRepos(ctx context.Context, req *AddReposRequest) (*AddReposResponse, error) {
|
||||
resp := &AddReposResponse{
|
||||
Total: len(req.URLs),
|
||||
Total: len(req.Repos),
|
||||
Succeeded: make([]AddRepoResult, 0),
|
||||
Failed: make([]AddRepoFailure, 0),
|
||||
}
|
||||
|
||||
for _, url := range req.URLs {
|
||||
// 如果提供了认证信息,创建凭据
|
||||
var credentialID *string
|
||||
if req.Username != "" && req.Password != "" {
|
||||
cred := &models.Credential{
|
||||
ID: generateCredentialID(),
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
AuthType: models.AuthTypeBasic,
|
||||
}
|
||||
|
||||
if err := s.store.Credentials().Create(ctx, cred); err != nil {
|
||||
logger.Logger.Warn().Err(err).Msg("failed to save credential, will continue without credentials")
|
||||
} else {
|
||||
credentialID = &cred.ID
|
||||
logger.Logger.Info().Str("credential_id", cred.ID).Msg("credential created")
|
||||
}
|
||||
}
|
||||
|
||||
for _, repoInput := range req.Repos {
|
||||
url := repoInput.URL
|
||||
branch := repoInput.Branch
|
||||
if branch == "" {
|
||||
branch = "main" // 默认分支
|
||||
}
|
||||
|
||||
// 校验URL
|
||||
if !isValidGitURL(url) {
|
||||
resp.Failed = append(resp.Failed, AddRepoFailure{
|
||||
@@ -97,10 +134,12 @@ func (s *RepoService) AddRepos(ctx context.Context, req *AddReposRequest) (*AddR
|
||||
localPath := filepath.Join(s.cacheDir, repoName)
|
||||
|
||||
repo := &models.Repository{
|
||||
URL: url,
|
||||
Name: repoName,
|
||||
LocalPath: localPath,
|
||||
Status: models.RepoStatusPending,
|
||||
URL: url,
|
||||
Name: repoName,
|
||||
CurrentBranch: branch,
|
||||
LocalPath: localPath,
|
||||
Status: models.RepoStatusPending,
|
||||
CredentialID: credentialID,
|
||||
}
|
||||
|
||||
if err := s.store.Repos().Create(ctx, repo); err != nil {
|
||||
@@ -136,6 +175,7 @@ func (s *RepoService) AddRepos(ctx context.Context, req *AddReposRequest) (*AddR
|
||||
Int64("repo_id", repo.ID).
|
||||
Str("url", url).
|
||||
Int64("task_id", task.ID).
|
||||
Bool("has_credentials", credentialID != nil).
|
||||
Msg("repository added")
|
||||
}
|
||||
|
||||
@@ -149,7 +189,17 @@ func (s *RepoService) GetRepo(ctx context.Context, id int64) (*models.Repository
|
||||
|
||||
// ListRepos 获取仓库列表
|
||||
func (s *RepoService) ListRepos(ctx context.Context, status string, page, pageSize int) ([]*models.Repository, int, error) {
|
||||
return s.store.Repos().List(ctx, status, page, pageSize)
|
||||
repos, total, err := s.store.Repos().List(ctx, status, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 设置has_credentials标志
|
||||
for _, repo := range repos {
|
||||
repo.HasCredentials = repo.CredentialID != nil && *repo.CredentialID != ""
|
||||
}
|
||||
|
||||
return repos, total, nil
|
||||
}
|
||||
|
||||
// SwitchBranch 切换分支
|
||||
@@ -253,19 +303,44 @@ func (s *RepoService) DeleteRepo(ctx context.Context, id int64) error {
|
||||
return s.store.Repos().Delete(ctx, id)
|
||||
}
|
||||
|
||||
// GetBranches 获取仓库分支列表
|
||||
func (s *RepoService) GetBranches(ctx context.Context, repoID int64) ([]string, error) {
|
||||
// 获取仓库信息
|
||||
repo, err := s.store.Repos().GetByID(ctx, repoID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get repository: %w", err)
|
||||
}
|
||||
|
||||
if repo == nil {
|
||||
return nil, fmt.Errorf("repository not found")
|
||||
}
|
||||
|
||||
if repo.Status != models.RepoStatusReady {
|
||||
return nil, fmt.Errorf("repository is not ready, status: %s", repo.Status)
|
||||
}
|
||||
|
||||
// 使用git命令获取分支列表
|
||||
branches, err := s.gitManager.ListBranches(ctx, repo.LocalPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list branches: %w", err)
|
||||
}
|
||||
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
// isValidGitURL 校验Git URL
|
||||
func isValidGitURL(url string) bool {
|
||||
// 简单校验:https:// 或 git@ 开头
|
||||
return strings.HasPrefix(url, "https://") ||
|
||||
strings.HasPrefix(url, "http://") ||
|
||||
strings.HasPrefix(url, "git@")
|
||||
return strings.HasPrefix(url, "https://") ||
|
||||
strings.HasPrefix(url, "http://") ||
|
||||
strings.HasPrefix(url, "git@")
|
||||
}
|
||||
|
||||
// extractRepoName 从URL提取仓库名称
|
||||
func extractRepoName(url string) string {
|
||||
// 移除.git后缀
|
||||
url = strings.TrimSuffix(url, ".git")
|
||||
|
||||
|
||||
// 提取最后一个路径部分
|
||||
parts := strings.Split(url, "/")
|
||||
if len(parts) > 0 {
|
||||
@@ -274,6 +349,13 @@ func extractRepoName(url string) string {
|
||||
name = regexp.MustCompile(`[^a-zA-Z0-9_-]`).ReplaceAllString(name, "_")
|
||||
return name
|
||||
}
|
||||
|
||||
|
||||
return "repo"
|
||||
}
|
||||
|
||||
// generateCredentialID 生成凭据ID
|
||||
func generateCredentialID() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@ func (c *Calculator) parseGitLog(output string) (*models.Statistics, error) {
|
||||
}
|
||||
|
||||
contributors := make(map[string]*models.ContributorStats)
|
||||
|
||||
var currentAuthor, currentEmail string
|
||||
|
||||
var currentAuthor, currentEmail, currentDate string
|
||||
commitCount := 0
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(output))
|
||||
@@ -105,7 +105,7 @@ func (c *Calculator) parseGitLog(output string) (*models.Statistics, error) {
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
@@ -114,14 +114,21 @@ func (c *Calculator) parseGitLog(output string) (*models.Statistics, error) {
|
||||
if matches := commitPattern.FindStringSubmatch(line); matches != nil {
|
||||
currentAuthor = matches[2]
|
||||
currentEmail = matches[3]
|
||||
currentDate = matches[4]
|
||||
commitCount++
|
||||
|
||||
// 初始化贡献者统计
|
||||
if _, ok := contributors[currentEmail]; !ok {
|
||||
// 第一次遇到该贡献者,这是最新的提交(git log从新到旧)
|
||||
contributors[currentEmail] = &models.ContributorStats{
|
||||
Author: currentAuthor,
|
||||
Email: currentEmail,
|
||||
Author: currentAuthor,
|
||||
Email: currentEmail,
|
||||
LastCommitDate: currentDate, // 第一次遇到就是最新的
|
||||
FirstCommitDate: currentDate, // 暂时设为相同,会不断更新
|
||||
}
|
||||
} else {
|
||||
// 继续更新首次提交日期,因为git log从新到旧,越往后越早
|
||||
contributors[currentEmail].FirstCommitDate = currentDate
|
||||
}
|
||||
contributors[currentEmail].Commits++
|
||||
continue
|
||||
@@ -135,7 +142,7 @@ func (c *Calculator) parseGitLog(output string) (*models.Statistics, error) {
|
||||
// 处理二进制文件(显示为 -)
|
||||
additions := 0
|
||||
deletions := 0
|
||||
|
||||
|
||||
if additionsStr != "-" {
|
||||
additions, _ = strconv.Atoi(additionsStr)
|
||||
}
|
||||
|
||||
@@ -20,11 +20,9 @@ type Pool struct {
|
||||
}
|
||||
|
||||
// NewPool 创建Worker池
|
||||
func NewPool(workerCount int, queueSize int, store storage.Store, handlers map[string]TaskHandler) *Pool {
|
||||
func NewPool(workerCount int, queue *Queue, store storage.Store, handlers map[string]TaskHandler) *Pool {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
queue := NewQueue(queueSize, store)
|
||||
|
||||
|
||||
pool := &Pool{
|
||||
queue: queue,
|
||||
workers: make([]*Worker, 0, workerCount),
|
||||
@@ -46,7 +44,7 @@ func NewPool(workerCount int, queueSize int, store storage.Store, handlers map[s
|
||||
// Start 启动Worker池
|
||||
func (p *Pool) Start() {
|
||||
logger.Logger.Info().Int("worker_count", len(p.workers)).Msg("starting worker pool")
|
||||
|
||||
|
||||
for _, worker := range p.workers {
|
||||
worker.Start(p.ctx)
|
||||
}
|
||||
@@ -55,15 +53,15 @@ func (p *Pool) Start() {
|
||||
// Stop 停止Worker池
|
||||
func (p *Pool) Stop() {
|
||||
logger.Logger.Info().Msg("stopping worker pool")
|
||||
|
||||
|
||||
p.cancel()
|
||||
|
||||
|
||||
for _, worker := range p.workers {
|
||||
worker.Stop()
|
||||
}
|
||||
|
||||
|
||||
p.queue.Close()
|
||||
|
||||
|
||||
logger.Logger.Info().Msg("worker pool stopped")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user