2026-06-18 22:48:16 +08:00
package handlers
import (
"database/sql"
2026-06-18 22:59:51 +08:00
"encoding/json"
"fmt"
2026-06-18 22:48:16 +08:00
"net/http"
"os"
"path/filepath"
2026-06-18 22:59:51 +08:00
"strconv"
"time"
2026-06-18 22:48:16 +08:00
2026-06-18 22:59:51 +08:00
"github.com/HoHD/PR-Helper/services"
2026-06-18 22:48:16 +08:00
"github.com/gin-gonic/gin"
)
type ReposHandler struct {
2026-06-18 22:59:51 +08:00
db * sql . DB
2026-06-18 22:48:16 +08:00
reposDir string
}
func NewReposHandler ( db * sql . DB , reposDir string ) * ReposHandler {
return & ReposHandler { db : db , reposDir : reposDir }
}
func ( h * ReposHandler ) ListRepos ( c * gin . Context ) {
2026-06-20 21:57:46 +08:00
user := GetCurrentUser ( c )
if user == nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "未登录" })
2026-06-20 21:57:46 +08:00
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 )
2026-06-18 22:48:16 +08:00
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 ,
})
}
}
2026-06-20 00:06:00 +08:00
if err := rows . Err (); err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
2026-06-18 22:48:16 +08:00
if repos == nil {
repos = [] gin . H {}
}
c . JSON ( http . StatusOK , repos )
}
func ( h * ReposHandler ) DeleteRepo ( c * gin . Context ) {
2026-06-20 21:57:46 +08:00
user := GetCurrentUser ( c )
if user == nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "未登录" })
2026-06-20 21:57:46 +08:00
return
}
2026-06-18 22:48:16 +08:00
id := c . Param ( "id" )
var localPath string
2026-06-20 21:57:46 +08:00
err := h . db . QueryRow ( `SELECT local_path FROM repositories WHERE id = ? AND user_id = ?` , id , user . ID ). Scan ( & localPath )
2026-06-18 22:48:16 +08:00
if err == sql . ErrNoRows {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusNotFound , gin . H { "error" : "仓库未找到" })
2026-06-18 22:48:16 +08:00
return
}
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
2026-06-20 00:06:00 +08:00
if err := os . RemoveAll ( localPath ); err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "remove repo dir: " + err . Error ()})
return
}
2026-06-20 21:57:46 +08:00
if _ , err := h . db . Exec ( `DELETE FROM analyses WHERE repo_id = ? AND user_id = ?` , id , user . ID ); err != nil {
2026-06-20 00:06:00 +08:00
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "delete analyses: " + err . Error ()})
return
}
2026-06-20 21:57:46 +08:00
if _ , err := h . db . Exec ( `DELETE FROM repositories WHERE id = ? AND user_id = ?` , id , user . ID ); err != nil {
2026-06-20 00:06:00 +08:00
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "delete repository: " + err . Error ()})
return
}
2026-06-18 22:48:16 +08:00
c . JSON ( http . StatusOK , gin . H { "ok" : true })
}
func ( h * ReposHandler ) CleanupRepos ( c * gin . Context ) {
2026-06-20 21:57:46 +08:00
user := GetCurrentUser ( c )
if user == nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "未登录" })
2026-06-20 21:57:46 +08:00
return
}
2026-06-18 22:48:16 +08:00
var maxAgeDays string
2026-06-20 23:21:24 +08:00
h . db . QueryRow ( "SELECT value FROM user_settings WHERE user_id = ? AND `key` = 'cache.max_age_days'" , user . ID ). Scan ( & maxAgeDays )
2026-06-18 22:48:16 +08:00
if maxAgeDays == "" {
maxAgeDays = "7"
}
2026-06-20 22:40:46 +08:00
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 )
2026-06-18 22:48:16 +08:00
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
defer rows . Close ()
var cleaned [] int64
2026-06-20 00:06:00 +08:00
var errs [] string
2026-06-18 22:48:16 +08:00
for rows . Next () {
var id int64
var localPath string
if rows . Scan ( & id , & localPath ) == nil {
2026-06-20 00:06:00 +08:00
if err := os . RemoveAll ( localPath ); err != nil {
errs = append ( errs , fmt . Sprintf ( "remove %d: %s" , id , err . Error ()))
continue
}
2026-06-20 21:57:46 +08:00
if _ , err := h . db . Exec ( `DELETE FROM analyses WHERE repo_id = ? AND user_id = ?` , id , user . ID ); err != nil {
2026-06-20 00:06:00 +08:00
errs = append ( errs , fmt . Sprintf ( "delete analyses %d: %s" , id , err . Error ()))
}
2026-06-20 21:57:46 +08:00
if _ , err := h . db . Exec ( `DELETE FROM repositories WHERE id = ? AND user_id = ?` , id , user . ID ); err != nil {
2026-06-20 00:06:00 +08:00
errs = append ( errs , fmt . Sprintf ( "delete repo %d: %s" , id , err . Error ()))
}
2026-06-18 22:48:16 +08:00
cleaned = append ( cleaned , id )
}
}
2026-06-20 00:06:00 +08:00
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 )
2026-06-18 22:48:16 +08:00
}
2026-06-21 14:31:33 +08:00
// 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 {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "未登录" })
2026-06-21 14:31:33 +08:00
return
}
id := c . Param ( "id" )
2026-06-21 14:58:12 +08:00
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 )
2026-06-21 14:31:33 +08:00
if err == sql . ErrNoRows {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusNotFound , gin . H { "error" : "仓库未找到" })
2026-06-21 14:31:33 +08:00
return
}
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
2026-06-21 14:58:12 +08:00
// 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 )
2026-06-21 14:31:33 +08:00
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 ,
})
}
2026-06-18 22:59:51 +08:00
// CloneRepo handles POST /api/repos with SSE progress events.
2026-06-18 22:48:16 +08:00
func ( h * ReposHandler ) CloneRepo ( c * gin . Context ) {
2026-06-20 21:57:46 +08:00
user := GetCurrentUser ( c )
if user == nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "未登录" })
2026-06-20 21:57:46 +08:00
return
}
2026-06-18 22:59:51 +08:00
var req struct {
URL string `json:"url" binding:"required"`
Username string `json:"username"`
Password string `json:"password"`
}
if err := c . ShouldBindJSON ( & req ); err != nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusBadRequest , gin . H { "error" : "请输入仓库地址" })
2026-06-18 22:59:51 +08:00
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" )
2026-06-18 23:04:58 +08:00
c . Status ( http . StatusOK )
2026-06-18 22:59:51 +08:00
flusher , ok := c . Writer .( http . Flusher )
if ! ok {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "服务器不支持流式传输" })
2026-06-18 22:59:51 +08:00
return
}
sendEvent := func ( event string , data interface {}) {
2026-06-20 00:06:00 +08:00
jsonData , err := json . Marshal ( data )
if err != nil {
2026-06-21 15:11:20 +08:00
jsonData = [] byte ( `{"error":"序列化事件数据失败"}` )
2026-06-20 00:06:00 +08:00
}
2026-06-18 22:59:51 +08:00
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
}
2026-06-21 14:58:12 +08:00
// 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
}
2026-06-20 21:57:46 +08:00
// Save to database with user_id
2026-06-21 14:39:36 +08:00
now := time . Now (). Format ( "2006-01-02 15:04:05" )
2026-06-21 14:58:12 +08:00
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 )
2026-06-18 22:59:51 +08:00
if err != nil {
sendEvent ( "error" , map [ string ] interface {}{ "message" : "save to db: " + err . Error ()})
return
}
2026-06-20 00:06:00 +08:00
repoID , err := res . LastInsertId ()
if err != nil {
sendEvent ( "error" , map [ string ] interface {}{ "message" : "get repo id: " + err . Error ()})
return
}
2026-06-18 22:59:51 +08:00
sendEvent ( "complete" , map [ string ] interface {}{
"repo_id" : repoID ,
"size_bytes" : result . SizeBytes ,
})
2026-06-18 22:48:16 +08:00
}
2026-06-18 22:59:51 +08:00
// GetGraph handles GET /api/repos/:id/graph — returns D3.js-compatible data.
2026-06-18 22:48:16 +08:00
func ( h * ReposHandler ) GetGraph ( c * gin . Context ) {
2026-06-20 21:57:46 +08:00
user := GetCurrentUser ( c )
if user == nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "未登录" })
2026-06-20 21:57:46 +08:00
return
}
2026-06-18 22:59:51 +08:00
id := c . Param ( "id" )
var localPath string
2026-06-20 21:57:46 +08:00
err := h . db . QueryRow ( `SELECT local_path FROM repositories WHERE id = ? AND user_id = ?` , id , user . ID ). Scan ( & localPath )
2026-06-18 22:59:51 +08:00
if err == sql . ErrNoRows {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusNotFound , gin . H { "error" : "仓库未找到" })
2026-06-18 22:59:51 +08:00
return
}
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
// Update last_used
2026-06-20 22:40:46 +08:00
h . db . Exec ( `UPDATE repositories SET last_used = NOW() WHERE id = ?` , id )
2026-06-18 22:59:51 +08:00
repo , err := services . OpenRepo ( localPath )
if err != nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "打开仓库失败: " + err . Error ()})
2026-06-18 22:59:51 +08:00
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 )
2026-06-18 22:48:16 +08:00
}
2026-06-18 22:59:51 +08:00
// GetDiff handles GET /api/repos/:id/diff — returns unified diff or per-file diffs.
2026-06-18 22:48:16 +08:00
func ( h * ReposHandler ) GetDiff ( c * gin . Context ) {
2026-06-20 21:57:46 +08:00
user := GetCurrentUser ( c )
if user == nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "未登录" })
2026-06-20 21:57:46 +08:00
return
}
2026-06-18 22:59:51 +08:00
id := c . Param ( "id" )
base := c . Query ( "base" )
head := c . Query ( "head" )
if base == "" || head == "" {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusBadRequest , gin . H { "error" : "请选择 Base 和 Head 分支" })
2026-06-18 22:59:51 +08:00
return
}
var localPath string
2026-06-20 21:57:46 +08:00
err := h . db . QueryRow ( `SELECT local_path FROM repositories WHERE id = ? AND user_id = ?` , id , user . ID ). Scan ( & localPath )
2026-06-18 22:59:51 +08:00
if err == sql . ErrNoRows {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusNotFound , gin . H { "error" : "仓库未找到" })
2026-06-18 22:59:51 +08:00
return
}
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ()})
return
}
// Update last_used
2026-06-20 22:40:46 +08:00
h . db . Exec ( `UPDATE repositories SET last_used = NOW() WHERE id = ?` , id )
2026-06-18 22:59:51 +08:00
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 ) {
2026-06-20 21:57:46 +08:00
user := GetCurrentUser ( c )
if user == nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "未登录" })
2026-06-20 21:57:46 +08:00
return
}
2026-06-18 22:59:51 +08:00
id := c . Param ( "id" )
var localPath string
2026-06-20 21:57:46 +08:00
err := h . db . QueryRow ( `SELECT local_path FROM repositories WHERE id = ? AND user_id = ?` , id , user . ID ). Scan ( & localPath )
2026-06-18 22:59:51 +08:00
if err == sql . ErrNoRows {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusNotFound , gin . H { "error" : "仓库未找到" })
2026-06-18 22:59:51 +08:00
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 ) {
2026-06-20 21:57:46 +08:00
user := GetCurrentUser ( c )
if user == nil {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "未登录" })
2026-06-20 21:57:46 +08:00
return
}
2026-06-18 22:59:51 +08:00
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
2026-06-20 21:57:46 +08:00
err := h . db . QueryRow ( `SELECT local_path FROM repositories WHERE id = ? AND user_id = ?` , id , user . ID ). Scan ( & localPath )
2026-06-18 22:59:51 +08:00
if err == sql . ErrNoRows {
2026-06-21 15:11:20 +08:00
c . JSON ( http . StatusNotFound , gin . H { "error" : "仓库未找到" })
2026-06-18 22:59:51 +08:00
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 )
2026-06-18 22:48:16 +08:00
}
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
}