package handlers import ( "database/sql" "encoding/json" "fmt" "net/http" "os" "path/filepath" "strconv" "time" "github.com/HoHD/PR-Helper/services" "github.com/gin-gonic/gin" ) type ReposHandler struct { db *sql.DB reposDir string } func NewReposHandler(db *sql.DB, reposDir string) *ReposHandler { return &ReposHandler{db: db, reposDir: reposDir} } func (h *ReposHandler) ListRepos(c *gin.Context) { user := GetCurrentUser(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } rows, err := h.db.Query(`SELECT id, url, local_path, size_bytes, cloned_at, last_used FROM repositories WHERE user_id = ? ORDER BY last_used DESC`, user.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var repos []gin.H for rows.Next() { var id int64 var url, localPath string var sizeBytes int64 var clonedAt, lastUsed string if rows.Scan(&id, &url, &localPath, &sizeBytes, &clonedAt, &lastUsed) == nil { repos = append(repos, gin.H{ "id": id, "url": url, "local_path": localPath, "size_bytes": sizeBytes, "cloned_at": clonedAt, "last_used": lastUsed, }) } } if err := rows.Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if repos == nil { repos = []gin.H{} } c.JSON(http.StatusOK, repos) } func (h *ReposHandler) DeleteRepo(c *gin.Context) { user := GetCurrentUser(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } id := c.Param("id") var localPath string err := h.db.QueryRow(`SELECT local_path FROM repositories WHERE id = ? AND user_id = ?`, id, user.ID).Scan(&localPath) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "仓库未找到"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := os.RemoveAll(localPath); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "remove repo dir: " + err.Error()}) return } if _, err := h.db.Exec(`DELETE FROM analyses WHERE repo_id = ? AND user_id = ?`, id, user.ID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "delete analyses: " + err.Error()}) return } if _, err := h.db.Exec(`DELETE FROM repositories WHERE id = ? AND user_id = ?`, id, user.ID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "delete repository: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *ReposHandler) CleanupRepos(c *gin.Context) { user := GetCurrentUser(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } var maxAgeDays string h.db.QueryRow("SELECT value FROM user_settings WHERE user_id = ? AND `key` = 'cache.max_age_days'", user.ID).Scan(&maxAgeDays) if maxAgeDays == "" { maxAgeDays = "7" } rows, err := h.db.Query(`SELECT id, local_path FROM repositories WHERE user_id = ? AND last_used < DATE_SUB(NOW(), INTERVAL ? DAY)`, user.ID, maxAgeDays) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var cleaned []int64 var errs []string for rows.Next() { var id int64 var localPath string if rows.Scan(&id, &localPath) == nil { if err := os.RemoveAll(localPath); err != nil { errs = append(errs, fmt.Sprintf("remove %d: %s", id, err.Error())) continue } if _, err := h.db.Exec(`DELETE FROM analyses WHERE repo_id = ? AND user_id = ?`, id, user.ID); err != nil { errs = append(errs, fmt.Sprintf("delete analyses %d: %s", id, err.Error())) } if _, err := h.db.Exec(`DELETE FROM repositories WHERE id = ? AND user_id = ?`, id, user.ID); err != nil { errs = append(errs, fmt.Sprintf("delete repo %d: %s", id, err.Error())) } cleaned = append(cleaned, id) } } if err := rows.Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } result := gin.H{"cleaned": cleaned} if len(errs) > 0 { result["errors"] = errs } c.JSON(http.StatusOK, result) } // PullRepo handles POST /api/repos/:id/pull — fetches and merges latest changes. func (h *ReposHandler) PullRepo(c *gin.Context) { user := GetCurrentUser(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } id := c.Param("id") var localPath, authType string var credential sql.NullString err := h.db.QueryRow(`SELECT local_path, auth_type, credential FROM repositories WHERE id = ? AND user_id = ?`, id, user.ID).Scan(&localPath, &authType, &credential) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "仓库未找到"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Parse stored credentials var username, password string if authType == "basic" && credential.Valid { var cred struct { Username string `json:"username"` Password string `json:"password"` } json.Unmarshal([]byte(credential.String), &cred) username = cred.Username password = cred.Password } result, err := services.PullRepo(localPath, username, password) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } h.db.Exec(`UPDATE repositories SET size_bytes = ?, last_used = NOW() WHERE id = ?`, result.SizeBytes, id) c.JSON(http.StatusOK, gin.H{ "ok": true, "repo_id": id, "size_bytes": result.SizeBytes, }) } // CloneRepo handles POST /api/repos with SSE progress events. func (h *ReposHandler) CloneRepo(c *gin.Context) { user := GetCurrentUser(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } var req struct { URL string `json:"url" binding:"required"` Username string `json:"username"` Password string `json:"password"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请输入仓库地址"}) return } // Set SSE headers c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("X-Accel-Buffering", "no") c.Status(http.StatusOK) flusher, ok := c.Writer.(http.Flusher) if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器不支持流式传输"}) return } sendEvent := func(event string, data interface{}) { jsonData, err := json.Marshal(data) if err != nil { jsonData = []byte(`{"error":"序列化事件数据失败"}`) } fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, jsonData) flusher.Flush() } sendEvent("start", map[string]interface{}{"url": req.URL}) // Generate unique directory name repoName := filepath.Base(req.URL) if repoName == "" || repoName == "." || repoName == "/" { repoName = fmt.Sprintf("repo_%d", time.Now().UnixNano()) } repoDir := filepath.Join(h.reposDir, fmt.Sprintf("%s_%d", repoName, time.Now().UnixNano())) result, err := services.Clone(services.CloneOptions{ URL: req.URL, Dir: repoDir, Username: req.Username, Password: req.Password, }, func(event string, data interface{}) { sendEvent(event, data) }) if err != nil { sendEvent("error", map[string]interface{}{"message": err.Error()}) return } // Determine auth type and credential for persistence authType := "none" var credential *string if req.Username != "" || req.Password != "" { authType = "basic" credJSON, _ := json.Marshal(map[string]string{ "username": req.Username, "password": req.Password, }) s := string(credJSON) credential = &s } // Save to database with user_id now := time.Now().Format("2006-01-02 15:04:05") res, err := h.db.Exec(`INSERT INTO repositories (user_id, url, local_path, size_bytes, cloned_at, last_used, auth_type, credential) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, user.ID, req.URL, repoDir, result.SizeBytes, now, now, authType, credential) if err != nil { sendEvent("error", map[string]interface{}{"message": "save to db: " + err.Error()}) return } repoID, err := res.LastInsertId() if err != nil { sendEvent("error", map[string]interface{}{"message": "get repo id: " + err.Error()}) return } sendEvent("complete", map[string]interface{}{ "repo_id": repoID, "size_bytes": result.SizeBytes, }) } // GetGraph handles GET /api/repos/:id/graph — returns D3.js-compatible data. func (h *ReposHandler) GetGraph(c *gin.Context) { user := GetCurrentUser(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } id := c.Param("id") var localPath string err := h.db.QueryRow(`SELECT local_path FROM repositories WHERE id = ? AND user_id = ?`, id, user.ID).Scan(&localPath) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "仓库未找到"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Update last_used h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id) repo, err := services.OpenRepo(localPath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "打开仓库失败: " + err.Error()}) return } maxCommits := 200 if mc := c.Query("max_commits"); mc != "" { if n, err := strconv.Atoi(mc); err == nil && n > 0 { maxCommits = n } } graph, err := services.GetGraph(repo, maxCommits) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, graph) } // GetDiff handles GET /api/repos/:id/diff — returns unified diff or per-file diffs. func (h *ReposHandler) GetDiff(c *gin.Context) { user := GetCurrentUser(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } id := c.Param("id") base := c.Query("base") head := c.Query("head") if base == "" || head == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择 Base 和 Head 分支"}) return } var localPath string err := h.db.QueryRow(`SELECT local_path FROM repositories WHERE id = ? AND user_id = ?`, id, user.ID).Scan(&localPath) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "仓库未找到"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Update last_used h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id) repo, err := services.OpenRepo(localPath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Check if per-file mode is requested if c.Query("per_file") == "true" { files, err := services.GetDiffFiles(repo, base, head) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, files) return } diff, err := services.GetDiff(repo, base, head) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"diff": diff}) } // GetRefs handles GET /api/repos/:id/refs — returns branches and tags. func (h *ReposHandler) GetRefs(c *gin.Context) { user := GetCurrentUser(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } id := c.Param("id") var localPath string err := h.db.QueryRow(`SELECT local_path FROM repositories WHERE id = ? AND user_id = ?`, id, user.ID).Scan(&localPath) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "仓库未找到"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } repo, err := services.OpenRepo(localPath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } refs, err := services.GetRefs(repo) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, refs) } // GetCommits handles GET /api/repos/:id/commits — returns commit log for a ref. func (h *ReposHandler) GetCommits(c *gin.Context) { user := GetCurrentUser(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } id := c.Param("id") refName := c.Query("ref") if refName == "" { refName = "HEAD" } maxCommits := 100 if mc := c.Query("limit"); mc != "" { if n, err := strconv.Atoi(mc); err == nil && n > 0 { maxCommits = n } } var localPath string err := h.db.QueryRow(`SELECT local_path FROM repositories WHERE id = ? AND user_id = ?`, id, user.ID).Scan(&localPath) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "仓库未找到"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } commits, err := services.GetBranchCommits(localPath, refName, maxCommits) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, commits) } func dirSize(path string) int64 { var size int64 filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } size += info.Size() return nil }) return size }