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 }