feat: Phase 1 — 项目骨架搭建,集成 Gin、SQLite 和设置页面
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type GenerateHandler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewGenerateHandler(db *sql.DB) *GenerateHandler {
|
||||
return &GenerateHandler{db: db}
|
||||
}
|
||||
|
||||
// Generate handles POST /api/repos/:id/generate — stub for Phase 4
|
||||
func (h *GenerateHandler) Generate(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "generate not implemented yet — coming in Phase 4"})
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/HoHD/PR-Helper/models"
|
||||
)
|
||||
|
||||
type PageHandler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewPageHandler(db *sql.DB) *PageHandler {
|
||||
return &PageHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *PageHandler) Index(c *gin.Context) {
|
||||
repos, _ := h.listRepos()
|
||||
c.HTML(http.StatusOK, "pages/index.html", gin.H{
|
||||
"Repos": repos,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PageHandler) Repo(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "pages/repo.html", gin.H{
|
||||
"ID": c.Param("id"),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PageHandler) Generate(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "pages/generate.html", gin.H{
|
||||
"ID": c.Param("id"),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PageHandler) Review(c *gin.Context) {
|
||||
topN := "20"
|
||||
h.db.QueryRow(`SELECT value FROM settings WHERE key = 'review.top_n'`).Scan(&topN)
|
||||
c.HTML(http.StatusOK, "pages/review.html", gin.H{
|
||||
"ID": c.Param("id"),
|
||||
"TopN": topN,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PageHandler) Settings(c *gin.Context) {
|
||||
settings := make(map[string]string)
|
||||
for key := range models.DefaultSettings {
|
||||
settings[key] = ""
|
||||
}
|
||||
rows, err := h.db.Query(`SELECT key, value FROM settings`)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var k, v string
|
||||
if rows.Scan(&k, &v) == nil {
|
||||
settings[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
c.HTML(http.StatusOK, "pages/settings.html", gin.H{
|
||||
"Settings": settings,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PageHandler) listRepos() ([]models.Repository, error) {
|
||||
rows, err := h.db.Query(`SELECT id, url, local_path, size_bytes, cloned_at, last_used FROM repositories ORDER BY last_used DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var repos []models.Repository
|
||||
for rows.Next() {
|
||||
var r models.Repository
|
||||
if err := rows.Scan(&r.ID, &r.URL, &r.LocalPath, &r.SizeBytes, &r.ClonedAt, &r.LastUsed); err != nil {
|
||||
continue
|
||||
}
|
||||
repos = append(repos, r)
|
||||
}
|
||||
return repos, nil
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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) {
|
||||
rows, err := h.db.Query(`SELECT id, url, local_path, size_bytes, cloned_at, last_used FROM repositories ORDER BY last_used DESC`)
|
||||
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 repos == nil {
|
||||
repos = []gin.H{}
|
||||
}
|
||||
c.JSON(http.StatusOK, repos)
|
||||
}
|
||||
|
||||
func (h *ReposHandler) DeleteRepo(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var localPath string
|
||||
err := h.db.QueryRow(`SELECT local_path FROM repositories WHERE id = ?`, id).Scan(&localPath)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "repository not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// Remove from filesystem
|
||||
os.RemoveAll(localPath)
|
||||
// Remove from database
|
||||
h.db.Exec(`DELETE FROM analyses WHERE repo_id = ?`, id)
|
||||
h.db.Exec(`DELETE FROM repositories WHERE id = ?`, id)
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *ReposHandler) CleanupRepos(c *gin.Context) {
|
||||
// Get max age from settings
|
||||
var maxAgeDays string
|
||||
h.db.QueryRow(`SELECT value FROM settings WHERE key = 'cache.max_age_days'`).Scan(&maxAgeDays)
|
||||
if maxAgeDays == "" {
|
||||
maxAgeDays = "7"
|
||||
}
|
||||
// Find expired repos
|
||||
rows, err := h.db.Query(`SELECT id, local_path FROM repositories WHERE last_used < datetime('now', '-' || ? || ' days')`, maxAgeDays)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
var cleaned []int64
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var localPath string
|
||||
if rows.Scan(&id, &localPath) == nil {
|
||||
os.RemoveAll(localPath)
|
||||
h.db.Exec(`DELETE FROM analyses WHERE repo_id = ?`, id)
|
||||
h.db.Exec(`DELETE FROM repositories WHERE id = ?`, id)
|
||||
cleaned = append(cleaned, id)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"cleaned": cleaned})
|
||||
}
|
||||
|
||||
// CloneRepo handles POST /api/repos — stub for Phase 2
|
||||
func (h *ReposHandler) CloneRepo(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "clone not implemented yet — coming in Phase 2"})
|
||||
}
|
||||
|
||||
// GetGraph handles GET /api/repos/:id/graph — stub for Phase 2
|
||||
func (h *ReposHandler) GetGraph(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "graph not implemented yet — coming in Phase 2"})
|
||||
}
|
||||
|
||||
// GetDiff handles GET /api/repos/:id/diff — stub for Phase 2
|
||||
func (h *ReposHandler) GetDiff(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "diff not implemented yet — coming in Phase 2"})
|
||||
}
|
||||
|
||||
// dirSize returns the total size of a directory in bytes
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ReviewHandler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewReviewHandler(db *sql.DB) *ReviewHandler {
|
||||
return &ReviewHandler{db: db}
|
||||
}
|
||||
|
||||
// Review handles POST /api/repos/:id/review — stub for Phase 4
|
||||
func (h *ReviewHandler) Review(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "review not implemented yet — coming in Phase 4"})
|
||||
}
|
||||
|
||||
// SaveNotes handles POST /api/repos/:id/review/notes — stub for Phase 5
|
||||
func (h *ReviewHandler) SaveNotes(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "notes not implemented yet — coming in Phase 5"})
|
||||
}
|
||||
|
||||
// GetNotes handles GET /api/repos/:id/review/notes — stub for Phase 5
|
||||
func (h *ReviewHandler) GetNotes(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "notes not implemented yet — coming in Phase 5"})
|
||||
}
|
||||
|
||||
// GeneratePDF handles POST /api/repos/:id/review/pdf — stub for Phase 5
|
||||
func (h *ReviewHandler) GeneratePDF(c *gin.Context) {
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "pdf not implemented yet — coming in Phase 5"})
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SettingsHandler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSettingsHandler(db *sql.DB) *SettingsHandler {
|
||||
return &SettingsHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *SettingsHandler) GetSettings(c *gin.Context) {
|
||||
settings := make(map[string]string)
|
||||
rows, err := h.db.Query(`SELECT key, value FROM settings`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var k, v string
|
||||
if rows.Scan(&k, &v) == nil {
|
||||
settings[k] = v
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, settings)
|
||||
}
|
||||
|
||||
func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
|
||||
var body map[string]string
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
tx, err := h.db.Begin()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
for key, val := range body {
|
||||
_, err := tx.Exec(`INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)`, key, val)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
Reference in New Issue
Block a user