feat: migrate database from SQLite to MySQL with .env configuration
- Replace go-sqlite3 with go-sql-driver/mysql (pure Go, no CGO needed)
- Add joho/godotenv for .env file loading
- Rewrite database/db.go with MySQL-compatible DDL (BIGINT AUTO_INCREMENT, InnoDB, utf8mb4)
- Convert all SQLite-specific SQL: INSERT OR IGNORE → INSERT IGNORE,
INSERT OR REPLACE → INSERT ... ON DUPLICATE KEY UPDATE,
datetime('now') → NOW(), date arithmetic → DATE_SUB()
- Add MySQL config fields to config.go (host/port/user/password/database)
- Add .env.example with connection parameter template
- Update Dockerfile: remove CGO/gcc/musl dependency, smaller build
- Update docker-compose.yml: add MySQL service container with healthcheck
- Update CLAUDE.md documentation
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
# Server
|
||||||
|
PORT=8080
|
||||||
|
GIN_MODE=debug
|
||||||
|
SESSION_SECRET=
|
||||||
|
|
||||||
|
# Data directory (cloned repos, etc.)
|
||||||
|
DATA_DIR=data
|
||||||
|
|
||||||
|
# MySQL
|
||||||
|
MYSQL_HOST=127.0.0.1
|
||||||
|
MYSQL_PORT=3306
|
||||||
|
MYSQL_USER=root
|
||||||
|
MYSQL_PASSWORD=
|
||||||
|
MYSQL_DATABASE=pr_helper
|
||||||
@@ -8,7 +8,7 @@ PR-Helper is a self-hosted web service that auto-generates PR descriptions from
|
|||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Backend**: Go + Gin + SQLite (go-sqlite3) + go-git
|
- **Backend**: Go + Gin + MySQL + go-git
|
||||||
- **Frontend**: Go html/template + HTMX + D3.js + diff2html + Tailwind CSS
|
- **Frontend**: Go html/template + HTMX + D3.js + diff2html + Tailwind CSS
|
||||||
- **LLM**: OpenAI-compatible API (SSE streaming)
|
- **LLM**: OpenAI-compatible API (SSE streaming)
|
||||||
- **Deploy**: Docker
|
- **Deploy**: Docker
|
||||||
@@ -16,14 +16,17 @@ PR-Helper is a self-hosted web service that auto-generates PR descriptions from
|
|||||||
## Build & Run
|
## Build & Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build (CGO required for SQLite)
|
# Build
|
||||||
CGO_ENABLED=1 go build -o pr-helper .
|
go build -o pr-helper .
|
||||||
|
|
||||||
# Run
|
# Configure — copy .env.example to .env and set MySQL credentials
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Run (requires MySQL server)
|
||||||
./pr-helper
|
./pr-helper
|
||||||
# Server listens on :8080
|
# Server listens on :8080
|
||||||
|
|
||||||
# Docker
|
# Docker (includes MySQL container)
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -33,13 +36,13 @@ docker compose up --build
|
|||||||
handlers/ → HTTP handlers (pages + JSON API + SSE endpoints)
|
handlers/ → HTTP handlers (pages + JSON API + SSE endpoints)
|
||||||
services/ → Business logic (git ops, LLM calls, PDF gen, cache management)
|
services/ → Business logic (git ops, LLM calls, PDF gen, cache management)
|
||||||
models/ → Data models (repository, settings, analysis)
|
models/ → Data models (repository, settings, analysis)
|
||||||
database/ → SQLite init and migrations
|
database/ → MySQL init and migrations
|
||||||
config/ → Configuration loading
|
config/ → Configuration loading
|
||||||
templates/ → Go HTML templates (layouts/, pages/, partials/)
|
templates/ → Go HTML templates (layouts/, pages/, partials/)
|
||||||
static/ → CSS (Tailwind output), JS (graph, diff-viewer, sse), vendor libs
|
static/ → CSS (Tailwind output), JS (graph, diff-viewer, sse), vendor libs
|
||||||
```
|
```
|
||||||
|
|
||||||
**Data flow**: Browser ↔ Gin handlers → services (git/llm) → SQLite + filesystem (`data/`)
|
**Data flow**: Browser ↔ Gin handlers → services (git/llm) → MySQL + filesystem (`data/`)
|
||||||
|
|
||||||
**Key service layer responsibilities**:
|
**Key service layer responsibilities**:
|
||||||
- `services/git.go` — clone, diff, graph data extraction via go-git
|
- `services/git.go` — clone, diff, graph data extraction via go-git
|
||||||
@@ -63,8 +66,9 @@ static/ → CSS (Tailwind output), JS (graph, diff-viewer, sse), vendor libs
|
|||||||
|
|
||||||
## Data Storage
|
## Data Storage
|
||||||
|
|
||||||
- SQLite DB at `data/pr-helper.db` — settings (KV), repositories, analyses, review_notes
|
- MySQL database `pr_helper` — settings (KV), repositories, analyses, review_notes
|
||||||
- Cloned repos cached at `data/repos/` with configurable expiry (default 7 days)
|
- Cloned repos cached at `data/repos/` with configurable expiry (default 7 days)
|
||||||
|
- Database connection configured via `.env` file (see `.env.example`)
|
||||||
|
|
||||||
## Development Notes
|
## Development Notes
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -1,15 +1,14 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM golang:1.24-alpine AS builder
|
FROM golang:1.24-alpine AS builder
|
||||||
RUN apk add --no-cache gcc musl-dev
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN CGO_ENABLED=1 go build -o pr-helper .
|
RUN CGO_ENABLED=0 go build -o pr-helper .
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/pr-helper .
|
COPY --from=builder /app/pr-helper .
|
||||||
COPY --from=builder /app/templates ./templates
|
COPY --from=builder /app/templates ./templates
|
||||||
|
|||||||
+20
-2
@@ -3,8 +3,11 @@ package config
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -12,14 +15,27 @@ type Config struct {
|
|||||||
GinMode string
|
GinMode string
|
||||||
DataDir string
|
DataDir string
|
||||||
SessionSecret string
|
SessionSecret string
|
||||||
|
MySQLHost string
|
||||||
|
MySQLPort string
|
||||||
|
MySQLUser string
|
||||||
|
MySQLPassword string
|
||||||
|
MySQLDatabase string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
|
// Load .env file if present (ignore error if missing)
|
||||||
|
godotenv.Load()
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Port: getEnv("PORT", "8080"),
|
Port: getEnv("PORT", "8080"),
|
||||||
GinMode: getEnv("GIN_MODE", "debug"),
|
GinMode: getEnv("GIN_MODE", "debug"),
|
||||||
DataDir: getEnv("DATA_DIR", "data"),
|
DataDir: getEnv("DATA_DIR", "data"),
|
||||||
SessionSecret: getEnv("SESSION_SECRET", ""),
|
SessionSecret: getEnv("SESSION_SECRET", ""),
|
||||||
|
MySQLHost: getEnv("MYSQL_HOST", "127.0.0.1"),
|
||||||
|
MySQLPort: getEnv("MYSQL_PORT", "3306"),
|
||||||
|
MySQLUser: getEnv("MYSQL_USER", "root"),
|
||||||
|
MySQLPassword: getEnv("MYSQL_PASSWORD", ""),
|
||||||
|
MySQLDatabase: getEnv("MYSQL_DATABASE", "pr_helper"),
|
||||||
}
|
}
|
||||||
// Generate random session secret if not provided
|
// Generate random session secret if not provided
|
||||||
if cfg.SessionSecret == "" {
|
if cfg.SessionSecret == "" {
|
||||||
@@ -33,8 +49,10 @@ func Load() *Config {
|
|||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) DBPath() string {
|
// MySQLDSN returns the MySQL Data Source Name for go-sql-driver/mysql.
|
||||||
return filepath.Join(c.DataDir, "pr-helper.db")
|
func (c *Config) MySQLDSN() string {
|
||||||
|
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||||
|
c.MySQLUser, c.MySQLPassword, c.MySQLHost, c.MySQLPort, c.MySQLDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) ReposDir() string {
|
func (c *Config) ReposDir() string {
|
||||||
|
|||||||
+40
-78
@@ -4,7 +4,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
"github.com/HoHD/PR-Helper/models"
|
"github.com/HoHD/PR-Helper/models"
|
||||||
)
|
)
|
||||||
@@ -13,11 +13,14 @@ type DB struct {
|
|||||||
conn *sql.DB
|
conn *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dbPath string) (*DB, error) {
|
func New(dsn string) (*DB, error) {
|
||||||
conn, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
conn, err := sql.Open("mysql", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := conn.Ping(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
db := &DB{conn: conn}
|
db := &DB{conn: conn}
|
||||||
if err := db.migrate(); err != nil {
|
if err := db.migrate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -39,111 +42,70 @@ func (db *DB) Conn() *sql.DB {
|
|||||||
func (db *DB) migrate() error {
|
func (db *DB) migrate() error {
|
||||||
stmts := []string{
|
stmts := []string{
|
||||||
`CREATE TABLE IF NOT EXISTS settings (
|
`CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
key VARCHAR(255) PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
)`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||||
`CREATE TABLE IF NOT EXISTS users (
|
`CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
email TEXT NOT NULL UNIQUE,
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||||
`CREATE TABLE IF NOT EXISTS user_settings (
|
`CREATE TABLE IF NOT EXISTS user_settings (
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT NOT NULL,
|
||||||
key TEXT NOT NULL,
|
key VARCHAR(255) NOT NULL,
|
||||||
value TEXT NOT NULL,
|
value TEXT NOT NULL,
|
||||||
PRIMARY KEY (user_id, key)
|
PRIMARY KEY (user_id, key),
|
||||||
)`,
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||||
`CREATE TABLE IF NOT EXISTS repositories (
|
`CREATE TABLE IF NOT EXISTS repositories (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT,
|
||||||
url TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
local_path TEXT NOT NULL,
|
local_path TEXT NOT NULL,
|
||||||
size_bytes INTEGER DEFAULT 0,
|
size_bytes BIGINT DEFAULT 0,
|
||||||
cloned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
cloned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
|
last_used DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
)`,
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||||
`CREATE TABLE IF NOT EXISTS analyses (
|
`CREATE TABLE IF NOT EXISTS analyses (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT,
|
||||||
repo_id INTEGER REFERENCES repositories(id),
|
repo_id BIGINT,
|
||||||
type TEXT NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
base_ref TEXT NOT NULL,
|
base_ref TEXT NOT NULL,
|
||||||
head_ref TEXT NOT NULL,
|
head_ref TEXT NOT NULL,
|
||||||
result TEXT,
|
result LONGTEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
)`,
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (repo_id) REFERENCES repositories(id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||||
`CREATE TABLE IF NOT EXISTS review_notes (
|
`CREATE TABLE IF NOT EXISTS review_notes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT,
|
||||||
analysis_id INTEGER REFERENCES analyses(id),
|
analysis_id BIGINT,
|
||||||
scope TEXT NOT NULL,
|
scope VARCHAR(50) NOT NULL,
|
||||||
scope_key TEXT NOT NULL,
|
scope_key TEXT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
)`,
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (analysis_id) REFERENCES analyses(id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||||
}
|
}
|
||||||
for _, s := range stmts {
|
for _, s := range stmts {
|
||||||
if _, err := db.conn.Exec(s); err != nil {
|
if _, err := db.conn.Exec(s); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Run incremental migrations for existing databases
|
|
||||||
if err := db.migrateV2(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// migrateV2 adds user_id columns to existing tables and removes the old UNIQUE constraint on url.
|
|
||||||
func (db *DB) migrateV2() error {
|
|
||||||
migrations := []string{
|
|
||||||
`ALTER TABLE repositories ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE`,
|
|
||||||
`ALTER TABLE analyses ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE`,
|
|
||||||
`ALTER TABLE review_notes ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE`,
|
|
||||||
}
|
|
||||||
for _, m := range migrations {
|
|
||||||
// ALTER TABLE ADD COLUMN will fail if column already exists; ignore the error
|
|
||||||
db.conn.Exec(m)
|
|
||||||
}
|
|
||||||
// Recreate repositories table without the UNIQUE constraint on url (same url can be cloned by different users)
|
|
||||||
// SQLite doesn't support DROP CONSTRAINT, so we use a workaround
|
|
||||||
var hasOldUnique int
|
|
||||||
db.conn.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='repositories_old'`).Scan(&hasOldUnique)
|
|
||||||
if hasOldUnique == 0 {
|
|
||||||
// Check if the UNIQUE constraint exists by trying to detect the old schema
|
|
||||||
// We'll just do the migration idempotently
|
|
||||||
db.conn.Exec(`CREATE TABLE IF NOT EXISTS repositories_new (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
url TEXT NOT NULL,
|
|
||||||
local_path TEXT NOT NULL,
|
|
||||||
size_bytes INTEGER DEFAULT 0,
|
|
||||||
cloned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)`)
|
|
||||||
db.conn.Exec(`INSERT OR IGNORE INTO repositories_new (id, user_id, url, local_path, size_bytes, cloned_at, last_used)
|
|
||||||
SELECT id, user_id, url, local_path, size_bytes, cloned_at, last_used FROM repositories`)
|
|
||||||
// Only swap if the new table has data or old table is empty
|
|
||||||
var newCount, oldCount int
|
|
||||||
db.conn.QueryRow(`SELECT COUNT(*) FROM repositories_new`).Scan(&newCount)
|
|
||||||
db.conn.QueryRow(`SELECT COUNT(*) FROM repositories`).Scan(&oldCount)
|
|
||||||
if newCount >= oldCount {
|
|
||||||
db.conn.Exec(`DROP TABLE repositories`)
|
|
||||||
db.conn.Exec(`ALTER TABLE repositories_new RENAME TO repositories`)
|
|
||||||
} else {
|
|
||||||
db.conn.Exec(`DROP TABLE repositories_new`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) seedDefaults() error {
|
func (db *DB) seedDefaults() error {
|
||||||
for key, val := range models.DefaultSettings {
|
for key, val := range models.DefaultSettings {
|
||||||
_, err := db.conn.Exec(
|
_, err := db.conn.Exec(
|
||||||
`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, key, val,
|
`INSERT IGNORE INTO settings (key, value) VALUES (?, ?)`, key, val,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("warning: failed to seed setting %s: %v", key, err)
|
log.Printf("warning: failed to seed setting %s: %v", key, err)
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
services:
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD:-pr_helper_pass}
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-pr_helper}
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
pr-helper:
|
pr-helper:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
@@ -7,7 +23,16 @@ services:
|
|||||||
- pr-helper-data:/app/data
|
- pr-helper-data:/app/data
|
||||||
environment:
|
environment:
|
||||||
- GIN_MODE=release
|
- GIN_MODE=release
|
||||||
|
- MYSQL_HOST=mysql
|
||||||
|
- MYSQL_PORT=3306
|
||||||
|
- MYSQL_USER=root
|
||||||
|
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-pr_helper_pass}
|
||||||
|
- MYSQL_DATABASE=${MYSQL_DATABASE:-pr_helper}
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
mysql-data:
|
||||||
pr-helper-data:
|
pr-helper-data:
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ require (
|
|||||||
github.com/gin-contrib/sessions v1.1.0
|
github.com/gin-contrib/sessions v1.1.0
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/go-git/go-git/v5 v5.19.1
|
github.com/go-git/go-git/v5 v5.19.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/go-sql-driver/mysql v1.10.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/sashabaranov/go-openai v1.41.2
|
github.com/sashabaranov/go-openai v1.41.2
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
@@ -54,6 +56,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
@@ -73,6 +77,8 @@ github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzq
|
|||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
@@ -90,8 +96,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
|||||||
+1
-1
@@ -126,7 +126,7 @@ func (h *AuthHandler) HandleRegister(c *gin.Context) {
|
|||||||
|
|
||||||
// Seed default settings for the user
|
// Seed default settings for the user
|
||||||
for key, val := range defaultUserSettings {
|
for key, val := range defaultUserSettings {
|
||||||
h.db.Exec(`INSERT OR IGNORE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)`, userID, key, val)
|
h.db.Exec(`INSERT IGNORE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)`, userID, key, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-login: set session
|
// Auto-login: set session
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func (h *GenerateHandler) Generate(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update last_used
|
// Update last_used
|
||||||
h.db.Exec(`UPDATE repositories SET last_used = datetime('now') WHERE id = ?`, id)
|
h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id)
|
||||||
|
|
||||||
// Generate PR description (pass user ID for per-user LLM config)
|
// Generate PR description (pass user ID for per-user LLM config)
|
||||||
pr, err := services.GeneratePR(h.db, localPath, req.Base, req.Head, user.ID, sendEvent)
|
pr, err := services.GeneratePR(h.db, localPath, req.Base, req.Head, user.ID, sendEvent)
|
||||||
|
|||||||
+3
-3
@@ -104,7 +104,7 @@ func (h *ReposHandler) CleanupRepos(c *gin.Context) {
|
|||||||
if maxAgeDays == "" {
|
if maxAgeDays == "" {
|
||||||
maxAgeDays = "7"
|
maxAgeDays = "7"
|
||||||
}
|
}
|
||||||
rows, err := h.db.Query(`SELECT id, local_path FROM repositories WHERE user_id = ? AND last_used < datetime('now', '-' || ? || ' days')`, user.ID, maxAgeDays)
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -244,7 +244,7 @@ func (h *ReposHandler) GetGraph(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update last_used
|
// Update last_used
|
||||||
h.db.Exec(`UPDATE repositories SET last_used = datetime('now') WHERE id = ?`, id)
|
h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id)
|
||||||
|
|
||||||
repo, err := services.OpenRepo(localPath)
|
repo, err := services.OpenRepo(localPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -297,7 +297,7 @@ func (h *ReposHandler) GetDiff(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update last_used
|
// Update last_used
|
||||||
h.db.Exec(`UPDATE repositories SET last_used = datetime('now') WHERE id = ?`, id)
|
h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id)
|
||||||
|
|
||||||
repo, err := services.OpenRepo(localPath)
|
repo, err := services.OpenRepo(localPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ func (h *ReviewHandler) Review(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update last_used
|
// Update last_used
|
||||||
h.db.Exec(`UPDATE repositories SET last_used = datetime('now') WHERE id = ?`, id)
|
h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id)
|
||||||
|
|
||||||
// Run AI review (pass user ID for per-user LLM config)
|
// Run AI review (pass user ID for per-user LLM config)
|
||||||
reviewResult, err := services.GenerateReview(h.db, localPath, req.Base, req.Head, topN, concurrency, user.ID, sendEvent)
|
reviewResult, err := services.GenerateReview(h.db, localPath, req.Base, req.Head, topN, concurrency, user.ID, sendEvent)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for key, val := range body {
|
for key, val := range body {
|
||||||
_, err := tx.Exec(`INSERT OR REPLACE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)`, user.ID, key, val)
|
_, err := tx.Exec(`INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value)`, user.ID, key, val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
|
||||||
db, err := database.New(cfg.DBPath())
|
db, err := database.New(cfg.MySQLDSN())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to initialize database: %v", err)
|
log.Fatalf("failed to initialize database: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user