Files
GitCodeStatic/internal/service/repo_service.go
2025-12-31 16:23:40 +08:00

362 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"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"
"github.com/gitcodestatic/gitcodestatic/internal/worker"
)
// RepoService 仓库服务
type RepoService struct {
store storage.Store
queue *worker.Queue
cacheDir string
gitManager git.Manager
}
// NewRepoService 创建仓库服务
func NewRepoService(store storage.Store, queue *worker.Queue, cacheDir string, gitManager git.Manager) *RepoService {
return &RepoService{
store: store,
queue: queue,
cacheDir: cacheDir,
gitManager: gitManager,
}
}
// AddReposRequest 批量添加仓库请求
// RepoInput 仓库输入
type RepoInput struct {
URL string `json:"url"`
Branch string `json:"branch"`
}
type AddReposRequest struct {
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"`
}
// 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.Repos),
Succeeded: make([]AddRepoResult, 0),
Failed: make([]AddRepoFailure, 0),
}
// 如果提供了认证信息,创建凭据
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{
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,
CurrentBranch: branch,
LocalPath: localPath,
Status: models.RepoStatusPending,
CredentialID: credentialID,
}
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).
Bool("has_credentials", credentialID != nil).
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) {
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 切换分支
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)
}
// 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@")
}
// 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"
}
// generateCredentialID 生成凭据ID
func generateCredentialID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}