功能开发完成

This commit is contained in:
2025-12-31 16:23:40 +08:00
parent 2b51050ca8
commit 6f0598a859
28 changed files with 5463 additions and 118 deletions

View File

@@ -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)
}

View File

@@ -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)
}

View 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)
}

View File

@@ -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)
})
})

View File

@@ -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"`
}

View File

@@ -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 == "" {

View File

@@ -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
}

View File

@@ -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 凭据模型

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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")
}