基本能力编写完成

This commit is contained in:
2025-12-31 14:23:53 +08:00
parent ac5aa1eb70
commit 2b51050ca8
33 changed files with 5464 additions and 7 deletions

View File

@@ -0,0 +1,279 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"regexp"
"strings"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
"github.com/gitcodestatic/gitcodestatic/internal/worker"
)
// RepoService 仓库服务
type RepoService struct {
store storage.Store
queue *worker.Queue
cacheDir string
}
// NewRepoService 创建仓库服务
func NewRepoService(store storage.Store, queue *worker.Queue, cacheDir string) *RepoService {
return &RepoService{
store: store,
queue: queue,
cacheDir: cacheDir,
}
}
// AddReposRequest 批量添加仓库请求
type AddReposRequest struct {
URLs []string `json:"urls"`
}
// AddReposResponse 批量添加仓库响应
type AddReposResponse struct {
Total int `json:"total"`
Succeeded []AddRepoResult `json:"succeeded"`
Failed []AddRepoFailure `json:"failed"`
}
// AddRepoResult 添加仓库成功结果
type AddRepoResult struct {
RepoID int64 `json:"repo_id"`
URL string `json:"url"`
TaskID int64 `json:"task_id"`
}
// AddRepoFailure 添加仓库失败结果
type AddRepoFailure struct {
URL string `json:"url"`
Error string `json:"error"`
}
// AddRepos 批量添加仓库
func (s *RepoService) AddRepos(ctx context.Context, req *AddReposRequest) (*AddReposResponse, error) {
resp := &AddReposResponse{
Total: len(req.URLs),
Succeeded: make([]AddRepoResult, 0),
Failed: make([]AddRepoFailure, 0),
}
for _, url := range req.URLs {
// 校验URL
if !isValidGitURL(url) {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: "invalid git URL",
})
continue
}
// 检查是否已存在
existing, err := s.store.Repos().GetByURL(ctx, url)
if err != nil {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: fmt.Sprintf("failed to check existing repo: %v", err),
})
continue
}
if existing != nil {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: "repository already exists",
})
continue
}
// 创建仓库记录
repoName := extractRepoName(url)
localPath := filepath.Join(s.cacheDir, repoName)
repo := &models.Repository{
URL: url,
Name: repoName,
LocalPath: localPath,
Status: models.RepoStatusPending,
}
if err := s.store.Repos().Create(ctx, repo); err != nil {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: fmt.Sprintf("failed to create repository: %v", err),
})
continue
}
// 提交clone任务
task := &models.Task{
TaskType: models.TaskTypeClone,
RepoID: repo.ID,
Priority: 0,
}
if err := s.queue.Enqueue(ctx, task); err != nil {
resp.Failed = append(resp.Failed, AddRepoFailure{
URL: url,
Error: fmt.Sprintf("failed to enqueue clone task: %v", err),
})
continue
}
resp.Succeeded = append(resp.Succeeded, AddRepoResult{
RepoID: repo.ID,
URL: url,
TaskID: task.ID,
})
logger.Logger.Info().
Int64("repo_id", repo.ID).
Str("url", url).
Int64("task_id", task.ID).
Msg("repository added")
}
return resp, nil
}
// GetRepo 获取仓库详情
func (s *RepoService) GetRepo(ctx context.Context, id int64) (*models.Repository, error) {
return s.store.Repos().GetByID(ctx, id)
}
// 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)
}
// SwitchBranch 切换分支
func (s *RepoService) SwitchBranch(ctx context.Context, repoID int64, branch string) (*models.Task, error) {
// 检查仓库是否存在
repo, err := s.store.Repos().GetByID(ctx, repoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 创建切换分支任务
params := models.TaskParameters{
Branch: branch,
}
paramsJSON, _ := json.Marshal(params)
task := &models.Task{
TaskType: models.TaskTypeSwitch,
RepoID: repoID,
Parameters: string(paramsJSON),
Priority: 0,
}
if err := s.queue.Enqueue(ctx, task); err != nil {
return nil, err
}
logger.Logger.Info().
Int64("repo_id", repoID).
Str("branch", branch).
Int64("task_id", task.ID).
Msg("switch branch task submitted")
return task, nil
}
// UpdateRepo 更新仓库pull
func (s *RepoService) UpdateRepo(ctx context.Context, repoID int64) (*models.Task, error) {
// 检查仓库是否存在
repo, err := s.store.Repos().GetByID(ctx, repoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 创建pull任务
task := &models.Task{
TaskType: models.TaskTypePull,
RepoID: repoID,
Priority: 0,
}
if err := s.queue.Enqueue(ctx, task); err != nil {
return nil, err
}
logger.Logger.Info().
Int64("repo_id", repoID).
Int64("task_id", task.ID).
Msg("update task submitted")
return task, nil
}
// ResetRepo 重置仓库
func (s *RepoService) ResetRepo(ctx context.Context, repoID int64) (*models.Task, error) {
// 检查仓库是否存在
_, err := s.store.Repos().GetByID(ctx, repoID)
if err != nil {
return nil, err
}
// 创建reset任务
task := &models.Task{
TaskType: models.TaskTypeReset,
RepoID: repoID,
Priority: 1, // 高优先级
}
if err := s.queue.Enqueue(ctx, task); err != nil {
return nil, err
}
logger.Logger.Info().
Int64("repo_id", repoID).
Int64("task_id", task.ID).
Msg("reset task submitted")
return task, nil
}
// DeleteRepo 删除仓库
func (s *RepoService) DeleteRepo(ctx context.Context, id int64) error {
return s.store.Repos().Delete(ctx, id)
}
// isValidGitURL 校验Git URL
func isValidGitURL(url string) bool {
// 简单校验https:// 或 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 {
name := parts[len(parts)-1]
// 移除特殊字符
name = regexp.MustCompile(`[^a-zA-Z0-9_-]`).ReplaceAllString(name, "_")
return name
}
return "repo"
}

View File

@@ -0,0 +1,221 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gitcodestatic/gitcodestatic/internal/cache"
"github.com/gitcodestatic/gitcodestatic/internal/git"
"github.com/gitcodestatic/gitcodestatic/internal/logger"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
"github.com/gitcodestatic/gitcodestatic/internal/worker"
)
// StatsService 统计服务
type StatsService struct {
store storage.Store
queue *worker.Queue
cache *cache.FileCache
gitManager git.Manager
}
// NewStatsService 创建统计服务
func NewStatsService(store storage.Store, queue *worker.Queue, fileCache *cache.FileCache, gitManager git.Manager) *StatsService {
return &StatsService{
store: store,
queue: queue,
cache: fileCache,
gitManager: gitManager,
}
}
// CalculateRequest 统计请求
type CalculateRequest struct {
RepoID int64 `json:"repo_id"`
Branch string `json:"branch"`
Constraint *models.StatsConstraint `json:"constraint"`
}
// Calculate 触发统计计算
func (s *StatsService) Calculate(ctx context.Context, req *CalculateRequest) (*models.Task, error) {
// 校验参数
if err := ValidateStatsConstraint(req.Constraint); err != nil {
return nil, err
}
// 检查仓库
repo, err := s.store.Repos().GetByID(ctx, req.RepoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 创建统计任务
params := models.TaskParameters{
Branch: req.Branch,
Constraint: req.Constraint,
}
paramsJSON, _ := json.Marshal(params)
task := &models.Task{
TaskType: models.TaskTypeStats,
RepoID: req.RepoID,
Parameters: string(paramsJSON),
Priority: 0,
}
if err := s.queue.Enqueue(ctx, task); err != nil {
return nil, err
}
logger.Logger.Info().
Int64("repo_id", req.RepoID).
Str("branch", req.Branch).
Int64("task_id", task.ID).
Msg("stats task submitted")
return task, nil
}
// QueryResultRequest 查询统计结果请求
type QueryResultRequest struct {
RepoID int64 `json:"repo_id"`
Branch string `json:"branch"`
ConstraintType string `json:"constraint_type"`
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
Limit int `json:"limit,omitempty"`
}
// QueryResult 查询统计结果
func (s *StatsService) QueryResult(ctx context.Context, req *QueryResultRequest) (*models.StatsResult, error) {
// 检查仓库
repo, err := s.store.Repos().GetByID(ctx, req.RepoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 构建约束
constraint := &models.StatsConstraint{
Type: req.ConstraintType,
}
if req.ConstraintType == models.ConstraintTypeDateRange {
constraint.From = req.From
constraint.To = req.To
} else {
constraint.Limit = req.Limit
}
// 获取当前HEAD commit hash
commitHash, err := s.gitManager.GetHeadCommitHash(ctx, repo.LocalPath)
if err != nil {
return nil, fmt.Errorf("failed to get HEAD commit hash: %w", err)
}
// 生成缓存键
cacheKey := cache.GenerateCacheKey(req.RepoID, req.Branch, constraint, commitHash)
// 查询缓存
result, err := s.cache.Get(ctx, cacheKey)
if err != nil {
logger.Logger.Warn().Err(err).Str("cache_key", cacheKey).Msg("failed to get cache")
}
if result != nil {
return result, nil
}
// 缓存未命中
return nil, errors.New("statistics not found, please submit calculation task first")
}
// CountCommitsRequest 统计提交次数请求
type CountCommitsRequest struct {
RepoID int64 `json:"repo_id"`
Branch string `json:"branch"`
From string `json:"from"`
}
// CountCommitsResponse 统计提交次数响应
type CountCommitsResponse struct {
RepoID int64 `json:"repo_id"`
Branch string `json:"branch"`
From string `json:"from"`
To string `json:"to"`
CommitCount int `json:"commit_count"`
}
// CountCommits 统计提交次数(辅助查询)
func (s *StatsService) CountCommits(ctx context.Context, req *CountCommitsRequest) (*CountCommitsResponse, error) {
// 检查仓库
repo, err := s.store.Repos().GetByID(ctx, req.RepoID)
if err != nil {
return nil, err
}
if repo.Status != models.RepoStatusReady {
return nil, errors.New("repository is not ready")
}
// 统计提交次数
count, err := s.gitManager.CountCommits(ctx, repo.LocalPath, req.Branch, req.From)
if err != nil {
return nil, fmt.Errorf("failed to count commits: %w", err)
}
resp := &CountCommitsResponse{
RepoID: req.RepoID,
Branch: req.Branch,
From: req.From,
To: "HEAD",
CommitCount: count,
}
logger.Logger.Info().
Int64("repo_id", req.RepoID).
Str("branch", req.Branch).
Str("from", req.From).
Int("count", count).
Msg("commits counted")
return resp, nil
}
// ValidateStatsConstraint 校验统计约束
func ValidateStatsConstraint(constraint *models.StatsConstraint) error {
if constraint == nil {
return errors.New("constraint is required")
}
if constraint.Type != models.ConstraintTypeDateRange && constraint.Type != models.ConstraintTypeCommitLimit {
return fmt.Errorf("constraint type must be %s or %s", models.ConstraintTypeDateRange, models.ConstraintTypeCommitLimit)
}
if constraint.Type == models.ConstraintTypeDateRange {
if constraint.From == "" || constraint.To == "" {
return fmt.Errorf("%s requires both from and to", models.ConstraintTypeDateRange)
}
if constraint.Limit != 0 {
return fmt.Errorf("%s cannot be used with limit", models.ConstraintTypeDateRange)
}
} else if constraint.Type == models.ConstraintTypeCommitLimit {
if constraint.Limit <= 0 {
return fmt.Errorf("%s requires positive limit value", models.ConstraintTypeCommitLimit)
}
if constraint.From != "" || constraint.To != "" {
return fmt.Errorf("%s cannot be used with date range", models.ConstraintTypeCommitLimit)
}
}
return nil
}

View File

@@ -0,0 +1,35 @@
package service
import (
"context"
"github.com/gitcodestatic/gitcodestatic/internal/models"
"github.com/gitcodestatic/gitcodestatic/internal/storage"
)
// TaskService 任务服务
type TaskService struct {
store storage.Store
}
// NewTaskService 创建任务服务
func NewTaskService(store storage.Store) *TaskService {
return &TaskService{
store: store,
}
}
// GetTask 获取任务详情
func (s *TaskService) GetTask(ctx context.Context, id int64) (*models.Task, error) {
return s.store.Tasks().GetByID(ctx, id)
}
// ListTasks 获取任务列表
func (s *TaskService) ListTasks(ctx context.Context, repoID int64, status string, page, pageSize int) ([]*models.Task, int, error) {
return s.store.Tasks().List(ctx, repoID, status, page, pageSize)
}
// CancelTask 取消任务
func (s *TaskService) CancelTask(ctx context.Context, id int64) error {
return s.store.Tasks().Cancel(ctx, id)
}